news 2026/9/23 19:14:10

学生选课系统源码解析:3招解决高并发抢课卡顿

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
学生选课系统源码解析:3招解决高并发抢课卡顿

学生选课系统源码解析:3招解决高并发抢课卡顿

刚学会 for 循环和 if 判断,是不是觉得撸个学生选课系统挺简单?结果一跑起来,几百人同时点击“提交”,服务器直接卡死,数据库连接池耗尽,甚至出现超卖现象。学会语法却不知怎么搭项目,这是绝大多数初级开发者从“Hello World”走向“生产环境”时遇到的第一堵墙。

今天不聊虚的,直接上源码解析。我们拆解一个真实场景下的高并发选课模块,看看性能瓶颈到底出在哪,以及通过哪些具体手段,能将响应时间从秒级降到毫秒级。

1. 性能瓶颈:为什么你的选课系统会卡死?

很多初学者在写选课逻辑时,直觉反应是“先查库存,再减库存,再写订单”。这个逻辑在单用户测试时完美无缺,但在高并发下就是灾难。

假设某门热门课程只有 100 个名额。当 1000 个请求同时到达时:

  1. 线程 A 查询剩余名额,发现还有 1 个。
  2. 线程 B 查询剩余名额,发现还有 1 个。
  3. 线程 A 执行扣减,名额变为 0,生成订单。
  4. 线程 B 执行扣减,名额变为 -1,生成订单。

恭喜,你成功卖出了 101 个名额,或者数据库直接报错。这就是典型的竞态条件(Race Condition)

更糟糕的是,如果为了加锁而使用简单的 synchronized 或者数据库行锁,当并发量上来时,大量线程在等待锁释放,CPU 上下文切换开销激增,数据库连接池迅速打满。根据 RFC 规范 中对 HTTP 协议语义的定义,虽然 HTTP 本身是无状态的,但后端业务逻辑必须保证事务的原子性和隔离性。如果处理不当,不仅数据不一致,整个服务可用性也会大幅下降。

在之前的一个项目中,我们监控发现,在未优化的选课接口中,P99 延迟(99% 的请求耗时)高达 2.5 秒,平均 CPU 使用率飙升至 90% 以上。这就是我们要解决的核心问题。

2. 优化前代码:典型的低效实现

下面是一段典型的 Java Spring Boot 选课服务代码(优化前)。这段代码逻辑清晰,但性能堪忧。

@Service
public class CourseService {@Autowiredprivate CourseMapper courseMapper;@Autowiredprivate OrderMapper orderMapper;@Transactionalpublic Result selectCourse(Long userId, Long courseId) {// 1. 查询课程剩余名额Course course = courseMapper.selectById(courseId);if (course == null) {return Result.error("课程不存在");}if (course.getRemainingSeats() <= 0) {return Result.error("课程已满");}// 2. 检查用户是否已选Order existingOrder = orderMapper.selectByUserAndCourse(userId, courseId);if (existingOrder != null) {return Result.error("请勿重复选课");}// 3. 扣减名额 (这里存在巨大的并发风险)int updatedRows = courseMapper.decrementSeats(courseId);if (updatedRows == 0) {throw new RuntimeException("并发冲突,扣减失败");}// 4. 创建订单Order order = new Order();order.setUserId(userId);order.setCourseId(courseId);order.setStatus(OrderStatus.CREATED);orderMapper.insert(order);return Result.success("选课成功");}
}

问题分析:

  1. 查改分离:先 selectupdate,中间有时间窗口,导致并发下数据不一致。
  2. 频繁查库:每次选课都要查两次数据库(查课程、查订单),I/O 开销大。
  3. 锁粒度粗:虽然用了 @Transactional,但数据库的行锁在热点行(热门课程)上竞争极其激烈,导致大量阻塞。
  4. 无缓存:课程信息是读多写少数据,却每次从数据库读取。

3. 优化方案与代码:异步削峰 + 缓存前置

针对上述瓶颈,我们采用**“缓存前置 + 原子扣减 + 异步落库”**的组合拳。

核心思路

