防超卖的分布式锁选型没有绝对最优,高并发场景优先选Redisson(Redis锁),但必须搭配数据库条件更新和唯一约束做兜底,锁只是第一道闸门,兜底才是真正防超卖的底线。
用户在下单抢购时,库存扣减是典型的高并发写操作,如果多台应用服务器同时扣同一件商品的库存,锁的取舍直接决定会不会出现超卖,下面这套选型思路和失效兜底方案,已经经过大量电商系统验证,核心逻辑是:不要让锁单独承担防超卖的全部责任。
防超卖分布式锁怎么选型?先看这三种锁的本质
分布式锁选型对比,绕不开Redis、Zookeeper和数据库三种方案,它们各自解决不同层面的并发问题,没有哪个是万能药。
Redis分布式锁:快,但必须处理锁续期和主从切换
Redis锁通常用SET key value NX EX timeout实现,Redisson在此基础上封装了看门狗机制,自动续期,它最大的优点就是吞吐高,能扛住瞬时高并发,但有个隐性问题:如果主节点在锁未删除时崩溃,从节点接管数据时可能还没同步这条锁记录,新的请求就会拿到锁,业内专家指出,要解决这个漏洞,最实用的办法是让锁的失效时间远小于业务最长执行时间,同时用Redisson的看门狗拉长锁生命周期,避免锁提前过期。
Zookeeper分布式锁:强一致,但性能有天花板
Zookeper锁靠临时顺序节点实现,客户端创建节点,谁序号最小谁拿到锁,因为节点监听和会话超时机制都很严格,不会出现两个客户端同时持锁的尴尬,缺点是每次加锁都要走一次网络通信和节点创建,锁释放还要触发监听通知,在高频扣库存场景下吞吐量不够,如果追求绝对一致且并发量有限,Zookeeper是不错的选择。
数据库分布式锁:简单可靠,但别让它扛大流量
数据库锁分为乐观锁和悲观锁,悲观锁就是SELECT ... FOR UPDATE,直接锁住库存行,更新完才释放,可靠,但事务时间稍长,数据库连接就被占着,容易拖垮库,乐观锁用version字段或者库存条件做校验,并发高时大量请求会更新失败,需要重试,数据库锁适合并发极低、对一致性要求严格的后台任务。

下表简单对比三种锁:
| 维度 | Redis锁 | Zookeeper锁 | 数据库锁 |
|---|---|---|---|
| 一致性 | 最终一致,主从切换可能丢失 | 强一致 | 强一致 |
| 吞吐能力 | 最高 | 中等 | 最低 |
| 锁过期风险 | 有,需续期 | 会话超时会释放 | 事务结束才释放 |
| 运维成本 | 低 | 需要维护ZK集群 | 不用额外组件 |
| 典型场景 | 秒杀、高并发库存扣减 | 任务调度、配置服务 | 低并发管理后台 |
行业共识认为,锁选型先看吞吐要求,再看一致性预期,多数情况下,生产环境的库存扣减都选Redis锁,因为速度快,后续靠兜底弥补一致性。
分布式锁失效兜底方案:超卖的防线不能只靠锁
锁失效是必然的,网络超时、GC停顿、主从切换、锁过期,任何一环出问题都会让多个请求同时走进扣库存的临界区,这时候如果没有兜底,库存就会变负,下面这几个兜底方案按推荐程度排列。
兜底方案一:SQL层直接加库存条件,最有效
把扣减库存的SQL写成:
UPDATE inventory SET stock = stock - 1 WHERE id = ? AND stock > 0;
这条语句依赖数据库行锁,在事务中执行时,若库存不足,影响行数为0,业务直接判断失败,它不依赖任何分布式锁,是防超卖的终极防线,即使前面Redis锁失效,这条SQL也能拦住超卖。所有库存扣减都必须带上stock > 0条件,这是硬性要求。
兜底方案二:Redis Lua脚本原子扣减
Redis锁失效前,可以在加锁后继续用Lua脚本保证“判断库存-扣减”两步原子执行。
if redis.call('get', KEYS[1]) > 0 then
return redis.call('decr', KEYS[1])
else
return -1
end
这个脚本在Redis单线程里执行,天然串行,如果脚本返回-1,说明库存已扣完,业务直接返回“已售罄”,用Redisson时,也不要迷信

