题目
设计一个简单秒杀系统的库存扣减方案
信息
- 类型:问答
- 难度:⭐
考点
并发控制, 数据库事务, 缓存应用
快速回答
实现秒杀库存扣减的核心要点:
- 使用数据库事务:确保查询和扣减操作的原子性
- 乐观锁机制:通过版本号避免超卖问题
- 缓存预热:提前加载库存数据到Redis减少数据库压力
- 请求拦截:在缓存层进行库存预减,过滤无效请求
原理说明
秒杀系统的核心挑战是在高并发下保证库存数据的准确性和系统稳定性。需要解决两个关键问题:
- 超卖问题:多个请求同时扣减库存导致库存变为负数
- 数据库压力:大量请求直接访问数据库导致服务崩溃
解决方案与代码示例
1. 数据库层设计(MySQL)
使用乐观锁实现原子操作:
CREATE TABLE seckill_items (
id INT PRIMARY KEY,
name VARCHAR(100),
stock INT NOT NULL, -- 库存数量
version INT DEFAULT 0 -- 乐观锁版本号
);扣减库存的SQL操作:
UPDATE seckill_items
SET stock = stock - 1,
version = version + 1
WHERE id = 1001
AND stock > 0
AND version = #{currentVersion}; -- 匹配当前版本号2. 缓存层设计(Redis)
缓存预热脚本:
import redis
# 启动时加载数据库库存到Redis
r = redis.Redis()
r.set('seckill:1001:stock', 100) # 商品ID1001初始库存100请求处理逻辑:
def handle_seckill(user_id, item_id):
# 1. Redis预减库存
stock = r.decr(f'seckill:{item_id}:stock')
if stock < 0:
r.incr(f'seckill:{item_id}:stock') # 恢复库存
return "秒杀已结束"
# 2. 数据库正式扣减(包含在事务中)
try:
# 执行上述SQL更新操作
create_order(user_id, item_id) # 创建订单
return "秒杀成功"
except Exception as e:
r.incr(f'seckill:{item_id}:stock') # 回滚Redis库存
return "系统繁忙"最佳实践
- 读写分离:秒杀读请求走Redis缓存,写请求走数据库
- 分层过滤:
- 前端:按钮置灰防止重复提交
- 缓存层:库存预减拦截99%请求
- 数据库层:最终一致性保证
- 热点数据隔离:秒杀商品数据单独使用Redis实例
常见错误
- 先查询后更新:
SELECT stock后直接UPDATE会导致超卖 - 忘记缓存回滚:数据库操作失败时未恢复Redis库存
- 过度依赖缓存:未做数据库兜底导致数据不一致
扩展知识
- 令牌桶限流:使用Guava RateLimiter控制每秒请求量
- 库存分段:将100件库存拆分为10个Redis分片,减少单个key压力
- 异步下单:扣减库存成功后发送MQ消息创建订单(中级进阶方案)