本文目录导读:

这是一个在技术面试和架构设计中非常经典的高并发场景,所谓“秒杀峰值”,核心挑战在于瞬间的流量爆炸(通常是平时流量的几十到几百倍)对数据库、带宽和业务一致性带来的冲击。
下面我从场景痛点、核心解决思路、具体架构方案以及代码/策略细节四个维度来拆解这个案例。
场景痛点和核心指标
- 瞬时流量巨大:比如只有1万件商品,但100万人同时点击“立即购买”。
- 热点数据集中:所有请求都打在同一个商品ID上,导致数据库单行锁竞争剧烈。
- 超卖问题:必须保证库存扣减不能为负数(数据一致性)。
- 系统雪崩:如果数据库被打垮,可能拖垮缓存、应用服务器,导致整个系统不可用。
核心解决思路:分层漏斗 + 异步化
秒杀架构的本质是一个漏斗,层层过滤掉无效请求,只将极少数的有效请求透传到数据库。
客户端 ↓ (极大量请求) ① 前端/CDN 层(静态化 + 限流) ↓ (过滤大部分无效点击) ② 网关/接入层(负载均衡 + 令牌桶限流) ↓ (拦截恶意请求,控制流量进入) ③ 应用层 (Redis 预扣减库存) ↓ (内存操作,极高吞吐量) ④ MQ 异步队列 (削峰填谷) ↓ (串行化/异步处理) ⑤ 数据库层 (最终扣减)
具体架构方案与关键代码
以下是各个层次的具体落地策略:
前端/客户端层
- 按钮置灰:点击后立即禁用按钮,防止重复提交(防抖)。
- 随机延迟:在秒杀开始前,前端做
Math.random() * 1000毫秒的随机等待,避免请求在零点瞬间同时发出。
网关与接入层
- Nginx/LVS:做负载均衡,分散流量。
- 令牌桶限流:对单个用户IP或UID进行请求频率限制(每秒5次),如果超过阈值,直接返回“请求过于频繁”,不再往后传递。
- 概念对比:
- 漏桶:恒定速率流出(严格排队)。
- 令牌桶:允许一定程度的突发流量,适合秒杀这种瞬时大流量。
- 概念对比:
应用层(核心:Redis 预扣减)
这是整个秒杀系统的大脑,绝不直接扣数据库,而是操作 Redis 内存数据。
核心步骤(判断与扣减必须原子性):
- 库存预热:系统启动时,将库存数量
seckill:stock:1001提前加载到 Redis。 - 原子扣减:使用 Lua 脚本或
Redis DECR命令。 - 拦截重复请求:使用 Redis Set 记录已成功下单的用户ID。
核心代码(Redis Lua 脚本演示):
-- KEYS[1]: 库存Key (seckill:stock:1001)
-- KEYS[2]: 用户去重Set Key (seckill:users:1001)
-- ARGV[1]: 用户ID
-- 判断该用户是否已经秒杀过
if redis.call('sismember', KEYS[2], ARGV[1]) == 1 then
return 2 -- 表示重复抢购
end
-- 判断库存是否充足
local stock = tonumber(redis.call('get', KEYS[1]) or '0')
if stock <= 0 then
return 0 -- 表示已售罄
end
-- 扣减库存
redis.call('decr', KEYS[1])
-- 记录用户ID到集合中
redis.call('sadd', KEYS[2], ARGV[1])
return 1 -- 表示成功
在这一步,由于是内存操作,单机吞吐量可达每秒10万+,不存在数据库的行锁阻塞问题。
异步处理层(MQ削峰)
不能直接在 HTTP 请求线程里写数据库,如果直接写,峰值流量会瞬间打爆数据库连接池,导致连接失败。
- 方案:应用层通过 Lua 脚本后,如果返回“成功”,则将用户的详细信息(用户ID、商品ID、地址)放入 RabbitMQ / RocketMQ 中的订单队列。
- 结果:用户立刻收到“排队中,请稍后查看结果”,系统在后台以稳定的速率(例如每秒1000条)消费MQ。
数据库层(最终一致性)
后台消费 MQ 的逻辑如下:
- 读取 MQ 消息。
- 执行
UPDATE seckill_stock SET stock = stock - 1 WHERE product_id = ? AND stock > 0(乐观锁防超卖)。 - 如果更新受影响行数为 1,则生成正式订单,持久化入库。
- 如果影响行数为 0,则说明库存以极低概率被并发耗尽,进行补偿(修改订单状态为“失败”)。
关键 SQL 设计:
-- 利用数据库的行锁和条件判断,保证不超卖
UPDATE product_stock
SET stock = stock - 1
WHERE product_id = #{productId} AND stock > 0;
关键策略细节与避坑指南
-
关于库存扣减的时机
- 不要删Redis库存完就认为成功,在返回“成功”给用户前,一定要确保消息成功投递到了MQ,否则用户以为抢到了,但后台没有订单,会导致客诉。
- Redis 扣减成功,但 MQ 投递失败,需要设计 补偿机制(比如定时任务扫描 Redis 扣减记录和数据库订单比对,不一致则回补库存)。
-
真实用户”数量
- 如果是大促,建议限制:一个用户只能抢一件,利用 Redis Set 的唯一性(上面代码中的
sismember),拦截黄牛和重复提交。
- 如果是大促,建议限制:一个用户只能抢一件,利用 Redis Set 的唯一性(上面代码中的
-
关于缓存穿透
- 如果秒杀活动刚结束,商品ID被删除,所有请求都会打到数据库,需要设置 空值缓存 或 布隆过滤器 拦截非法商品ID。
-
动静分离”
秒杀页面必须静态化,放在 CDN 上,完全不经过后端服务器,让后端只处理“真正的点击请求”,而不是去渲染 HTML。
画一张图总结(时序)
sequenceDiagram
participant U as 用户
participant W as Web网关
participant R as Redis
participant MQ as 消息队列
participant DB as MySQL
U->>W: 点击秒杀按钮发送请求
activate W
W->>W: 网关限流(令牌桶)拦截恶意流量
W->>R: 执行Lua脚本(检查库存+去重+扣减)
activate R
R-->>W: 返回成功(1)
deactivate R
alt 扣减成功
W->>MQ: 发送“创建订单”消息
W-->>U: 返回“正在排队秒杀中...”
activate MQ
MQ-->>DB: 消费消息,执行UPDATE SQL (stock > 0)
DB-->>MQ: 返回影响行数
MQ-->>U: 推送“秒杀成功”通知 (异步)
deactivate MQ
else 扣减失败
W-->>U: 返回“已抢光”或“重复提交”
end
deactivate W
面试回答话术
“秒杀案例的核心思路是将压力从数据库剥离,通过 Nginx 最外层做静态化缓存和 IP 限流;通过 Redis 的原子操作(Lua脚本)完成库存的预扣减和用户去重,这一步保证了高吞吐和防超卖;扣减成功后,将订单数据写入 MQ 进行异步削峰;MQ 消费者以稳定的速率更新数据库,为了兜底,数据库的 UPDATE 语句必须带上
stock > 0条件来做最终的原子性校验,并配合定时任务做 Redis 和 MySQL 的数据一致性对账。”