服务器与大带宽专家 · 持牌IDC/CDN/ISP服务商
简米科技官网JIANMI TECH
资讯 2026-08-30 更新于 2026-08-30 简米科技 4,348 字 10 分钟阅读

防超卖的库存一致性靠哪些技术兜底,分布式锁和乐观锁怎么实现

导读防超卖的库存一致性,从来不是靠单一技术解决,而是靠“数据库兜底+缓存拦截+异步对账”三层配合,其中数据库行锁和Redis Lua原子操作是核心支柱,只靠缓存扣减、不做数据库校验的系统,大促时大概率超卖;只靠数据库行锁、不加缓存拦截的系统,流量一大就雪崩,每一层都有明确分工,缺一不可,数据库行锁兜底:防超卖的定海……

防超卖的库存一致性,从来不是靠单一技术解决,而是靠“数据库兜底+缓存拦截+异步对账”三层配合,其中数据库行锁和Redis Lua原子操作是核心支柱。只靠缓存扣减、不做数据库校验的系统,大促时大概率超卖;只靠数据库行锁、不加缓存拦截的系统,流量一大就雪崩,每一层都有明确分工,缺一不可。

数据库行锁兜底:防超卖的定海神针

所有防超卖方案最终都要落在数据库这一层,行业共识是:库存扣减必须是一个带条件的原子操作,放在事务里执行,否则并发的扣减请求会互相覆盖,具体到MySQL,最常用的语法是SELECT ... FOR UPDATEUPDATE ... WHERE stock > 0配合使用。

for update行锁怎么做

经典的单库存扣减逻辑,大概四步:

  1. 开启事务,执行SELECT stock FROM product WHERE id = 123 FOR UPDATE
  2. 应用层拿到当前库存值,判断是否大于0
  3. 大于0则执行UPDATE product SET stock = stock - 1 WHERE id = 123
  4. 提交事务,释放行锁

这里FOR UPDATE会对满足条件的那一行记录加排他锁,其他事务的读和写都会被阻塞,直到当前事务提交或回滚,锁粒度是行级,并发操作同一商品时,实际上被串行化了。

where条件二次校验的作用

业内更常见的是直接把扣减语句写成带条件的更新,一步到位:

UPDATE product SET stock = stock - 1 WHERE id = 123 AND stock > 0

受影响行数为1代表扣减成功,为0代表库存不足,业务直接抛异常,这个写法省去了显式加锁,执行计划会锁定命中的索引行,效果等同行锁。重点是stock > 0这个条件,它是防超卖的最后一堵墙,即使前面缓存层全部失效,这一句也能挡住超卖请求。

这个方案有什么槽点

数据库行锁兜底很稳,但性能天花板明显,每秒能扛住的操作数,受限于数据库的连接数和锁等待开销,大多数团队在单机MySQL上,纯行锁扣减的QPS也就几千级别,大促秒杀场景,请求量动辄每秒几万,直接怼数据库,等待锁的请求会堆成山,连接池直接被拖垮。

所以数据库这层只负责兜底,不做主力承接,真实架构里,流量先过缓存层,数据库扣减的实际请求量是已经被削峰过的。

Redis原子操作防超卖:缓存层的第一道拦截

绝大多数秒杀请求会先打到缓存层,Redis的原子操作是拦截超卖的第一道闸门,方案核心是利用单线程执行命令的特性,保证多条指令在一个脚本里不会被其他命令插入。

Lua脚本保证三步一走完

Redis从2.6版本开始支持Lua脚本,脚本里的命令执行是原子的,中间不会被其他客户端命令打断,库存扣减的逻辑用一段Lua脚本搞定,

防超卖的库存一致性靠哪些技术兜底,分布式锁和乐观锁怎么实现

local stock = tonumber(redis.call('GET', KEYS[1]))
if stock and stock > 0 then
    redis.call('DECR', KEYS[1])
    return 1
end
return 0

