开学季集中选课触发分布式锁竞争时,首要解法是缩小锁粒度、用乐观锁替换互斥锁、并将抢课流程改为异步化,一把锁守护全校课程必然出现排队堆积,把锁拆小、拆短、拆走,才能让选课系统真正扛住瞬时峰值。
每年开学季,选课系统都要经历一轮“开闸式”流量冲击,上午十点整点开抢,几千名学生同时按住刷新键,课表接口、课程详情、选课按钮全部被同一批热点数据打满,此时如果后端用一把分布式锁保护所有写操作,等锁线程会在Redis和业务服务之间来回踢皮球,直到把服务拖到超时,分布式锁竞争问题不是锁本身坏了,而是锁的边界没划对。
开学季选课 分布式锁竞争 怎么缓解:先把锁粒度拆开
锁粒度太粗是竞争失控的主因。 多数教务系统早期的设计方式很直观:所有选课请求进入一个统一入口,用一把Redis锁把后续的库存校验、名额扣减、已选列表写入全部串行化,这种方式在并发量几十的时候毫无压力,一旦涌进几千个请求,所有学生都在同一把锁上排队,锁持有时间又被后面的重复校验无限拉长,超时雪崩几乎是必然结局。
整个选课流程里,真正需要互斥的只有一处:库存扣减那一刻的“判断+更新”原子性,学生信息加载、已选课程列表查询、页面渲染这些都是只读操作,完全不该进锁,锁内做重活,等于把食堂打饭窗口的排队时间全浪费在掏饭卡上。
缓解竞争的第一步,是把锁从“全校一把”拆成“每门课程一把”。
- 锁key从
lock:select_course改成lock:course:{courseId} - 只有一个课程的热门课,只会挡住抢这门课的人,不影响其他课程正常选课
- 热门课程再按开课校区、教学班编号二次拆分,同一门课的并行冲突进一步下降
拆锁之后,Redis里同时存在的锁数量增多,但每把锁的争抢概率大幅下降,行业共识认为,选课峰值期的分布式锁竞争,七成问题来自锁粒度设计不合理,而非Redis本身的性能上限。
把持锁时间压到最短。 锁内只保留库存扣减这一步操作,具体流程是:
- 请求进来后,先在Redis里用
SET key value NX PX 3000抢锁 - 抢锁成功后,只执行一条扣减库存的Lua脚本
- 立即释放锁,后续的学生选课记录异步写入数据库
用锁保护一段只花费几十毫秒的关键区,才能让单把锁的吞吐量从每秒几十提升到每秒几百,锁内跑一个涉及多张表的事务,持锁时间直接指数级增长,再多的锁也都是摆设。
教务系统选课 分布式锁 用Redis还是数据库乐观锁
选课业务的核心矛盾不是“锁性能”,而是“库存扣减不能超卖”。

理解这一点,就不必纠结所有环节都用Redis分布式锁,库存校验和扣减是典型的“比较并交换”操作,数据库原生支持原子更新,用一个条件判断就可以替代锁:
UPDATE course_stock SET stock = stock - 1
WHERE course_id = #{courseId} AND stock > 0;
这条语句是原子的,数据库的行锁只在执行期间存在,执行完立即释放,不存在Redis分布式锁的续期、超时、手动释放问题,选课系统把库存扣减交给数据库,Redis分布式锁则退居二线,只保护“同一学生同时提交多次选课请求”这一个场景。双写校验的做法在教务系统实践中相当普遍:Redis先做一次预扣减挡住大流量,数据库再以乐观锁做最终校验,两道闸门各司其职。
对比一下两种方案在选课场景的真实差异,能帮助选型更清晰:
| 对比项 | Redis分布式锁 | 数据库乐观锁 |
|---|---|---|
| 锁粒度 | 灵活可拆,按课程/分片拆分 | 行级,天然精准 |
| 抗高并发 | 好,取决于Redis QPS | 一般,数据库连接是瓶颈 |
| 持锁时长 | 必须手动控制,超时需处理 | 事务提交即释放,自动 |
| 超卖防护 | 需配合Lua脚本保证原子性 | WHERE条件天然防超卖 |
| 运维成本 | 需保证Redis高可用 | 需关注数据库连接池 |
| 失败处理 | 需重试队列或提示稍后 | 影响行数=0即可返回已满 |
最终落地时,多数学校选的是“Redis锁负责削峰,MySQL乐观锁负责兜底”的混合架构。 流量高峰期,前端请求先打到Redis做预校验和预扣减,只有预扣成功的学生请求才继续进数据库执行正式扣减,数据库侧压力被削减一大半,同时行锁竞争也被控制在极低水平。
业内专家指出,最佳方案不是固定模板,而是在每个环节选择最匹配的锁语义,库存用数据库操作数,防重复提交用分布式锁,查课表不加锁走读写分离,这三层各自独立。
选课系统 服务器成本 有限 优化锁竞争 从哪入手
不少高校的技术团队面临的实际问题不是“不知道方案”,而是“机房资源有限,能用的Redis就一台,数据库学生选课全靠同一套实例”,预算约束下,优化方式更应该偏向减少锁的使用次数,而不是增加硬件去扛住锁并发。
- 合并操作减少锁次数:原先把库存检查、预占名额、已选记录三次操作分别加锁,改为一段Lua脚本在Redis内一次完成,锁只抢一次
- 批量发放选课资格:提前生成每个学生的选课令牌(Token),令牌只在开始选课时一次性校验,后续选课过程中的锁直接去掉
- 错峰开闸:不同年级间隔2分钟开放选课,从源头上把单点流量拆成三条平缓曲线,这不是技术方案但对锁竞争缓解最直接
- 热门课程先用队列:抢课请求不直接进入选课逻辑,先在Redis队列里排队,后端每次处理队列头部一个请求,把系统内“多线程抢锁”变成“单线程消费”

