主题
界面
主题
项目 A 的矛盾是并发安全(不能超借);项目 B 的矛盾升级为高并发吞吐:
场景:100 件库存,5000 个请求(200 并发)抢购
矛盾:瞬时流量远超存储层的处理能力 ✗✗★ 本节的实测(同一场景、三种方案) ✓✓✓:
成功 拒绝 墙钟 P50 P99 「数据库」写
① 无保护直扣 100 0 8905 ms 3 ms 1270 ms 5000 次 ✗✗
② 限流 300 QPS 100 4581 753 ms 0 ms 509 ms 419 次 ✓✓
③ 异步削峰 100 4900 72 ms 0 ms 0 ms 100 次 ✓✓✓★ 墙钟从 8.9 秒降到 0.07 秒(约 124 倍)—— 而库存始终不多不少 ✓✓
labs/project-a)✓★ 先看无保护时发生了什么(实测第 1 节)✗✗:
无保护直扣:成功 100 / 失败 4900
响应 P50=3 ms P99=1270 ms ← 失败的 4900 人等了 1 秒多才知道失败 ✗✗
墙钟 8905 ms(约 9 秒才处理完)
「数据库」写 5000 次,忙碌 98% ← 它满负荷了,毫无余力 ✗✗★ 问题不在"慢",而在三点 ✗:
- 4900 个注定失败的请求,每个都打到了数据库 ✗✗(浪费 98% 的写能力)
- 失败的响应要 1 秒多(用户体验极差)✗
- 数据库满负荷——这时候任何一点额外流量都会压垮它(雪崩前夜)✗✗
★★ 一句话(这一页要背下来) ✓✓✓:
高并发的本质不是「让每层都变快」,
而是「让流量在到达昂贵资源之前被层层消化」 ✓✓✓
★ "昂贵资源"在本场景就是数据库写 ✓:1 次写 = 1 ms(串行锁下每秒最多约 1000 次), 而流量是 5000 个请求同时来 ✗ —— 差 5 倍 ✓
流量 → 【CDN/静态化】→【网关限流】→【内存预扣库存】→【MQ 削峰】→【数据库原子扣减】| 层 | 挡住什么 | 手段 |
|---|---|---|
| CDN/静态化 ★★ | 静态请求(图片/页面) | 根本不回源 ✓✓ |
| 网关限流 ★★★ | 超出容量的流量 | 令牌桶/滑动窗口(《Spring Cloud 核心组件》) |
| 内存预扣库存 ★★★★ | 大部分无效请求 | 原子操作,挡在数据库前 ✓✓✓ |
| MQ 削峰 ★★★ | 写库的瞬时洪峰 | 异步 + 消费者按能力消费(《消息队列基础与选型》) |
| 数据库原子扣减 ★★★★ | 最后的正确性防线 | 条件 UPDATE(项目 A 已验证) |
★ 核心思想 ✓✓:每一层都"消化"掉一部分流量,让到达下一层的量 = 下一层能承受的量 ✓
| 指标 | ① 无保护 | ② 限流 | ③ 异步削峰 |
|---|---|---|---|
| 成功 | 100 | 100 | 100 |
| 拒绝(限流) | 0 | 4581 | 4900 |
| 墙钟 | 8905 ms ✗✗ | 753 ms ✓ | 72 ms ✓✓✓ |
| P50 | 3 ms | 0 ms | 0 ms |
| P99 | 1270 ms | 509 ms | 0 ms |
| 数据库写 | 5000 次 ✗✗ | 419 次 | 100 次(只写成功的)✓✓✓ |
★ 三条解读 ✓✓:
| # | 解读 |
|---|---|
| ① | 限流把 4581 个请求挡在了数据库前面 —— 数据库写从 5000 降到 419 ✓✓ |
| ② | 异步把数据库写降到 100 次 —— 只写成功的(预扣挡住了全部无效请求)✓✓✓ |
| ③ | 墙钟约 124 倍的差距,全部来自"无效请求不再消耗数据库" ✓ |
实测对比 ✓✓:
| 不限流 | 限流 | |
|---|---|---|
| 注定失败的 4900 人 | 等 3 秒才失败 ✗✗ | 0 ms 立刻失败 ✓ |
| 成功的 100 人 | P50 463 ms ✗ | P50 ≈ 0 ms ✓ |
| 数据库 | 满负荷 98% ✗✗ | 有余力 ✓ |
★★ 一条反直觉的认识 ✓✓✓:
拒绝是好事 ✓✓
快速失败让用户立刻看到"已抢完/稍后再试", 而不是转圈 30 秒后失败 ✗✗
这就是"用部分用户失败,换系统整体存活" ✓✓✓
★★★ 本项目还踩到一个真实的实验设计坑 ✓✓:
第一版把模拟写库设为 5 ms —— 数据库串行锁把请求到达速率压到了约 200/s, 比令牌桶的 300/s 还低 → 限流器一个请求都没拦到(拒绝数 = 0) ✗✗
教训:限流器必须放在真正的洪峰处,否则它什么也保护不了 ✓✓✓
把模拟写库改成 1 ms 后,到达速率远超 300/s,限流立刻生效 ✓
实测 ✓✓✓:
异步削峰:P50=0 ms / P99=0 ms / 数据库写 100 次 / 墙钟 101 ms★ 为什么能做到"0 ms" ✓✓:
请求只做两件事: ① 内存预扣库存(
AtomicInteger,纳秒级)✓ ② 把落库任务丢进队列,立刻返回 ✓✓落库由消费者线程按自己的节奏完成 —— 洪峰被摊平 ✓✓✓
★ 代价 ✓:
用户拿到的是"受理",不是"成功" ✓ ——最终一致(《消息可靠性、幂等与顺序》)
这正是所有秒杀"先返回排队中、后通知结果"的原因 ✓✓
★ 缺一个就会出事故 ✗✗:
| # | 保障 | 对应 |
|---|---|---|
| ① | 预扣库存必须原子 | AtomicInteger / Redis DECR(《缓存穿透、击穿与雪崩》)✓✓ |
| ② | 消息消费必须幂等 | 唯一索引(《消息可靠性、幂等与顺序》)✓✓ |
| ③ | 数据库扣减必须带条件 | available_copies > 0(防 MQ 重复消费超卖)✓✓✓ |
| ④ | 对账兜底 | 预扣数 vs 落库数 vs 库存数,每天校验 ✓✓ |
★ 第 ③ 条最关键 ✓✓✓:
预扣(内存)和落库(数据库)是两个世界 ✗ —— MQ 至少一次投递必然重复 ✗ —— 数据库的条件 UPDATE 是防止"重复消费导致超卖"的最后防线 ✓✓✓
AtomicInteger reserved = new AtomicInteger(STOCK);
// 每个请求:
int remain = reserved.updateAndGet(cur -> cur > 0 ? cur - 1 : -1);
if (remain < 0) { return Result.soldOut(); } // ★ 纳秒级拒绝 ✓✓
queue.submit(() -> db.tryDeduct()); // 落库异步化
return Result.accepted();见 5.2 ✓
boolean ok = db.tryDeduct(); // ✗ 先打数据库
if (!limiter.tryAcquire()) { ... } // ✗ 限流器形同虚设代码:labs/project-b/src/main/java/cn/kb/learn/seckill/SeckillLesson.java
运行:
cd labs
.\mvnw.cmd -pl project-b compile
java "-Dstdout.encoding=UTF-8" -cp project-b/target/classes cn.kb.learn.seckill.SeckillLesson实测输出(关键部分) ✓:
===== 1. 无保护 =====
无保护直扣 成功 100 拒绝 0 失败 4900
响应 P50=3 ms P99=1270 ms 墙钟=8905 ms
「数据库」写 5000 次,忙碌 98% ✗✗
剩余库存:0 ✓ 未超卖
===== 2. 限流 300 QPS =====
限流 300 QPS 成功 100 拒绝 4581 失败 319
响应 P50=0 ms P99=509 ms 墙钟=753 ms
「数据库」写 419 次
===== 3. 异步削峰 =====
异步削峰 成功 100 拒绝 4900 失败 0
响应 P50=0 ms P99=0 ms 墙钟=72 ms
「数据库」写 100 次 ✓✓✓
剩余库存:0 ✓ 未超卖| # | 错误 | 说明 |
|---|---|---|
| 1 | 以为"加机器"就能解决秒杀 ✗✗ | 瓶颈在串行资源(库存行)✓ |
| 2 | 限流器不在洪峰处 ✗✗ | 本项目实测:形同虚设 ✓✓ |
| 3 | 只有 MQ 没有内存预扣 ✗✗ | 数据库写 = 全部请求 ✓ |
| 4 | 预扣不原子 ✗✗ | 超卖 ✓✓ |
| 5 | 消费端不幂等 ✗✗ | MQ 重复消费 → 重复下单 ✓✓ |
| 6 | 数据库扣减不带条件 ✗✗✗ | 最后防线失守 → 超卖 ✓✓✓ |
| 7 | 不做对账 ✗✗ | 预扣与落库对不上没人知道 ✓ |
目标:亲手把秒杀链路调到"高吞吐且不超卖"。
步骤:
SeckillLesson,记录三组数据 ✓✓;验收方式:能画出你的秒杀分层图,并说清每层挡住什么。
扩展任务:处理"下单超时未支付":
int remain = reserved.decrementAndGet();
if (remain < 0) { return 拒绝; }这样写有什么问题? 5. (设计)你的秒杀系统上线后,监控发现"预扣 10 万次但落库只有 100 次",怎么排查?
decrementAndGet 会把库存扣成负数 ✗✗ —— 虽然后续判断能拒绝,但预扣计数已经不准(-1、-2……累积)✗ —— 要用 updateAndGet(cur -> cur > 0 ? cur - 1 : cur) 这种失败不递减的写法 ✓✓★《项目 B:缓存、消息与限流落地》:把 S4 学的三个组件在秒杀场景里用对。