最近在帮学弟学妹们看毕业设计发现“电影购票系统”真是个高频选题。想法很美好但真做起来尤其是在模拟高并发场景时各种问题就冒出来了座位明明显示可选一提交就冲突库存扣减混乱电影票被“超卖”网络波动下同一个订单重复生成……这些问题不解决答辩时被老师一问很容易露怯。今天我就结合一个实际帮他们优化过的项目聊聊怎么用一套相对成熟的技术栈Spring Boot Redis RabbitMQ把这些坑一个个填平打造一个既有“面子”功能完整又有“里子”架构扎实的毕设项目。1. 背景与核心痛点为什么简单的“增删改查”会翻车很多同学一开始觉得购票系统不就是用户选座、下单、支付吗用 Spring Boot 写几个 CRUD 接口连上 MySQL 不就搞定了但一旦模拟多人同时抢票系统就变得非常脆弱。主要痛点集中在三个方面选座与库存的并发一致性这是最经典的问题。假设《流浪地球3》的 A 厅 5排6座只剩一张票。用户 A 和用户 B 几乎同时查询到这个座位“可用”然后都发起了下单请求。如果只是简单地在代码里先select再update很可能会把这一张票卖给两个人造成“超卖”。数据库的行锁在应用层高并发下防不住这种“判断后行动”的逻辑漏洞。订单的幂等性在抢票高峰网络可能不稳定。用户点击“提交订单”后前端没及时收到响应可能会再次点击或者客户端自动重试。如果后端没有防护就会基于同一个请求创建出两个一模一样的订单。用户付一次钱却生成了两个待支付的订单体验极差也容易引发资损纠纷。服务性能与耦合下单流程可能涉及扣减库存、生成订单、记录日志、发送短信通知等。如果全部在一个数据库事务里同步完成事务时间会拉得很长数据库连接池容易被耗光导致系统响应变慢甚至崩溃。特别是发短信这种第三方调用失败率高且慢不应该阻塞核心的下单交易。2. 技术选型为什么是它们面对这些问题我们需要选择合适的“武器”。为什么选 Spring Boot 而不是 Python 的 Django/Flask对于 Java 技术栈的同学来说Spring Boot 生态成熟、资料丰富是找工作和应对答辩的“安全牌”。它集成了 Spring 全家桶做微服务拆分哪怕毕设里只模拟两三个服务也很方便。Django 的 ORM 和 Admin 虽然开发快但在应对复杂业务逻辑、精细控制并发和深度集成消息队列方面Spring Boot 的灵活性和企业级特性更胜一筹。而且Java 在并发编程方面的工具包JUC非常强大便于我们实现各种锁和同步机制。为什么用 Redis 实现分布式锁而不是 ZooKeeper核心诉求是高性能的互斥访问。在扣减库存、锁定座位的场景下我们需要一个全局的、高性能的协调者。Redis基于内存性能极高SETNXSET if Not eXists命令天然适合实现锁。配合过期时间可以防止死锁。对于毕设级别的并发量几百到几千 QPSRedis 完全够用而且部署简单学习成本低。ZooKeeper强一致性保证通过创建有序临时节点来实现锁更安全可靠。但它性能不如 Redis部署和运维也更复杂。在我们的购票场景下“高性能”的优先级高于“极端情况下的绝对一致性”Redis 锁在极端主从切换时可能有问题但毕设场景可忽略。结论毕设追求的是在有限时间内用合适的工具解决核心问题。Redis 简单、高效、够用是更务实的选择。3. 核心实现细节拆解高并发下单流程整个下单流程我们把它设计成一个“校验锁定 - 异步下单 - 最终一致”的流程。第一步基于 Redis Lua 的原子化库存扣减与座位锁定这是防止超卖的关键。我们不能用先查后改的 Java 代码而要把“判断库存”和“扣减库存”变成一个原子操作。Redis 的 Lua 脚本完美符合要求因为它在 Redis 服务器端原子性执行。我们设计一个 Redis 数据结构来存每个场次的座位库存Key:stock:schedule:{scheduleId}Value: 可用座位数 (integer)同时为了锁定具体座位防止同一个座位被多人选中Key:seat_lock:schedule:{scheduleId}:seat:{seatId}Value: 用户ID 或订单令牌 (string)并设置过期时间如10分钟扣减库存和锁定座位的 Lua 脚本大致如下-- KEYS[1]: 库存key, KEYS[2]: 座位锁key -- ARGV[1]: 要购买的数量通常是1, ARGV[2]: 锁定的用户标识 local stock tonumber(redis.call(get, KEYS[1])) if stock and stock tonumber(ARGV[1]) then -- 库存充足尝试锁定座位 local lockSuccess redis.call(setnx, KEYS[2], ARGV[2]) if lockSuccess 1 then -- 设置锁过期时间防止死锁 redis.call(expire, KEYS[2], 600) -- 扣减库存 redis.call(decrby, KEYS[1], ARGV[1]) return 1 -- 成功 else return -1 -- 座位已被锁定 end else return 0 -- 库存不足 end在 Java 代码中我们通过Spring Data Redis的RedisTemplate来执行这个脚本。如果脚本返回 1表示前置校验和锁定成功可以进入下一步。第二步基于 RabbitMQ 的异步下单与削峰填谷前置校验成功后我们并不直接操作数据库下单而是向消息队列发送一条“下单任务”消息。这样做的好处是快速响应前端发送消息很快前端立刻得知“请求已接受正在处理中”。削峰突如其来的抢票请求会被堆积在消息队列里后端服务可以按照自己的能力匀速消费避免数据库被瞬间击垮。解耦下单核心逻辑、支付回调、短信通知等可以拆分成不同的消费者互不影响。消息内容至少包含用户ID、场次ID、座位信息、之前生成的唯一订单令牌。Component Slf4j public class OrderProducer { Autowired private RabbitTemplate rabbitTemplate; public void sendOrderCreateTask(OrderCreateMessage message) { // 建议为消息设置一个全局唯一ID用于后续幂等性判断 message.setMessageId(UUID.randomUUID().toString()); CorrelationData correlationData new CorrelationData(message.getMessageId()); // 发送到订单创建交换器 rabbitTemplate.convertAndSend(order.exchange, order.create, message, correlationData); log.info(订单创建消息已发送: {}, message.getMessageId()); } }4. 关键代码片段清晰、健壮的下单消费者消息的消费者是核心业务逻辑所在。这里要处理好幂等性、数据库事务和错误重试。Component Slf4j public class OrderCreateConsumer { Autowired private OrderService orderService; Autowired private RedisTemplateString, String redisTemplate; // 监听订单创建队列 RabbitListener(queues order.create.queue) Transactional(rollbackFor Exception.class) // 开启本地事务 public void handleOrderCreate(OrderCreateMessage message, Channel channel, Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws IOException { String messageId message.getMessageId(); String orderToken message.getOrderToken(); // 前置校验阶段生成的令牌 // --- 幂等性校验防止消息重复消费 --- String redisKey order_msg: messageId; Boolean isFirstProcess redisTemplate.opsForValue().setIfAbsent(redisKey, PROCESSED, 10, TimeUnit.MINUTES); if (Boolean.FALSE.equals(isFirstProcess)) { log.warn(消息 {} 已被消费跳过处理, messageId); channel.basicAck(deliveryTag, false); // 确认消息避免重复投递 return; } try { // --- 核心下单逻辑 --- // 1. 再次校验可选但建议根据 orderToken 检查 Redis 中的座位锁是否仍属于当前用户 // 2. 创建订单实体状态为“待支付” Order order new Order(); order.setOrderNo(generateOrderNo()); // 生成唯一订单号 order.setUserId(message.getUserId()); order.setScheduleId(message.getScheduleId()); order.setSeats(message.getSeats()); order.setStatus(OrderStatus.WAITING_PAYMENT); order.setCreateTime(new Date()); // 3. 写入数据库 orderService.save(order); // 4. 真实扣减数据库库存这里可以乐观锁version字段控制 int updateCount scheduleSeatMapper.reduceStock(message.getScheduleId(), message.getSeats().size()); if (updateCount 0) { // 数据库层面库存不足理论上不会发生因为Redis已校验抛出异常回滚事务 throw new RuntimeException(Database stock insufficient); } log.info(订单创建成功订单号: {}, order.getOrderNo()); // 事务提交后可以触发后续动作如发送延迟消息检查支付状态 channel.basicAck(deliveryTag, false); // 业务成功确认消息 } catch (Exception e) { log.error(订单创建失败消息ID: {}, 错误: , messageId, e); // 业务失败根据异常类型决定是重试还是丢弃 // 如果是网络抖动等可重试异常可以 nack 并 requeue // channel.basicNack(deliveryTag, false, true); // 如果是业务逻辑错误如库存真没了应直接确认消息避免无限重试 channel.basicAck(deliveryTag, false); // 注意这里ack了但数据库事务会回滚保证了数据一致性 // 需要记录失败日志并可能触发补偿机制如释放Redis中的座位锁 releaseSeatLockInRedis(message); // 补偿释放锁 } } }5. 性能与安全考量让项目更“硬核”压力测试使用 JMeter 模拟 500-1000 个用户并发抢票。关注几个核心指标QPS每秒查询率前置校验Redis操作的 QPS 应该很高几千上万。下单接口消息入队的 QPS 也会不错。最终数据库写入的 QPS 取决于消费者的数量和能力。错误率在库存充足的情况下错误率应接近于 0。库存售罄后应快速返回“已售完”提示而不是服务器错误。响应时间95% 的请求响应时间应在 200ms 以内主要指前置校验和消息入队阶段。防刷票与安全令牌Token机制前端在进入选座页时向后端请求一个一次性令牌。提交订单时必须带上此令牌后端验证后即失效防止重复提交和脚本刷票。限流在网关或应用层对/api/order/create接口按用户ID进行限流如每秒最多 2 次请求。SQL 注入防护坚持使用 MyBatis 的#{}预编译占位符绝不拼接 SQL 字符串。这是最基本的要求。6. 生产环境避坑指南进阶思考即使作为毕设了解这些潜在问题也能让你的答辩更有深度。Redis 缓存击穿如果某个热门场次的库存 Key 在缓存过期瞬间遭遇大量请求会全部打到数据库。解决方案使用 Redis 的SETNX实现一个简单的互斥锁让一个请求去重建缓存其他请求等待或使用旧数据短暂返回。消息积压如果消费者处理太慢消息队列会堆积。监控队列长度是必须的。可以动态增加消费者实例或者检查消费者代码是否有性能瓶颈。对于非核心消息如通知类可以准备一个降级策略直接丢弃或存入日志后续处理。本地事务与 MQ 的最终一致性我们上面的代码模式是“先发 MQ 消息再处理本地事务”。如果事务回滚消息却已发出就会导致数据不一致。我们采用的补偿机制在 catch 块中释放座位锁是一种事后补救。更严谨的做法是使用“事务消息”或“本地消息表”方案但这对于毕设来说复杂度较高可以作为扩展方向来阐述。结尾与扩展思考通过上面这一套组合拳你的电影购票毕设就已经从一个简单的 CRUD 项目升级为一个初步具备应对高并发能力、考虑了一致性与可用性的“微型分布式系统”了。这无论是在代码量、技术深度还是答辩陈述上都会是很大的亮点。当然这只是一个单影院、单厅的模型。你可以进一步思考如何扩展成支持多影院、多影厅的连锁购票系统数据层面影院、影厅、场次、座位模型需要重新设计层次关系更复杂。库存层面库存数据可能需要按影院、甚至按影厅进行分片存储不能再用一个全局 Redis Key。架构层面可以考虑将“影院管理”、“排片管理”、“订单服务”、“用户服务”拆分成独立的微服务通过 API 网关聚合。部署层面考虑如何做多机房部署让用户就近访问。建议你不妨以现在的系统为模板动手尝试重构增加这些功能。这个过程会让你对分布式系统有更深刻的理解。希望这篇笔记能为你带来启发祝你毕设顺利答辩高分