第 14 章 · 综合实战:秒杀系统

四个交互演示:① 架构图 · ② 超卖对比 · ③ 限流可视化 · ④ 完整流程动画

从客户端到 MySQL:一个请求的完整旅程

点击任意组件查看说明,或点「▶ 开始模拟」看一个请求逐层穿过的过程:

👤
用户客户端
500w 用户
🌍
CDN
挡 99% 流量
🛡
Nginx / LB
IP 限流
🚪
网关
登录/验证码
⚙️
应用层
用户限流
Redis
Lua 扣库存
📦
Stream MQ
异步消息
🤖
消费者
写订单
🗄
MySQL
~1k TPS

👈 点击任意组件查看说明

架构核心思想:把数据库前面所有能挡的都挡住。每经过一层,流量都被削掉一大块,最终到 MySQL 时 QPS 已经在可承受范围。

削峰漏斗: 500w 用户 → CDN 挡掉 95% → Nginx 限流挡 60% → 网关验证码挡 50% → 应用层用户限流挡 50% → Redis 库存判定挡 99%(库存只有 1000)→ 实际打到 MySQL 仅 ~1000 个 INSERT。

V1 GET-DECR vs V3 Lua:同样并发,结果天差地别

两边初始库存相同,并发请求数相同。看 V1 怎么把库存「扣穿」到负数:

❌ V1 不安全:GET → 判断 → DECR(三步分开)

剩余库存:
10
抢购成功:0 失败拒绝:0 超卖:0

✅ V3 安全:Lua 脚本(GET+判断+DECR 原子)

剩余库存:
10
抢购成功:0 失败拒绝:0 超卖:0
原理: 左侧 V1 在多线程下,多个线程可能同时 GET 到 stock=1,然后各自 DECR,库存被扣穿。 右侧 V3 用 Lua 在 Redis 单线程内一次性完成判断和扣减,其他请求只能排队等,永远不会超卖。

滑动窗口限流(用 ZSet 实现)

每来一个请求 ZADD ts uuid,先 ZREMRANGEBYSCORE 清掉窗口外的,再数 ZSet 长度判断是否超限:

滑动窗口(绿点=放行,红点=被拒)
-3s ↑ 当前时间
0
放行
0
被限流
0
窗口内当前请求数
ZSet rate:user:1001 内容(实时刷新):
(empty)
关键点: ① ZSet 的 score 存毫秒时间戳;② 每次请求先 ZREMRANGEBYSCORE 0 (now-window) 把过期的清掉; ③ 整段「清理 + 计数 + 加入」用 Lua 包成原子,并发安全。

完整秒杀流程动画

50 个用户同时点「抢」按钮,每个请求经过 5 个步骤。颜色标记当前所在阶段:

🚪
① 限流
0
🆔
② 用户去重
0
📉
③ 扣库存
0
📦
④ 推消息
0
🗄
⑤ 写订单
0
用户头像(每个圆代表一个抢购请求):
剩余库存:10 抢购成功:0 失败:0
Stream 消息(被消费者异步处理):
等待 限流 去重 扣库存 推消息 成功 失败
注意观察: 许多请求在「限流」「去重」「扣库存」三步就被挡掉,只有少数(约等于库存数)能走到最后。 Stream 消息数 = 抢购成功数,消费者按节奏慢慢消费,MySQL 永远不会被打挂。