- > “超卖怎么办?”
- < “加锁就可以了。”
- > “你再思考一下?”
库存超卖看似是“扣减库存”的简单问题,实际上是高并发下性能与一致性的博弈,也是大厂面试筛选基础扎实且具备场景思维的工程师的核心考点。 看似简单的超卖问题,可以从7种技术方案来吃透。
核心矛盾:为什么超卖难解决?
“超卖”的本质是,多线程/多服务(分布式)并发执行“查库存 -> 扣库存”时,出现“库存判断实效”。 比如100个请求同时查询到库存1,都执行了扣减,最后库存变成了-99,这就是典型的超卖。
解决超卖没有完美无缺的解法,所有的方案都是性能、一致性、复杂度的三方取舍。下面的7种方案,正是从简单兜底到高阶解法的取舍演进。
1: 数据库无符号字段——超卖的“最后防线”
| |
- 👍 兜底保证不超卖
- 👎 被动防御
技术原理
| |
要点
使用无符号字段,只能防范最终超卖,防不住并发请求穿透,例如100个并发请求查到库存是1,都执行了UPDATE,99个请求会因为库存负数报错, 但这些请求已经进入业务流程,会导致订单超卖,即订单数大于实际库存,用户下单成功但却被告知库存不足,用户体验差。
适用场景
中小型非秒杀场景的兜底方案,不能单独使用,需要配合其他并发控制手段。
2: 悲观锁——强一致性的性能杀手
| |
- 👍 强一致性
- 👎 吞吐量极低
技术原理
利用 InnoDB 的行锁机制,通过FOR UPDATE锁定查询行,避免并发修改
| |
要点
悲观锁的行锁带来的串行执行会导致吞吐量极低,秒杀场景几百个请求排队,超时率飙升;若 where 条件不命中索引,会升级为表锁,阻塞全表,使得业务彻底瘫痪。
适用场景
一致性要求极高、没有并发或并发极低的场景,例如奢侈品库存扣减,宁慢勿错。
3: 乐观锁(CAS)——中低并发的“平衡之选”
| |
- 👍 性能提升
- 👎 冲突时CPU空转
技术原理
给库存表增加版本号,更新时校验版本号,确保只有未修改过的库存才可以扣减:
| |
要点
- 高并发下限制重试次数,避免乐观锁重试过多
- 结合 Redis 预热缓存,减少数据库请求
- 可以用时间戳代替版本号,但需要注意时钟同步问题
适用场景
中低并发(QPS < 1000)下且允许少量重试的场景,例如电商日常促销。
4: Redis队列——瞬时高流量的削峰处理
| |
- 👍 扛住瞬时流量冲击
- 👎 逻辑复杂且容易写出问题
技术原理
预热阶段
将商品库存同步到Redis队列
| |
下单阶段
请求从队列取出元素,成功则有库存
| |
要点
需要保证双写一致性,保持Redis队列与库存一致
- 下单成功后,先更新Redis队列,再更新数据库,如数据库更新失败,需回滚Redis队列
- 定时任务同步数据库库存到Redis队列,修复异常差异
- 避免单商品多件购买(无法一次取多个),需额外处理批量场景
适用场景
瞬时高流量削峰,但库存粒度是单件的场景。
5: Redis Lua脚本——原子性的“终极保障”
| |
- 👍 Redis层面的原子性
- 👎 脚本难以维护
技术原理
Redis执行Lua脚本时,会阻塞其他命令,确保脚本内的操作原子性:
| |
要点
Lua脚本有可能会导致Redis阻塞,需分场景讨论:
- 短脚本阻塞时间极短,可忽略
- 长脚本涉及到复杂运算,会阻塞Redis,需拆分或用异步任务
- 需开启Redis持久化(AOF+RDB),避免脚本执行过程中因宕机导致库存丢失
适用场景
Redis主导库存管理,对原子性要求极高的场景,如直播带货瞬时下单。
6: 分布式锁——跨服务的“一致屏障”
| |
- 👍 分布式强一致
- 👎 性能瓶颈
技术原理
使用Redis分布式锁,控制多节点业务处理一致:
| |
要点
需要避免分布式死锁的问题:
- 设置锁超时,并开启自动续期,例如Redisson的watch dog机制
- 使用 finally 强制释放锁
- 避免锁嵌套,例如先锁A再锁B,在另一个服务先锁B再锁A
适用场景
微服务架构、分布式服务、多服务节点等操作同一库存的场景,如电商订单服务、库存分离等。
7: 分段锁——解决高热Key的分治手段
技术原理
分片初始化
将库存拆分成多个分片
| |
选分片下单
取用户ID按一定的算法定位分片,避免单分片热点,例如可以采用CRC32均分用户进行定位
| |
要点
- 需要预期确认分片数,如 QPS = 10000,分10分片,每片承载1000左右的QPS
- 避免过多分片,多分片会带来管理上的复杂度
- 剩余库存会分散在各个分片,可考虑合并分片或循环分片,如分片0售空,可尝试分片1、分片2,顺次遍历分片拿库存
适用场景
高并发秒杀场景,QPS > 10000 的极端流量,如字节电商、拼多多百亿补贴等。