防止超卖没有银弹,但有一套被大流量验证过的组合方案:常规场景用Redisson分布式锁扛住并发,极端情况用信号量做限流兜底,再配合库存扣减的乐观锁校验,三层防线才能保证不超卖。
先搞清楚分布式锁选型:Redis、ZooKeeper还是etcd
业内做秒杀系统,业界最常用的三种分布式锁实现是Redis、ZooKeeper和etcd,各有优势,也各有坑。
Redis分布式锁防超卖是绝大多数团队的第一选择,原因很直接:性能极高,加锁解锁都在毫秒级;接入成本低,公司基本都有现成的Redis集群;Redisson客户端把锁的自动续期、重入、公平排队都封装好了,几十行代码就能落地,但要小心两个坑:主从切换丢锁和锁过期导致并发进入临界区,前者需要合理配置RedLock或者容忍极小概率的丢锁,后者必须靠数据库层的库存校验做最终防线。
ZooKeeper的分布式锁走的是临时顺序节点方案,靠会话心跳维持锁的持有状态,客户端挂了锁自动消失无需设定过期时间,它的优势是不存在锁过期问题,可靠性更高,适合对一致性要求苛刻的场景,比如金融支付对账,缺点也很明显:性能比Redis差一个量级,秒杀高峰期每秒上万次的加解锁会拖垮ZooKeeper集群,而且在频繁GC时可能触发会话超时导致锁被误释放,官方建议锁的持有时间不超过几十秒。
etcd的分布式锁使用Lease租约实现,兼顾了性能和可靠性,也有续约机制但比Redisson要手动处理得多,国内用etcd做分布式锁的团队相对少,生态不如Redisson成熟,行业共识认为:追求极致性能选Redis,追求强一致性选ZooKeeper,需要跨云高可用选etcd。
锁过期是头号杀手:Redis分布式锁防超卖如何化解失效风险
看门狗机制只能解决业务超时,解决不了长事务
Redisson的看门狗默认每10秒自动续期到30秒,业务代码撑多久锁就续多久,很多团队初期觉得有这个就够了,结果遇到慢SQL查询或第三方接口响应时间超过30秒时,看门狗续期也会因为网络分区或主线程阻塞而中断,锁照样提前释放,业内专家指出:看门狗只是降低锁过期概率,不是彻底消除。
实操建议:在秒杀服务里给加锁代码设置合理的leaseTime,不要依赖看门狗默认值,明确预期的最长执行时间,比如库存扣减+订单写入合计不超过500毫秒,就把leaseTime设为3-5秒,宁可多放几次锁也不拖长锁的持有时间。
主从切换丢锁只能用RedLockPlus缓解,但不是万能的
Redis主节点宕机瞬间,从节点若还没同步到锁数据,另一个请求就能用同一把key加锁成功,两个线程同时操作库存,解决方向有两个:
- 引入RedLock协议,向≥3个独立Redis节点同时加锁,过半成功才算持有锁,但RedLock本身存在争议,Martin Kleppmann专门撰文分析过它的时钟跳跃问题,社区一直有争论。
- 用Redisson的MultiLock多重锁实现同等的多节点加锁效果。
实操上,如果公司Redis是Cluster模式,且锁的value用的是UUID+线程ID,加锁时检查当前值是否为自己持有,可以在一定程度上识别旧锁,避免误删,同时库存扣减的SQL必须加上乐观锁条件,比如UPDATE stock SET stock = stock - 1 WHERE sku_id = ? AND stock > 0,即使锁失效了,数据库的原子更新也会阻止库存变成负数,这是最兜底的防线。
实际项目里的失效兜底方案:降级开关+库存预热+信号量限流
我见过一个处理得比较好的电商秒杀案例,他们的兜底分为三层:
- 第一层:秒杀开始前把库存预热到Redis,扣减走Lua脚本;如果Redis锁获取失败,不立即报错,而是降级为本地限流+数据库乐观锁扣减,宁可丢弃请求也不放行重复扣减。
- 第二层:在网关层用Sentinel做热点参数限流,每个用户每秒最多请求一次秒杀接口,把流量压到系统能承载的范围,减少锁竞争的压力。
- 第三层:对Redis锁本身加fallback信号量,当Redis连接不可用时,用JVM内Semaphore做进程内限流,同时快速失败返回”正在排队中”,数据层用唯一订单号约束每个用户只能下单一次。
这套方案的核心不是锁本身有多强,而是让锁失败的代价可控:库存扣减永远有数据库约束兜底,请求永远有降级路径可走。
分布式锁选型对比:不同业务场景怎么选
| 维度 | Redis分布式锁 | ZooKeeper分布式锁 | etcd分布式锁 |
|---|---|---|---|
| 性能 | 极高,万级QPS没问题 | 中,千级QPS上限 | 中上,五千级QPS |
| 锁过期机制 | 支持,Redisson可续期 | 无过期时间,靠会话 | 支持Lease续约 |
| 主从切换问题 | 有丢锁风险 | 无丢锁问题 | 有,但比Redis好 |
| 实现复杂度 | 低,Redisson封装完善 | 中等,需维护ZK集群 | 较高,需配etcd集群 |
| 适用场景 | 互联网秒杀、活动 | 金融、对账、任务调度 | 云原生环境下的一致性控制 |
如果做秒杀系统防超卖,预算有限且Redis已经存在,直接用Redisson + Lua脚本 + 数据库乐观锁组合,不必引入额外中间件。如果是交易对账、发放优惠券这类强一致性场景,ZooKeeper的锁机制更合适,它的临时节点天然处理客户端宕机,不会因为锁忘释放而阻塞后续任务。如果在Kubernetes上运行且已经使用了etcd作为存储后端,顺手用它做分布式锁是个不错的选择,省去维护多套组件。淘宝双11这类超大规模场景下,很多团队根本不用分布式锁,而是用Redis原子自减库存,配合本地缓存和异步队列,锁只在小范围范围内使用。
做好防超卖先评估一下你需要哪个层级的锁
这取决于团队的技术栈和业务对超卖的容忍度:
- 刚起步的创业项目,Redis实例规模小,秒杀频率低,用Redisson就够了。
- 上线一段时间后流量上涨,锁竞争变激烈,考虑增加RedLock或多重锁,同时加强数据库乐观锁约束。
- 到了大促常态化阶段,库存预热到Redis + Lua脚本 + 网关限流 + 信号量兜底,分布式锁只是其中一环。
5个锁失效的典型案例和应对措施
-
业务执行时间超过leaseTime:一次促销活动里,锁设了3秒超时,结果数据库连接池满了,请求排队等待了5秒,锁释放后第二个线程进来了,解决:用Redisson看门狗续期,同时数据库操作设置超时时间,SQL超过1秒就快速失败。
-
主从切换窗口丢锁:主节点写入锁key后,异步复制到从节点前主节点宕机,从节点升主后锁丢失,解决:配置RedLock或MultiLock,至少要求3个节点;同步api,也就是读锁也要从主节点读,避免从节点读到旧数据。
-
网络抖动导致加锁超时:加锁请求发出后客户端等待响应超过100毫秒就报错,但服务端其实已经写入锁,解决:用Redisson的
tryLock(waitTime, leaseTime, TimeUnit),waitTime设置300-500毫秒,内部重试3次,减少瞬时抖动误判。 -
锁的粒度太粗:整个库存扣减流程包括查库存、预扣减、生成订单、扣减Redis库存四步全包在一把锁内,QPS被锁死到几百,解决:锁只包裹Redis库存扣减和数据库扣减两个原子操作,其他步骤放锁外执行,订单ID由雪花算法生成,数据库唯一索引防重。
-
锁Value值复用导致误删:两个服务用相同的业务key加锁,但value都是固定字符串”locked”,线程A超时释放锁后线程B加了新锁,线程A的finally代码块却把B的锁删了,解决:加锁时value设为
UUID + 当前线程ID,删除前先compareAndSet确保值匹配再删。
除了锁之外,防超卖还有哪些重要的兜底设计
数据库乐观锁是最后的防线
每次更新库存时,带上stock >= 1的条件,MySQL的行锁会保证同一行数据在同一时刻只有一个事务能更新成功,即使分布式锁失效了,数据库也会阻止超卖,代价是部分请求会更新失败,用户看到”商品已抢完”但系统数据是完整的。
Redis预热库存 + Lua脚本扣减
秒杀开始前,把库存量一次性写入Redis的hash或String,用Lua脚本保证查库存和扣库存是原子操作,不经过网络往返,性能极高,库存归零时返回0,秒杀入口直接关闭,不再放行请求到数据库层。
常见问题解答
Redis分布式锁防超卖效果怎么样?
效果很好,但必须配合看门狗、Redisson封装和数据库乐观锁使用,单靠裸Redis的SETNX做锁,大概率会遇到锁过期和误删问题,建议用Redisson的RLock,配置合理的leaseTime,并在finally块里确认身份后释放。
ZooKeeper分布式锁和Redis锁能同时用吗?
可以,但一般没必要,如果业务要求极端一致性,直接全部用ZooKeeper;如果追求性能,全部用Redis并在数据层兜底,混用会增加运维复杂度,而且锁的语义在两个系统之间不对齐,排查问题很容易混乱。
分布式锁失效后怎么快速恢复?
观察告警指标:加锁成功率下降、锁等待时间上升、Redis慢查询增多,恢复步骤是:先检查Redis网络和主从状态,确认连接池配置;再检查业务代码里是否有长事务或慢SQL占据锁时间;最后决定是否临时关闭秒杀入口,等待Redis稳定后再放开流量,整个过程在10分钟内可完成,核心思路是把异常流量挡在系统外部而不是死扛。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/636640.html