这个脚本先读库存,再判断,再扣减,全程原子,大量请求同时进来时,只会有一批返回1,其余返回0,从源头上拦住了超卖。

Redis库存扣减的最终校验

Redis扣减成功不等于订单一定成立,一个完整的下单流程通常是:

  1. Redis预扣减库存,返回成功则继续
  2. 订单服务创建订单记录,状态置为待支付
  3. 异步通知数据库扣减最终库存
  4. 数据库扣减失败则回滚Redis库存

这里存在Redis和数据库状态不统一的窗口期,比如Redis扣了10个,数据库实际只剩8个,那2个就属于预扣成功但落库失败,解决方案是数据库扣减以Redis的预扣记录为准,先把Redis扣减成功的订单批量落库,再统一做数据库库存核对,差额直接走人工或自动补单流程。

为什么不能只用Redis DECR

有的团队图省事,直接对Redis的key执行DECR,判断返回值是否大于等于0,这种做法能扛很高的QPS,但存在两个隐患:

  • 缓存不设过期时间,冷门商品的库存Key永远占着内存不释放
  • 缓存一旦丢失,库存数据无法通过数据库恢复,业务直接兜底失败

所以行业更普遍的做法是:Redis库存Key设置一个很长但非永久的过期时间,同时定期从数据库全量同步库存到缓存,大促期间同步频率加密,兜底能力更强。

分布式锁和Redis原子操作对比,谁更适合秒杀

提到防超卖,很多人会想到分布式锁,分布式锁确实能解决并发冲突,但用的地方和Redis原子操作不太一样,搞清楚这两者的差异,遇到高并发场景才能选对工具。

语义和性能的差异

分布式锁的语义是“互斥访问临界区”,典型实现是Redisson的RLock或基于ZooKeeper的临时顺序节点,拿锁的目的是让多个节点串行执行一段业务代码,比如生成订单号、校验库存、创建订单。

Redis原子操作解决的是“单条或多条Redis指令的原子性”,比如DECRINCR、Lua脚本,拿到的不是锁,而是扣减结果

从性能上看,分布式锁的获取和释放至少涉及两次网络往返,还要考虑锁超时、自动续期,而Redis原子操作一次往返即可,吞吐量高出不少,并发量过万时,分布式锁本身会成为瓶颈,而Redis原子操作还能支撑。

防超卖的库存一致性靠哪些技术兜底,分布式锁和乐观锁怎么实现

维度 分布式锁(Redisson) Redis原子操作(Lua)
核心能力 互斥执行代码块 原子执行Redis指令
适用场景 库存扣减前后有复杂业务逻辑 纯库存扣减和预扣
性能 中等,锁开销较大 高,单次往返即可
风险 锁超时、释放失败、重入问题 Redis不可用时缓存层完全失效
数据库一致性 靠代码逻辑保证 靠脚本顺序和条件判断保证

多节点部署防超卖方案,锁失效问题怎么处理

单机部署的防超卖比较简单,用本地锁加数据库条件更新就能搞定,但线上环境大多是集群部署,多个应用节点共享同一个库存数据源,这时候本地锁天然失效,必须引入分布式锁或依赖数据库行锁。

分布式的锁失效问题集中在三类:

  • 锁超时释放,业务还没执行完,锁已经被自动释放,其他节点趁虚而入
  • 主从切换丢锁,Redis主节点宕机,未同步的锁数据丢失,新主节点没有锁记录
  • 锁粒度太粗,整张订单表共用一个锁,不同商品的订单互相阻塞,吞吐量烂掉

解决多节点部署的锁问题,实操中比较常用的是组合方案:库存扣减主流程用Redis Lua脚本,分布式锁只用在涉及异步处理的环节,比如发送库存变更消息、同步缓存等操作,用分布式锁保证消息不会重复下发,既避免锁竞争影响核心扣减链路,也保证了消息的一致性,一个常见的操作路径是:

  1. 压测时先跑通Redis Lua脚本的扣减逻辑,确认TPS符合预期
  2. 在订单创建和缓存同步环节加Redisson分布式锁,锁的粒度按商品ID维度隔离
  3. 数据库保留stock > 0条件更新作为最终校验,消费消息时重复扣库存不生效