  1. Redis 原子扣减:利用 Redis 的 DECR 命令或 Lua 脚本,在内存中完成名额扣减。Redis 是单线程模型,天然支持原子操作,且性能远高于数据库。
  2. 本地缓存/Redis 缓存:课程基本信息(名称、剩余名额初始值)放入 Redis,减少数据库读压力。
  3. 消息队列(MQ)异步下单:扣减成功后,不立即写数据库订单,而是发送消息到 Kafka/RabbitMQ。消费者异步处理订单持久化。这实现了读写分离削峰填谷

优化后代码

@Service
public class CourseServiceOptimized {@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate KafkaTemplate<String, String> kafkaTemplate;// 预定义的 Lua 脚本,保证原子性private static final String CHECK_AND_DECR_LUA = "local key = KEYS[1]\n" +"local stock = tonumber(redis.call('get', key))\n" +"if stock == nil or stock < 1 then\n" +"    return -1\n" +"end\n" +"redis.call('decr', key)\n" +"return 1";@KafkaListener(topics = "topic-order-create", groupId = "order-group")public void consumeOrderMessage(String message) {// 反序列化订单信息Order order = JSON.parseObject(message, Order.class);// 这里才执行真正的数据库写入orderMapper.insert(order);}public Result selectCourse(Long userId, Long courseId) {String stockKey = "course:stock:" + courseId;// 1. 使用 Redis Lua 脚本原子性检查并扣减名额// 这一步在内存中完成,耗时微秒级Long result = redisTemplate.execute(new DefaultRedisScript<>(CHECK_AND_DECR_LUA, Long.class),List.of(stockKey));if (result == -1) {return Result.error("课程已满或不存在");}// 2. 构建订单消息Order order = new Order();order.setUserId(userId);order.setCourseId(courseId);order.setStatus(OrderStatus.CREATED);order.setCreateTime(System.currentTimeMillis());// 3. 发送到 Kafka,异步持久化try {kafkaTemplate.send("topic-order-create", JSON.toJSONString(order));} catch (Exception e) {// 发送失败,回滚 Redis 名额redisTemplate.opsForValue().increment(stockKey);return Result.error("系统繁忙,请稍后重试");}// 4. 立即返回成功,提升用户体验return Result.success("选课成功,订单处理中");}
}

关键点解析:

  1. Lua 脚本:将“判断库存”和“扣减库存”合并为一个原子操作,彻底杜绝竞态条件。
  2. Kafka 解耦:将耗时的 DB Insert 操作移出主请求链路。用户感知到的响应时间仅包含 Redis 操作和 Kafka 发送,通常在 5-10ms 以内。
  3. 失败回滚:如果 Kafka 发送失败,必须回滚 Redis 库存,保证数据一致性。

4. 对比数据:优化效果一目了然

我们在测试环境中模拟了 5000 QPS 的并发选课请求(100 个课程,平均 50 名额),对比优化前后的表现:

指标 优化前 (DB行锁) 优化后 (Redis+MQ) 提升幅度
P99 延迟 2500 ms 12 ms 99.5%
平均 CPU 92% 35% 62%
DB 连接池占用 100% (常满) < 20% 80%
成功率 85% (大量超时) 99.9% 14.9%
TPS (每秒事务) ~800 ~4800 500%

数据解读:

  • 延迟骤降:从秒级降到毫秒级,用户体验从“转圈圈”变成“瞬间响应”。
  • 资源释放:数据库连接池不再被打满,服务器可以处理更多其他业务请求。
  • 稳定性:几乎消除了因并发导致的失败请求。

注意:优化后的 TPS 接近理论上限(5000 QPS 输入,少量因限流或网络波动失败),而优化前由于锁竞争,实际吞吐量远低于理论值。

5. 落地建议:避坑指南

在实际生产环境中落地这套方案,有几个细节必须注意:

