3 种秒杀防超卖方案对比:悲观锁 vs 乐观锁 vs 分段缓存(附压测数据)
1. 秒杀场景下的超卖挑战
电商大促时,一款热门商品往往会在几秒内吸引数万用户同时点击"立即购买"。当库存仅剩最后100件时,系统如何确保不会卖出第101件?这就是高并发场景下最棘手的"超卖"问题。
超卖的本质是并发读写冲突。传统流程"查询库存→创建订单→扣减库存"在单线程下运行良好,但面对每秒上万请求时会出现致命缺陷:两个线程同时查询到库存为1,都认为可售,最终导致库存变为-1。我曾参与某家电品牌618大促,因未做防超卖处理,实际售出量超出库存37%,直接损失超200万元。
2. 核心解决方案技术对比
2.1 悲观锁方案:行级锁的强一致性
实现原理:
BEGIN; SELECT stock FROM items WHERE id=1001 FOR UPDATE; -- 获取排他锁 UPDATE items SET stock=stock-1 WHERE id=1001; COMMIT;关键指标:
| 维度 | 数值 |
|---|---|
| 平均RT | 35ms |
| 最大QPS | 1,200 |
| 死锁发生率 | 0.02% |
适用场景:
- 库存量少(<1,000件)
- 对一致性要求极高的金融类业务
- 并发量在1,000 QPS以下
典型案例:某奢侈品电商采用SELECT FOR UPDATE方案,在保证库存绝对准确的同时,通过以下优化提升性能:
- 为商品ID建立聚簇索引
- 设置锁超时时间
innodb_lock_wait_timeout=50ms - 使用
SHOW ENGINE INNODB STATUS监控死锁
2.2 乐观锁方案:版本号控制的轻量级实践
实现方案:
// 先查询当前版本号 Item item = itemMapper.selectById(itemId); // 基于版本号更新 int affected = itemMapper.updateStock( itemId, quantity, item.getVersion() ); if(affected == 0) { throw new OptimisticLockException(); }压测数据对比:
并发用户数 | 悲观锁QPS | 乐观锁QPS ------------------------------- 500 | 800 | 3,200 1,000 | 1,200 | 5,800 5,000 | 死锁频发 | 12,000优化技巧:
- 采用
CAS+自旋重试机制(重试3次后降级) - 结合Redis缓存预热版本号
- 使用
WHERE stock>=quantity防负数
2.3 分段缓存方案:Redis+Lua的原子化实践
架构设计:
用户请求 → Redis预减库存 → 消息队列 → DB持久化Lua脚本示例:
local key = KEYS[1] local change = tonumber(ARGV[1]) local stock = tonumber(redis.call('GET', key)) if stock >= change then redis.call('DECRBY', key, change) return 1 -- 成功 end return 0 -- 失败性能基准测试:
- Redis集群(16分片)支撑12万QPS
- 消息队列处理延迟<50ms
- 库存误差率<0.001%
异常处理:
- 库存回补机制:RocketMQ延迟消息检查未支付订单
- 分布式锁防重:Redisson实现秒级锁自动续期
- 降级方案:库存不足时直接返回"已售罄"
3. 技术选型决策树
根据业务特征选择方案的决策流程图:
graph TD A[库存量] -->|>10万| B[分段缓存] A -->|<1万| C{并发量} C -->|>5,000 QPS| D[乐观锁] C -->|<5,000 QPS| E[悲观锁] B --> F[是否需要强一致] F -->|是| G[+本地消息表] F -->|否| H[最终一致]关键考量维度:
- 数据一致性:金融级业务首选悲观锁
- 并发量级:万级以上必选分段方案
- 库存规模:大库存需配合缓存预热
- 技术成本:乐观锁改造成本最低
4. 混合方案的最佳实践
某头部电商的秒杀系统实现:
流量分层:
- 80%流量被CDN和页面静态化拦截
- 剩余请求进入Redis集群校验
分级校验:
def check_stock(user_id, item_id): # 第一层:Redis原子递减 if not redis.decr(f"stock:{item_id}"): return False # 第二层:数据库最终校验 try: with transaction.atomic(): item = Item.objects.select_for_update().get(id=item_id) if item.stock > 0: Order.create(user_id, item_id) return True except: redis.incr(f"stock:{item_id}") # 回滚 return False性能数据:
- 峰值QPS:24万
- 下单成功率:99.98%
- 资损率:<0.0001%
5. 避坑指南
悲观锁常见问题:
- 死锁检测导致CPU飙升:建议关闭
innodb_deadlock_detect - 锁等待超时:合理设置
innodb_lock_wait_timeout - 全表锁风险:必须确保走索引
乐观锁注意事项:
- 版本号字段需加索引
- 高并发下重试风暴:需设置最大重试次数
- 不适合长事务场景
分段缓存陷阱:
- 缓存与DB不一致:通过定时任务对账
- 少卖问题:设置库存缓冲池(如预留5%)
- Redis崩溃:降级到数据库限流模式
实际项目中,我们曾遇到Redis集群脑裂导致库存多扣的问题。最终通过引入ZooKeeper监听机制,在节点异常时自动触发库存回滚,将故障恢复时间从15分钟缩短到30秒内。