秒杀系统设计
问题
如何设计一个支持百万并发的秒杀系统?
答案
核心挑战
| 挑战 | 说明 |
|---|---|
| 瞬时高并发 | 大量请求集中在同一时刻 |
| 库存超卖 | 并发扣减导致卖出超过库存 |
| 恶意请求 | 刷单、机器人抢购 |
| 系统过载 | 数据库、服务被打垮 |
架构设计
关键设计
1. 前端:拦截无效请求
- 按钮灰置、倒计时
- 前端 URL 动态化(防止提前获取接口)
- CDN 缓存静态页面
2. 网关层:限流 + 风控
- 用户级限流(同一用户 1 秒内只能请求 1 次)
- IP 限流
- 验证码/滑块验证
3. Redis 预扣库存(核心)
Redis + Lua 原子扣库存
// Lua 脚本保证原子性:判断库存 + 扣减 在同一命令中完成
String luaScript = """
local stock = tonumber(redis.call('get', KEYS[1]))
if stock and stock > 0 then
redis.call('decr', KEYS[1])
return 1
end
return 0
""";
// 执行
Long result = redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
List.of("seckill:stock:" + productId)
);
if (result == 1) {
// 扣库存成功,发 MQ 异步创建订单
mqTemplate.send("seckill-order", orderMessage);
} else {
// 库存不足,直接返回
return "已售罄";
}
4. 异步下单
Redis 扣库存成功后,通过 MQ 异步创建订单,削峰填谷:
用户请求 → Redis 扣库存(毫秒级) → MQ 异步下单(秒级) → 查询订单结果
5. MySQL 最终一致
消费 MQ 消息时,用数据库乐观锁做最终扣减:
UPDATE product SET stock = stock - 1
WHERE id = #{productId} AND stock > 0;
常见面试问题
Q1: 如何防止超卖?
答案:
三层防护:
- Redis Lua 原子扣减:高并发下保证库存不会减到负数
- MQ 串行消费:同一商品的消息路由到同一分区
- 数据库乐观锁:
WHERE stock > 0兜底
Q2: 如果 Redis 扣了但 MQ 消息丢了怎么办?
答案:
- 定时对账:比较 Redis 已扣数量和实际订单数,差异部分回补库存
- 本地消息表:先写本地消息再发 MQ,定时重发未确认的消息
- Redis + MQ 事务消息:RocketMQ 事务消息保证一致性
Q3: 秒杀结束后如何恢复库存?
答案:
用户未支付超时后,回补库存:
- 创建订单时设置支付超时(如 15 分钟)
- 延迟消息/定时任务检查超时订单
- 超时后回补 Redis 库存 + 数据库库存