一个容易忽略的优化点是锁的自动续期。 选课请求有时在持锁期间发生GC暂停或网络抖动,锁自动过期导致一个请求还没执行完,另一个请求就拿到了同把锁,这种情况下,即使锁本身不竞争,业务也会出现双重提交,用带看门狗机制的锁客户端(如Redisson的 getLock)能自动续期,但要注意看门狗本身也有线程开销,如果学校预算有限,更务实的做法是在锁内只做毫秒级操作,不依赖续期机制,配合数据库最终校验兜底。
给一个具体可执行的分步操作路径,适合预算有限但想要快速见效的团队:
- 先用
Redis SLOWLOG检查选课期间慢查询,找到持锁时间超过100毫秒的脚本 - 把耗时操作从锁内搬回业务层,锁内只保留
Redis.call('DECR')这种指令 - 给热门课程加本地内存标记,前N个请求直接返池,减少无用Redis调用
- 用分段计数器:每门课的库存拆成10个Redis key,学生请求先随机命中一个key再扣减,锁竞争被分散到10个独立key上
在数据库侧,加一个简单的时间片切分也能大幅降锁压力。 每门课允许选课的时间窗口精确到秒,同一秒内最多涌入的请求受限于课容量本身,系统只需要做一个“当前是否已开抢”的开关判断,不需要全程高强度用锁。
开学季选课 锁竞争优化 落地的执行顺序
按照投入产出比排序,以下操作建议依次推进:
第一步:确认锁的实际竞争范围。 用Redis的 INFO commandstats 查看锁指令调用量,高峰期 SET NX 指令每秒调用次数超过五千,说明锁竞争已经很严重,此时先拆锁粒度,一小时内能完成改造。
第二步:锁内操作去重。 大多数选课系统在锁内做了两遍库存检查,一遍查缓存,一遍查数据库,把这些冗余检查移出去,锁内只留下一次原子扣减动作,持锁时间直接缩短一半。
第三步:数据库乐观锁兜底。 即便分布式锁竞争已经缓解,仍然可能存在极端情况导致锁漏过期,数据库那行

WHERE stock > 0 的更新是最稳妥的防线,改造代价极低,不需要额外资源,纯SQL层面完成。
第四步:引入异步队列。 抢课高峰期的请求全部先写Redis Stream,后端消费者按单线程方式顺序处理队列消息,选课结果通过Websocket或轮询接口返回给学生,这一步完成之后,分布式锁的并发压力会被彻底变成一个消费者自身的处理速度问题,竞争自然归零。
锁的异常处理不能只靠超时。在选课系统里,锁超时和选课失败不能画等号。 如果抢锁失败就直接返回“课程已满”,学生必然反复刷新重试,反而放大流量,正确做法是设置一个缓冲区:抢锁失败进入等待队列,等待队列满后再拒绝,拒绝时明确告知“前面尚有XX人排队”,从行为上降低学生刷新的动力。
选课场景的分布式锁优化,本质是让锁覆盖范围越来越小,持锁时间越来越短,直到锁“感知不到”它自己的存在,当一门课只剩一两个名额时,终究会有学生抢不到,但系统本身不能被高并发锁竞争拖垮,这应该是上线前必须验证的底线:无限并发下,响应时间稳定,锁不超时,DB不崩,能达到这个状态,开学季选课的分布式锁方案就算合格了。
开学季选课 分布式锁竞争 常见问题
问:选课系统用Redisson的分布式锁,为什么高峰时还是大量超时?
Redisson锁客户端本身不会造成超时,超时大概率来自锁内业务逻辑太重,选课高峰时,数据库连接池被打满,锁内查询课程信息的耗时从10ms涨到800ms,即使抢到锁也无法在锁过期前完成操作,解决办法是锁内去掉所有数据库查询,只做Redis原子操作,把课程信息预加载到本地缓存。
问:一门热门课的锁竞争特别严重,其他课程却都正常,这怎么处理?
用热Key探测工具(如JD的HotKey框架)识别出锁热Key后,把该课程拆成多个分片,比如一个课程300个名额拆成10个分片,学生通过学生ID哈希路由到固定分片,每个分片有独立锁key和独立库存计数,互不影响,分散后单把锁的争抢量降为原来的十分之一。
问:Redis锁过期后业务没执行完,这时候并发选课会不会超卖?
会,所以要给锁Key的Value设置一个唯一请求ID作为身份标识,业务执行完毕后执行Lua脚本,先比对Value值是否是自己写入的再释放锁,避免释放掉别人的锁,同时数据库侧必须有 stock > 0 条件更新兜底,两层防护下即便锁提前过期也不会超卖,最多出现一次重复更新被条件拦截,Redis锁的过期机制本身不依赖全局时钟,配合唯一ID后就是安全可用的。