尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

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

发布时间:2026/9/23 19:14:13

资讯中心
01
ARTICLE

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

学生选课系统源码解析:3招解决高并发抢课卡顿
学生选课系统源码解析:3招解决高并发抢课卡顿 刚学会 for 循环和 if 判断,是不是觉得撸个学生选课系统挺简单?结果一跑起来,几百人同时点击“提交”,服务器直接卡死,数据库连接池耗尽,甚至出现超卖现象。学会语法却不知怎么搭项目,这是绝大多数初级开发者从“Hello World”走向“生产环境”时遇到的第一堵墙。 今天不聊虚的,直接上源码解析。我们拆解一个真实场景下的高并发选课模块,看看性能瓶颈到底出在哪,以及通过哪些具体手段,能将响应时间从秒级降到毫秒级。 1. 性能瓶颈:为什么你的选课系统会卡死? 很多初学者在写选课逻辑时,直觉反应是“先查库存,再减库存,再写订单”。这个逻辑在单用户测试时完美无缺,但在高并发下就是灾难。 假设某门热门课程只有 100 个名额。当 1000 个请求同时到达时:线程 A 查询剩余名额,发现还有 1 个。 线程 B 查询剩余名额,发现还有 1 个。 线程 A 执行扣减,名额变为 0,生成订单。 线程 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(选课成功);} }问题分析:查改分离:先 select 再 update,中间有时间窗口,导致并发下数据不一致。 频繁查库:每次选课都要查两次数据库(查课程、查订单),I/O 开销大。 锁粒度粗:虽然用了 @Transactional,但数据库的行锁在热点行(热门课程)上竞争极其激烈,导致大量阻塞。 无缓存:课程信息是读多写少数据,却每次从数据库读取。3. 优化方案与代码:异步削峰 + 缓存前置 针对上述瓶颈,我们采用**“缓存前置 + 原子扣减 + 异步落库”**的组合拳。 核心思路Redis 原子扣减:利用 Redis 的 DECR 命令或 Lua 脚本,在内存中完成名额扣减。Redis 是单线程模型,天然支持原子操作,且性能远高于数据库。 本地缓存/Redis 缓存:课程基本信息(名称、剩余名额初始值)放入 Redis,减少数据库读压力。 消息队列(MQ)异步下单:扣减成功后,不立即写数据库订单,而是发送消息到 Kafka/RabbitMQ。消费者异步处理订单持久化。这实现了读写分离和削峰填谷。优化后代码 @Service public class CourseServiceOptimized {@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate KafkaTemplateString, 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(选课成功,订单处理中);} }关键点解析:Lua 脚本:将“判断库存”和“扣减库存”合并为一个原子操作,彻底杜绝竞态条件。 Kafka 解耦:将耗时的 DB Insert 操作移出主请求链路。用户感知到的响应时间仅包含 Redis 操作和 Kafka 发送,通常在 5-10ms 以内。 失败回滚:如果 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. 落地建议:避坑指南 在实际生产环境中落地这套方案,有几个细节必须注意:Redis 集群分片:如果课程数量巨大,需对 Redis Key 进行哈希分片,避免单节点热点。 幂等性设计:MQ 消费者可能重复消费消息。在 Order 表中增加唯一索引 (userId, courseId),或者在消费逻辑中加入去重表,防止重复下单。 库存预热:服务启动时,需从数据库加载课程库存到 Redis。如果 Redis 宕机,需有降级策略(如直接查库加锁,限流保护)。 监控告警:重点监控 Kafka 积压量、Redis 内存使用率、以及“回滚次数”。如果回滚次数激增,说明下游 MQ 或 DB 出现瓶颈,需及时扩容或排查。性能优化不是玄学,而是基于数据的工程实践。不要凭感觉写代码,要用 APM 工具(如 SkyWalking、Pinpoint)定位瓶颈,用 JMeter 或 Locust 压测验证效果。 从“能跑”到“跑得快”,中间隔着的不仅是代码,更是对高并发场景的理解。学生选课系统看似简单,实则是检验后端架构能力的试金石。 这个知识点你面试被问过吗?比如“如何保证分布式环境下的库存扣减一致性”或者“Redis 和数据库双写不一致怎么解决”。留言说说你的看法,或者分享你踩过的坑。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。