开学季集中选课时的分布式锁竞争,缓解关键不是把锁做得更重,而是拆锁、限流、异步化,用数据库唯一约束和缓存原子操作兜底。 一旦选课开放,热点课程会瞬间变成兵家必争之地,谁先把大锁拆小、把请求排队、把扣减做成原子操作,谁就能让系统稳住。
为什么开学季选课一开,锁竞争就爆了?
选课洪峰长什么样
开学前固定时间开放选课,几万甚至十几万请求几乎同时到达,热点课程名额少,用户反复刷新、重复提交,真正的有效请求可能只占一部分。
- 开放时间集中,流量曲线是尖刺,不是缓坡。
- 热门课程容量有限,大量请求争抢同一份余量。
- 用户会重试、切设备、多标签页操作,重复请求放大竞争。
- 选课结果又要求准确,不能超卖,也不能把已选上的记录丢掉。
业内专家指出,选课洪峰的本质是热点数据竞争,而不是锁本身慢,锁只是把竞争暴露出来。
分布式锁到底锁了什么
很多系统一开始会用一把全局锁保护整个选课接口,这样做简单,但代价很大。
- 锁课程余量:同一门课所有请求串行。
- 锁用户资格:判断是否已选、是否欠费、是否满足先修。
- 锁重复提交:防止同一用户短时间多次点击。
锁粒度越大,持锁时间越长,等待队列就越长,最后不是数据库扛不住,而是锁的获取和释放把线程池拖垮。
锁竞争失控的常见信号
- 获取锁超时次数快速上升。
- 接口平均响应时间飙升,排队线程越来越多。
- Redis CPU 使用率异常,热 key 集中在少数课程。
- 应用线程池满,后续请求直接拒绝。
- 日志里大量
tryLock失败,或者锁等待超过接口超时。
这些信号一旦出现,继续调大锁超时时间往往没用,反而会让死锁和长时间占用更严重。
选课系统分布式锁超时怎么办?先拆锁粒度再谈超时
把全局锁拆成课程维度锁
锁的 key 应该尽量小,不要锁 lock:selection,而是锁 lock:course:{courseId},同一门课串行,不同课程并行。
- 基础锁:
lock:course:123 - 分段锁:把一门课的余量拆成多个桶,
lock:course:123:seg:1,每个桶独立扣减。 - 本地锁加分布式锁:单机先用
ReentrantLock拦一层,减少 Redis 访问。 - 用户维度锁:
lock:user:{userId}防重复提交,和课程锁分开。
分段锁的代价是库存要分片管理,最终汇总,好处是热点课程不再只争一把锁。
给锁设置合理租约与看门狗
以 Redisson 为例,常用方法是:
RLock lock = redissonClient.getLock("lock:course:" + courseId);
boolean locked = lock.tryLock(100, 3000, TimeUnit.MILLISECONDS);
waitTime是最大等待时间,失败就快速返回,不要无限等。leaseTime是租约时间,要覆盖业务逻辑,但短于接口超时。- 看门狗续期适合无法预估耗时的场景,高并发选课下不建议无限续期。
- 释放锁时用 Lua 比较 value,避免释放别人的锁。
简单 Redis 命令可以这样写:
SET lock:course:123 uuid NX PX 3000
释放时先比较 uuid,再删除:
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
快速失败与排队
锁获取失败后,不要自旋,自旋会把 Redis 打得更热。
- 立即返回“排队中”,把请求写入消息队列。
- 前端轮询选课结果,后端异步处理。
- 对热点课程单独设置更短的锁等待时间。
- 超过阈值直接降级,返回“稍后重试”。
锁是协调工具,不是吞吐工具,把请求从锁竞争里拿出来,才能让系统喘口气。
Redis分布式锁和ZooKeeper锁选课场景对比:怎么选不踩坑
性能与一致性取舍
| 维度 | Redis 分布式锁 | ZooKeeper 锁 |
|---|---|---|
| 一致性模型 | AP,最终一致 | CP,强一致 |
| 性能 | 高吞吐、低延迟 | 较低,适合小规模 |
| 锁释放 | 租约加看门狗 | 会话断开自动释放 |
| 实现复杂度 | 需处理锁失效、误删 | 顺序临时节点,较直观 |
| 选课适用 | 秒杀型选课、热点课程 | 名额强一致、小规模选课 |
Redis 锁快,但可能因为主从切换、网络分区出现锁失效,ZooKeeper 锁稳,但吞吐有限,临时节点和 watch 机制在极高并发下压力不小。
选课场景的落地建议
- 多数选课系统:Redis 锁加数据库唯一索引加消息队列,足够应对。
- 强一致选课名额:可以用 ZooKeeper 或 etcd,但必须配合限流。
- 不要用锁替代幂等,唯一索引和去重表才是最后防线。
- 行业共识认为,锁只是协调,不能替代业务校验和最终一致性。
如果课程余量允许少量异步,Redis 方案更合适,如果名额极少且绝对不能超卖,强一致组件更稳妥,但入口必须削峰。
高并发选课分布式锁性能价格大概怎么算?先看资源账
成本不在锁,在冗余与运维
锁组件的直接授权费用通常不是大头,真正花钱的是集群冗余、监控、告警和人力。
- 云 Redis 按规格、地域计费,北京、上海等区域价格有差异。
- 自建 ZooKeeper 需要奇数节点,机器和运维成本不能忽略。
- 跨可用区部署会增加网络延迟,也会增加带宽成本。
- 热点 key 需要额外监控和拆分,否则扩容也难解决。
用免费方案降低锁竞争
- 本地锁加 Redis 分片锁,减少网络往返。
- 数据库唯一索引兜底,不需要额外授权成本。
- 消息队列削峰,把锁竞争时间窗拉长。
- 缓存预热课程余量,减少数据库查询。
- 对非热点课程降低锁强度,甚至直接走数据库乐观锁。
价格问题不能只看组件账单,一次选课崩溃带来的影响,往往比多买几台机器更贵。
开学季选课系统分布式锁怎么优化?五步实操
入口限流与排队
在 Nginx 或网关层先挡一道。
limit_req_zone $binary_remote_addr zone=sel:10m rate=20r/s; limit_req zone=sel burst=40 nodelay;
- 网关做令牌桶,用户维度限流。
- 热点课程单独限流,避免拖垮全站。
- 超出阈值的请求进入消息队列,前端展示排队进度。
请求幂等与预校验
- 用
SET sel:user:course NX EX 60防重复提交。 - 先查缓存余量,不直接压数据库。
- 校验用户资格、时间冲突、先修课程。
- 预校验失败直接返回,不进入锁竞争。
分布式锁加原子扣减
锁只保护竞争窗口,不保护整个事务,扣减用 Lua 原子完成。
local seat = redis.call("get", KEYS[1])
if not seat or tonumber(seat) <= 0 then
return 0
end
redis.call("decr", KEYS[1])
return 1
- 先扣缓存余量,再异步落库。
- 锁 key 按课程或分段设置。
- 扣减失败直接返回“已满”。
异步落库与补偿
- 消息队列异步创建选课记录。
- 数据库唯一索引兜底:
UNIQUE KEY uk_stu_course (student_id, course_id)。 - 失败重试,设置最大重试次数。
- 对账任务定期修复缓存与数据库不一致。
监控与熔断
- 监控锁等待时间、获取失败率、Redis 热 key。
- 监控消息队列积压和消费延迟。
- 接口超时或错误率升高时自动熔断。
- 降级返回排队页,而不是让请求堆死。
北京高校选课系统分布式锁解决方案有哪些本地化考量?
多校区部署与网络延迟
北京高校常有多个校区,机房分散,锁集群如果跨校区访问,延迟会明显增加。
- 同城双活,锁集群就近访问。
- 热点课程数据尽量靠近用户入口。
- 跨校区同步走异步补偿,不要求强同步。
- 选课开放前做全链路压测,覆盖校区网络差异。
合规与运维
- 数据不出校园,本地化部署更符合要求。
- 等保要求下,日志、审计、权限要完整。
- 开源方案加自建机房,减少云依赖。
- 但自建也要考虑高可用,别把单点故障留给自己。
地域化不是换个机房那么简单,网络路径、运维响应、合规边界都会影响锁竞争缓解效果。
开学季选课锁竞争缓解的核心,是把“抢一把大锁”变成“拆锁、排队、原子扣减和唯一约束兜底”。 按这个顺序落地,系统更稳,运维也更轻松。
Q&A:开学季选课分布式锁竞争缓解常见问题
选课瞬间锁竞争太激烈,能不能直接不用分布式锁?
可以,用数据库唯一约束加 Redis 原子扣减加消息队列,锁只作为可选层,前提是业务允许异步落库,并且有对账补偿,对于绝对不能超卖的核心名额,仍需强一致校验。
Redis分布式锁和ZooKeeper锁在选课场景下哪个更贵?
成本取决于规模和运维方式,自建 ZooKeeper 需要奇数节点,机器和人力成本不低,云 Redis 按规格和地域计费,北京上海等区域价格不同,多数情况下 Redis 方案资源占用更少,但强一致场景 ZooKeeper 更合适。
选课系统分布式锁超时后如何处理已扣减库存?
用最终一致性补偿,本地消息表、消息队列重试、定时对账任务都可以,库存扣减和选课记录落库分离,数据库唯一索引防止重复记录,缓存与数据库不一致时以数据库为准修复。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/769888.html




