跳到主要内容

秒杀系统设计

问题

如何设计一个支持百万并发的秒杀系统?

答案

核心挑战

挑战说明
瞬时高并发大量请求集中在同一时刻
库存超卖并发扣减导致卖出超过库存
恶意请求刷单、机器人抢购
系统过载数据库、服务被打垮

架构设计

关键设计

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: 如何防止超卖?

答案

三层防护:

  1. Redis Lua 原子扣减:高并发下保证库存不会减到负数
  2. MQ 串行消费:同一商品的消息路由到同一分区
  3. 数据库乐观锁WHERE stock > 0 兜底

Q2: 如果 Redis 扣了但 MQ 消息丢了怎么办?

答案

  1. 定时对账:比较 Redis 已扣数量和实际订单数,差异部分回补库存
  2. 本地消息表:先写本地消息再发 MQ,定时重发未确认的消息
  3. Redis + MQ 事务消息:RocketMQ 事务消息保证一致性

Q3: 秒杀结束后如何恢复库存?

答案

用户未支付超时后,回补库存:

  1. 创建订单时设置支付超时(如 15 分钟)
  2. 延迟消息/定时任务检查超时订单
  3. 超时后回补 Redis 库存 + 数据库库存

相关链接