tryLock,锁拿到后依然要用Lua或SQL来扣减,双重保险。
兜底方案三:乐观锁版本号防重放
给库存表增加version字段,扣减时带上版本号:
UPDATE inventory SET stock = stock - 1, version = version + 1 WHERE id = ? AND version = ?;
如果更新行数为0,说明版本过期,需要重新读取库存并重试,这个方案适合库存变化频率不高但需要防并发误操作的系统,它的好处是不需要额外锁组件,但会有部分请求因版本冲突失败,需要设计重试策略。
兜底方案四:唯一约束和业务流水号,最后一道墙
超卖常伴随重复订单,给订单表建立product_id和user_id的唯一约束,或使用业务方生成的唯一流水号作为订单主键,重复提交就会被数据库拒绝,这个兜底不依赖锁,但需要提前设计表结构,例如锁定商品ID和用户ID的组合,同一用户重复秒杀时直接违反约束,数据库抛异常,业务捕获后返回“不可重复购买”。
兜底方案五:消息队列串行化,从源头消除并发
如果允许异步扣减,可以把所有扣库存请求发给MQ,让消费者单线程或单分区消费,天然串行,该方案适合对实时性要求不高的场景,比如库存流水异步入账,或者订单支付后扣减库存,它牺牲了部分延迟,换取了绝对没有并发问题的确定性。
具体操作:如何保住锁的有效窗口
用Redisson时,锁默认30秒过期,看门狗每10秒续期一次,但业务代码里最好还是显式设置lockLeaseTime,并确保异常时在finally块释放锁,如果不用Redisson,可以自己管理续期,比如每5秒执行一次PEXPIRE,但要注意续期线程和业务线程的生命周期对齐,这里有一个实操细节:锁的过期时间不要设得太短,保守起见,设置成业务峰值耗时的三倍以上。
选型落地的实操路径:从锁到兜底的完整链路
下面是一套可直接套用的决策流程,帮助你从零构建防超卖方案。
第一步:按并发量级初筛锁类型
- 高并发商品(例如限量发售、整点秒杀):选Redis锁,配合Lua脚本扣库存。
- 中低并发商品(例如普通促销):选数据库乐观锁,省去Redis依赖。
- 强一致场景(例如库存对账、资金变动):选Zookeeper锁,牺牲吞吐换可靠性。

第二步:把兜底方案按照优先级排序
优先用SQL条件更新拦住超卖,其次用Redis Lua脚本提前挡一部分流量,最后用唯一约束处理异常重复请求,不要反过来,把SQL条件更新放到最后,因为那是最终防线。
第三步:监控锁的“健康状态”
监控三个指标:锁等待时间、锁过期释放次数、扣减失败率,锁等待时间突增,说明锁粒度太大或竞争太激烈;锁过期释放次数频繁,说明业务执行时间超过了锁时间上限;扣减失败率升高,可能是库存本身不足或SQL条件失效,根据监控数据动态调整超时时间和续期策略。
第四步:压测时故意让锁失效
在测试环境模拟主从切换、网络延迟、锁过期,观察兜底是否生效,只有锁失效时系统仍不超卖,才算真正合格,这一步很多团队漏掉,导致上线后遇到极端情况手忙脚乱。
Q&A
防超卖分布式锁失效怎么办?
锁失效后,数据库条件是救援的主力,只要扣减SQL里写了stock > 0,锁失效再多也不会减到负数,如果SQL也漏了,唯一约束能拦下重复订单,所以要定期检查这些兜底代码是否被改掉,或者在CI中增加代码扫描,强制要求库存扣减SQL必须带条件。
分布式锁选型对比需要关注哪些关键维度?
核心看三方面:一致性、吞吐、运维成本,Redis锁快但一致性偏弱,适合高并发非严格一致场景;Zookeeper锁强一致但性能不高,适合低并发高可靠场景;数据库锁最简单,性能最差,适合内部系统,没有唯一的正确答案,只有匹配你的业务场景的锁。
Redis锁过期但业务还没走完怎么办?
优先保证业务代码在finally块里释放锁,同时让锁续期机制覆盖业务执行时间,如果锁在业务执行中真的过期了,后续操作不要继续更新库存,而是返回错误码,让上层做补偿处理,此时SQL条件更新仍能防止超卖,所以业务即使中断,库存数据也是安全的。