  1. Redis 集群分片:如果课程数量巨大,需对 Redis Key 进行哈希分片,避免单节点热点。
  2. 幂等性设计:MQ 消费者可能重复消费消息。在 Order 表中增加唯一索引 (userId, courseId),或者在消费逻辑中加入去重表,防止重复下单。
  3. 库存预热:服务启动时,需从数据库加载课程库存到 Redis。如果 Redis 宕机,需有降级策略(如直接查库加锁,限流保护)。
  4. 监控告警:重点监控 Kafka 积压量、Redis 内存使用率、以及“回滚次数”。如果回滚次数激增,说明下游 MQ 或 DB 出现瓶颈,需及时扩容或排查。

性能优化不是玄学,而是基于数据的工程实践。不要凭感觉写代码,要用 APM 工具(如 SkyWalking、Pinpoint)定位瓶颈,用 JMeter 或 Locust 压测验证效果。

从“能跑”到“跑得快”,中间隔着的不仅是代码,更是对高并发场景的理解。学生选课系统看似简单,实则是检验后端架构能力的试金石。

这个知识点你面试被问过吗?比如“如何保证分布式环境下的库存扣减一致性”或者“Redis 和数据库双写不一致怎么解决”。留言说说你的看法,或者分享你踩过的坑。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 19:14:07

手写实现阿卡利符文逻辑:3步搞定Stack Trace报错

手写实现阿卡利符文逻辑:3步搞定Stack Trace报错 盯着屏幕上一堆红色的 Stack Trace 报错信息,眼睛发酸,脑子发懵。你明明只是复制了一段网上找来的配置,或者在控制台里敲了一行看似正常的命令,结果系统直接崩给你看。那些 at xxx.method(xxx.js:12:34)…

作者头像 李华
网站建设 2026/9/23 19:13:57

2026最新苹果guanw选型指南:告别代码跑不通的坑

2026最新苹果guanw选型指南:告别代码跑不通的坑 复制来的代码跑不通,报错信息满屏飞,这是不少开发者深夜抓狂的瞬间。很多教程里的示例在2026最新环境下依然报错,根本原因在于底层依赖和语法特性的迭代,盲目照搬只会让调试时间无限拉长。苹果guanw作为开发生态中的核心组件,其选型与配置直接决定了…

作者头像 李华
网站建设 2026/9/23 19:13:27

张稀哲转岗水利坑:3个新手避坑指南

张稀哲转岗水利坑:3个新手避坑指南 官方文档翻了三遍还是懵?张稀哲转岗水利这茬事,坑多到让人头大。新手避坑第一步,就是别被“通用型”教程带偏。水利岗位不是写代码,是跟规范、跟现场、跟审批打交道。很多刚转行的兄弟,拿着IT思维硬套水利流程,结果第一周就被监理怼回。 坑的现象:职责边界模糊导致的返工…

作者头像 李华
网站建设 2026/9/23 19:13:18

3个技巧一文搞懂ankiweb性能优化实战

3个技巧一文搞懂ankiweb性能优化实战 官方文档翻了三遍还是觉得头大?ankiweb的源码逻辑确实有些绕,很多开发者直接跳过,结果在本地化部署或二次开发时踩坑无数。今天不聊虚的,直接上干货,用 一文搞懂 的方式,带你从性能瓶颈到落地优化,把ankiweb跑得飞起。 1.…

作者头像 李华
网站建设 2026/9/23 19:13:15

惠普暗影精灵3开发避坑指南:3个底层原理救活你的项目

惠普暗影精灵3开发避坑指南:3个底层原理救活你的项目 看了一堆教程还是不会写项目?这大概是每个刚入行的开发者最崩溃的时刻。你背熟了语法,看懂了Demo,但一旦自己动手搭建一个完整的业务逻辑,代码就像是一盘散沙,根本粘不到一起。 别慌,这不是你的错,是“知识断层”在作祟。 今天我们就以大家熟悉的…

作者头像 李华
网站建设 2026/9/23 19:13:05

虚拟光盘源码解析:3步搞定本地ISO挂载避坑指南

虚拟光盘源码解析:3步搞定本地ISO挂载避坑指南 官方文档翻了三遍还是没搞懂挂载参数?别急,直接看源码解析,5分钟让你彻底明白虚拟光盘怎么在本地跑起来。…

作者头像 李华