超卖监控与修复

线上问题定位,团队都会看监控,具体的排查路径可以这么记:

  1. 查询订单表的唯一商品在下单高峰时段的总量,比对库存变更记录表
  2. 如果订单量超过库存变更量,说明MySQL行锁条件更新没拦住
  3. 再查Redis扣减日志,Lua脚本执行返回0的记录是否被放行到了下单流程
  4. 重点排查订单服务和库存扣减服务的事务是否真的落在同一个事务组里

排查完修复,不是删掉订单那么简单,要回补库存,写一个反向脚本,根据订单创建时间倒序,把超卖部分的状态置为失效并回补Redis的库存计数,再给目标用户发站内信,这套操作是标准流程,所有秒杀系统都留着这套保命脚本。

防超卖的库存一致性靠哪些技术兜底,分布式锁和乐观锁怎么实现

异步对账与库存回补:兜底的最后一公里

即便有数据库行锁和Redis原子操作两层,超卖或库存不准确仍然可能发生,场景很常见:用户下单成功但支付超时,订单自动取消后库存没有回补;或者Redis缓存更新成功、数据库读写分离主从同步延迟,导致扣减时读到旧库存。

消息队列解决库存回补的时差

缓存层的扣减,只是“预占”,真正的扣减,需要在下单流程中发送一条“库存扣减消息”到MQ,由专门的消费者去执行数据库扣减,如果扣减失败,MQ重试或降级处理,这个方案把库存扣减变成了异步化操作,核心链路只需要操作Redis和订单表,数据库的压力被削平一大截。

异步化最大的好处,是解决了多处扣减的时间错位问题,比如用户秒杀成功,但订单在支付时间截止前没有完成,订单状态变为已取消,此时需要发送一条“释放库存”消息,这条消息必须确保不丢失,否则Redis库存就永远少一个数。

对账任务如何兜底

定时对账是最后一道防线,一个典型方案是每天凌晨跑一个定时任务:

  1. 扫描当天所有订单,按商品维度汇总已支付订单的扣减量
  2. 比对商品表的实际库存变更流水
  3. 找出多扣或漏扣的记录,生成差异报表
  4. 按配置的策略自动回滚Redis库存或补偿订单

对于“黄牛一次性囤完优惠价再全部退款”这种极端场景,对账任务也会发现退款订单的库存释放没有生效,触发自动回补。

常见关于防超卖的疑问

订单超卖如何排查?线上疑似超卖先看哪里

先查商品表的库存字段和订单表的已支付订单量,两者不一致才有必要继续深挖,一致的话再走日志链路排查,看看是不是购买了按钮重复点击导致的重复订单,如果订单总量确实超过库存,顺着日志往下查Redis Lua脚本的返回值和数据库条件更新的受影响行数,定位断点。

大促值不值得上Redisson分布式锁?

大促的秒杀接口,库存扣减本身用Redis原子操作,锁顶多用在异步环节,没必要在主干扣库存流程上引入分布式锁,分布式锁在并发量大时性能衰减非常明显,而且续期、释放的复杂性会给高并发链路埋雷。

Redis挂了,防超卖方案怎么办?

Redis暂不可用时,降级方案是直接走数据库行锁扣减,业务直接UPDATE product SET stock = stock - 1 WHERE id = ? AND stock > 0,大促时研发会提前部署限流和熔断组件,让请求分批进入数据库,避免击穿,核心原则是不让不确定的失败影响仓库的实际扣减,宁可把部分请求拒之门外,也不在数据库里留下超卖的数据,流量恢复后再重新同步库存到缓存。

分享本文
本文为 简米科技官网 原创,已由运维技术专家审核。转载请注明来源:原文链接
售前咨询 服务热线 售后 邮箱