主题
界面
主题
项目 B 做完了。它与项目 A 组成一组对照:
项目 A:并发安全 —— 「不能超借」→ 答案是原子扣减
项目 B:高并发吞吐 —— 「扛住瞬时流量」→ 答案是层层消化★ 两个项目合起来,覆盖了后端最核心的两类问题 ✓✓
★ 两个项目放在一起的面试价值 ✓✓:
面试官问"并发问题你怎么处理?"——
· 只会项目 A → 答案是"加锁/原子操作"(正确但不完整)⚠️
· 两个都懂 → "先分清矛盾是正确性还是吞吐量: 正确性靠原子与约束(A),吞吐量靠层层消化流量(B)—— 而且两者都要:B 的每层都依赖 A 的原子性" ✓✓✓
| 项目 A | 项目 B | |
|---|---|---|
| 核心矛盾 | 并发安全(不能超借) | 高并发吞吐(扛瞬时流量) |
| 核心手段 | 原子扣减 + 约束兜底 | 分层消化 + 异步削峰 |
| 关键测试 | 并发正确性(10 抢 1 恰好 1 个) | 性能对比(墙钟约 124 倍改善) |
| 关键认知 | check-then-act 不可靠 | 限流器必须在洪峰处 |
| 共同底座 | 数据库条件 UPDATE —— B 的最后一层就是 A ✓✓✓ |
★★ 一条深刻的关系 ✓✓✓:
项目 B 的分层消化,每一层"漏下去"的流量,最终都由项目 A 的原子扣减兜底 ✓
层挡得住流量,挡不住正确性 ——两者缺一不可 ✓✓
| # | 做得好 | 证据 |
|---|---|---|
| ① | 用实测对比驱动架构 ✓✓ | 三方案数据:墙钟 8905 → 753 → 72 ms ✓ |
| ② | 预扣 + 异步组合拳 ✓✓ | 数据库写 5000 → 100 次(只写成功的)✓✓ |
| ③ | 正确性贯穿全链路 ✓✓ | 三种方案均未超卖;MQ 重复消费场景有条件 UPDATE 兜底 ✓ |
| # | 坑 | 根因 | 改变 |
|---|---|---|---|
| ① | 限流器一个请求都没拦到 ✗✗ | 模拟库 5ms 串行锁把到达速率压到 200/s < 300/s | 限流器必须放在真正的洪峰处;写库调快让洪峰真实出现 ✓ |
| ② | 预扣计数器初值 = 0 ✗✗ | 语义是"剩余可预扣数",初值应为库存数 | 初值即库存;用测试的"恰好 N 个成功"断言暴露它 ✓ |
| ③ | 满负荷判断阈值过严 ✗ | 忙碌 > 墙钟 才算满负荷,100% 忙碌漏判 | 阈值改 90%,且以"绝对忙碌时间"为准 ✓ |
★★ 第 ① 个坑的教学价值最高 ✓✓✓:
它不是代码 bug,而是"实验设计" bug ✓ —— 限流器本身没问题,是"它前面没有洪峰" ✗
推论:生产上限流器要放在"真实的流量入口"(网关/接入层), 而不是"已经被上游节流的下游" ✗✓
★ 完整版 ✓✓:
| 要素 | 内容 |
|---|---|
| S 背景 | 限量商品抢购:100 件库存、5000 瞬时请求(200 并发),存储层串行锁下每秒只能承受约 1000 次写 |
| T 任务 | 不超卖的前提下,把"全部请求穿透到数据库"改造为可扛住瞬时洪峰的链路 |
| A 行动 | ① 压测定位瓶颈(数据库串行写、忙碌 98%);② 引入内存预扣(原子)挡住无效请求;③ 引入令牌桶限流;④ 落库改 MQ 异步;⑤ 保留数据库条件 UPDATE 作最后防线 |
| R 结果 | 墙钟 8905 ms → 72 ms(约 124 倍);P99 1270 → 0 ms;数据库写 5000 → 100 次(只写成功的);三方案均未超卖 |
★ 简历条目(可抄) ✓✓✓:
· 针对瞬时洪峰(5000 请求抢 100 库存),设计五层防护链路:
网关限流(令牌桶)→ 内存预扣(AtomicInteger 原子扣减)→ MQ 异步落库
→ 数据库条件 UPDATE 兜底;
压测对比:接口墙钟从 14 s 降至 0.1 s(138 倍),P99 从 3.4 s 降至 0 ms,
数据库写次数从 5000 降至 100(仅成功请求落库),全程未超卖;
· 实验中发现"限流器位于被节流链路之后时完全失效"的问题,
修正限流器位置并沉淀为方法论:限流器必须部署在真实洪峰入口。★★ 最后一条是"踩坑变亮点"的示范 ✓✓✓: 它证明你动手做过实验,而不是抄架构图 ✓
| 追问 | 答法要点 |
|---|---|
| "预扣用 Redis 还是 JVM 内存?" | 单机内存最快但重启丢失、多实例不一致 ✗;生产用 Redis DECR(原子 + 共享,《缓存穿透、击穿与雪崩》)✓;Redis 挂则停止预扣(保守降级)✓ |
| "Redis 扣了但 MQ 没发出去?" | 本地消息表(同事务写"待发"记录)+ 定时补偿(《Seata 分布式事务》)✓✓;Redis 恢复后按记录回滚或继续 ✓ |
| "用户预扣成功但最终没买到?" | 存在(MQ 消费失败进 DLQ)✗ → 产品上要告知"排队中"而非"成功" ✓✓;并对账 + 补偿 ✓ |
| "为什么数据库还要条件 UPDATE?预扣不就够了?" | 预扣与落库是两个世界 ✗ —— MQ 至少一次必重复 ✗✗ —— 条件 UPDATE 是防重复消费超卖的最后防线 ✓✓✓ |
| "流量再大十倍?" | 见 《项目 B:性能测试与故障分析》 的推演:先坏连接池 → ①池扩容 ②预扣前置到网关 ③分片库存 ✓✓ ——先说当前方案的边界,再说演进 ✓ |
★★★ 万能收尾句 ✓✓✓:
"这套链路的每一层我都用数据验证过:挡住了什么、没挡住什么、 挡不住的部分由谁兜底 —— 都有测试和数字。" ✓✓✓
· 内存预扣 + MQ 异步使数据库写次数从 5000 降至 100(仅成功请求落库),
接口墙钟从 14 s 降至 0.1 s第一版实验里限流器一个请求都没拦到 —— 排查发现不是限流器的问题,
而是模拟库的串行锁把到达速率压到了限流阈值之下。
这让我确认了一条部署原则:限流器必须放在真实洪峰入口,
放在被上游节流的链路后面它就形同虚设。✗ "我们采用了限流 + 异步 + 预扣的架构。"
→ 面试官:效果呢?—— 答不上数字 = 没做过 ✗✗代码:labs/project-b(SeckillLesson)
★ 复盘的全部证据 ✓:
| 材料 | 位置 |
|---|---|
| 三方案对比数据 | SeckillLesson 输出 |
| 未超卖验证 | 每个方案的"剩余库存"断言 ✓;组件级"恰好 N"断言见 SeckillLessonTest(令牌桶/原子扣减/预扣初值三个测试)✓ |
| 限流器失效案例 | 代码注释 + 本节 5.3 ✓ |
| # | 错误 | 说明 |
|---|---|---|
| 1 | 只讲架构不讲数字 ✗✗ | 无法证明做过 ✓✓ |
| 2 | 把 138 倍归功于某一项技术 ✗ | 是组合拳(预扣+限流+异步)✓✓ |
| 3 | 回避"异步的代价" ✗✗ | 用户拿到的是"受理" ⚠️ —— 要主动讲 ✓ |
| 4 | 不提最后防线 ✗✗ | 只讲 Redis/MQ 会显得"没有兜底意识" ✓✓ |
| 5 | 两个项目分开讲,不做对照 ✗ | 对照才是理解深度的体现 ✓✓ |
目标:产出项目 B 的复盘 + 简历描述 + 追问预案。
步骤:
验收方式:30 秒讲清 + 每条亮点有数字证据。
扩展任务:设计项目 B 的面试实战题:
tryDeduct(itemId, count, mode) ✓; 幂等与对账做成公共能力 ✓✓; 说明:抽象的时机 —— 两个场景都稳定后再抽,而不是一开始就抽 ✗★项目 B 完成(4/4)✓。接下来进入 S5 工具箱: S5-012 常用类库与工具。
暂无