题目
高并发场景下如何设计库存扣减系统避免超卖
信息
- 类型:问答
- 难度:⭐⭐⭐
考点
事务隔离级别,锁机制,并发控制,数据库设计,分布式事务
快速回答
核心解决方案要点:
- 事务隔离级别:使用
REPEATABLE READ或SERIALIZABLE保证数据一致性 - 锁机制选择:悲观锁(
SELECT FOR UPDATE)或乐观锁(版本号控制) - 数据库设计:库存字段使用无符号整数,添加
CHECK约束防止负数 - 分层防御:应用层限流 + 缓存预扣减 + 数据库最终扣减
- 补偿机制:通过日志和定时任务处理异常状态
问题背景
在高并发秒杀场景中,当多个请求同时扣减同一商品库存时,可能出现超卖(库存减为负数)。例如:
-- 错误示例(非原子操作)
SELECT stock FROM products WHERE id=100; -- 假设查到stock=1
UPDATE products SET stock=stock-1 WHERE id=100; -- 并发时可能被多个线程执行核心解决方案
1. 数据库层方案
方案A:悲观锁(行级锁)
BEGIN;
-- 锁定目标行(MySQL InnoDB)
SELECT stock FROM products WHERE id=100 FOR UPDATE;
-- 应用层校验
IF stock > 0 THEN
UPDATE products SET stock=stock-1 WHERE id=100;
END IF;
COMMIT;- 优点:强一致性
- 缺点:并发性能低,可能死锁
- 隔离级别:需
REPEATABLE READ及以上
方案B:乐观锁(版本控制)
UPDATE products
SET stock=stock-1, version=version+1
WHERE id=100
AND stock>0
AND version=#{current_version} -- 应用层传递当前版本号- 优点:高并发性能好
- 缺点:需处理大量失败请求
- 注意:需配合重试机制
方案C:直接条件更新
UPDATE products SET stock=stock-1
WHERE id=100 AND stock>0; -- 数据库原子操作- 优点:简单高效,无需事务
- 缺点:无法获取扣减后库存值
2. 架构层增强方案
分层防御体系:
- 接入层:Nginx限流(令牌桶算法)
- 服务层:Redis分布式锁 + Lua脚本预扣减
-- Redis原子操作示例 if redis.call('get', 'stock_100') > '0' then return redis.call('decr', 'stock_100') else return 0 end - 数据库层:最终一致性扣减(通过MQ异步执行)
最佳实践
- 库存分段:将库存拆分为多桶(如10桶×100件),分散锁竞争
- 热点数据处理:
- Redis集群分片存储热点商品
- 本地缓存+标记位快速过滤无效请求
- 事务优化:
- 事务范围最小化
- 避免跨服务分布式事务(用最终一致性替代)
常见错误
- 错误1:在应用层计算库存(未用数据库原子操作)
- 错误2:
READ COMMITTED隔离级别下二次查询导致幻读 - 错误3:Redis预扣减后未设超时时间,导致库存冻结
扩展知识
- 分布式事务方案:
- TCC(Try-Confirm-Cancel)
- 基于MQ的最终一致性
- 数据库选型:
- TiDB(分布式HTAP数据库)
- 阿里云PolarDB(读写分离架构)
- 容灾设计:
- Redis与DB双写一致性方案(先更DB再删缓存)
- 库存扣减流水表(用于对账补偿)