主题
第 6 周 · Day 7:周复盘,把一次写请求的完整路径画出来
手册任务:周复盘 + 整理。用 Excalidraw 画一张「请求进入后的缓存→锁→队列」全景流程图,写 300 字周记,当日产出:流程图 + 周记。 本篇解决的问题只有一个:五结构、缓存、锁、队列、幂等,六个主题单学都会,怎么确认它们在你脑子里串成了一条完整的请求链路。
今日目标
- 不看教程画出「写请求全景流程图」:八个主步骤、每步的组件标注、每步的失败分支,导出 PNG 存档
- 按四段模板写一篇 300 字周记
- 过一遍 10 题自检清单,答不上来的标记出来,回读对应 Day 的教程
概念讲解:为什么"串成一条线"比"六个都会"更值钱
先说一个本周特有的现象。Day 1 到 Day 6,每天学一个组件:五结构、Cache-Aside、分布式锁、BullMQ、重试与延迟、幂等键。单拿出来,每个都能讲两句。但放进真实系统,这六个东西不是并列的六个知识点,它们分布在同一次写请求的生命周期上,各守一道关卡,先后顺序本身就是设计。
于是出现一种典型的"会了但没串起来":问 SET NX PX,答得出;问幂等键,也答得出;问"一个创建订单的请求从进门到返回要过哪几道关,每道关失败怎么办",开始含糊。组件知识是点,请求路径是线。点都记住了,线没连上,说明脑子里装的是六个散件,不是一套系统。
线怎么检验?口头讲一遍容易漏,一张图画得清。所以本周复盘的核心产出是「写请求全景流程图」:节点是步骤,边是调用顺序,每个节点标两样东西,用什么组件、失败走哪条边。第 1 周的结构图考"谁依赖谁",是地图;本周的流程图考"谁先谁后、失败去哪",是路线。地图和路线是两种记忆,今天补的是后面这种。
| 输出方式 | 逼出什么 | 对应今天的产出 |
|---|---|---|
| 画图 | 组件协作的时序是否成形 | 写请求全景流程图 |
| 写作 | 概念是否真的理解 | 300 字周记 |
| 自测 | 细节是否记牢 | 10 题自检清单 |
方法还是第 1 周那套
输出倒逼输入,识别和提取的区别,第 1 周 Day 7 讲透了,没印象的先翻那篇。方法不动,变的只有图的内容:从静态结构换成动态流程。
核心知识
1. 写请求全景流程图复盘法
先定场景,别画抽象图。就用 Day 6 做过的接口:POST /orders 创建订单,请求头带 Idempotency-Key。一次请求从进门到返回,完整走一遍下面八个步骤。
工具照旧 Excalidraw,excalidraw.com 打开就画,手绘风,别在美观上花时间。
一张合格的全景流程图,照这份文字版清单画:
text
场景:POST /orders,请求头带 Idempotency-Key
主流程节点(8 个,方框,从左到右一条主线):
① 请求进来:中间件层,拿到 Idempotency-Key 请求头
② 幂等键检查:SET idem:{key} "processing" NX PX 86400000
③ 分布式锁:SET lock:order:{userId} {随机token} NX PX 5000
④ 业务校验:参数、库存、权限;查用户数据走 Cache-Aside 读
⑤ DB 写:INSERT 订单,幂等键上加唯一约束兜底
⑥ 删缓存:DEL cache:user:{userId},Cache-Aside 的写侧
⑦ 投递异步 job:queue.add('order-created', payload, { jobId: 幂等键 })
⑧ 返回:幂等键值改存结果摘要,Lua 释放锁,返回 201 + 订单号
失败边(6 条,虚线或红箭头):
②a NX 失败(key 已存在)→ 查已存结果:已完成则直接返回上次
结果;值还是 processing 则返回 409"处理中"
③a 抢锁失败 → 返回 409"操作太频繁",客户端稍后带同一键重试
④a 校验不过 → Lua 释放锁、删幂等键,返回 422
⑤a DB 写失败 → Lua 释放锁、删幂等键(让重试能重新进门),
返回 500,客户端带同一键退避重试
⑥a 删缓存失败 → 不阻塞主流程,记日志,靠 TTL 兜底
⑦a 投递失败 → 不回滚订单,落一条待发送记录,定时任务补偿投递
组件标注(写在节点旁边):
② Redis String(NX 当存在性开关)
③ Redis String + Lua 脚本释放
④ Redis 缓存 + PG 读
⑤ PostgreSQL 唯一约束(幂等的最后防线)
⑥ Redis DEL
⑦ BullMQ Queue(jobId 去重)画的时候心里要有三个层次:节点是步骤,边是时序,标注是组件。这张图最容易漏的是失败边。只画一条从 ① 直通 ⑧ 的主干,那不是全景,是理想国。真实系统里每个环节都可能失败,失败边画得全不全,直接暴露你想没想过异常路径。
还有一条隐藏考点:② 和 ③ 的顺序。为什么幂等在锁前面?幂等是无代价的门卫,挡的是已知重复;锁是有代价的闸机,挡的是未知并发。重复请求在 ② 就被礼貌送回,不该走到抢锁那一步。反过来画,功能上未必出错,但每个重复请求都要白抢一次锁、白跑一遍业务。顺序也是设计,这正是流程图要逼你想清楚的东西。
画完对着主线口头走一遍:请求带键进门,② 用 NX 登记幂等键,占了坑;③ 抢锁挡住并发;④ 校验时顺手走了一次 Cache-Aside 读;⑤ 落库,唯一约束兜底;⑥ 先写库后删缓存;⑦ 把发邮件这类慢活丢给 BullMQ,jobId 还是那个幂等键;⑧ 存结果、放锁、返回。八步里每一步单看都是本周某一天的课题,串起来才是一次完整请求。
画不出来也别对着教程抄
先凭记忆画,卡住了翻 Day 2、Day 3、Day 6 的教程确认,然后合上继续画。对着教程抄出来的图没有复盘效果。
画完导出 PNG,存到笔记目录,命名 week06-write-path.png,.excalidraw 源文件一起留着。
2. 300 字周记模板
周记不是日记。日记记感受,周记记认知变化。四段结构不变,每段一两句,300 字上下:
- 本周最大的收获:用自己的话,禁止抄教程原句
- 卡得最久的一个问题:以及怎么解决的。这段最值钱,同类坑下周还会出现
- 还含糊的概念:诚实列出来,它是下周的输入
- 下周开始前要补什么:留具体动作,别写"多练习"
第 6 周示例,照这个密度写:
text
① 本周最大收获:缓存、锁、幂等不是三个孤立考点,是同一条写链路上的三道
闸门。幂等键挡重复,锁挡并发,缓存挡读压力,谁站前谁站后本身就是设计。
② 卡得最久:压测时偶发把别的实例的锁释放了。排查半天,发现释放逻辑是
GET 再 DEL 两步,中间锁刚好过期被别人抢走,DEL 就误伤了。改成 Lua 里
比对随机 token 再删,再没复现。
③ 还含糊:ZSet 手写延迟队列和 BullMQ delayed job 的适用边界;删缓存失
败后的补偿链路。
④ 下周前补:把 Day 6 的幂等中间件脱稿重写一遍,重点写"处理中"分支。300 字是刻意的限制,逼你取舍。写得下"最大收获",写不下"所有收获",你就必须想清楚哪个才是最大。
3. 第 6 周知识自检清单
规则不变:每题先口头回答,说完整了再点开答案对照。说不出来的记下题号,回读对应 Day 的教程。
问题 1:String、Hash、List、Set、ZSet 各适合存什么?每个举一个场景。(Day 1)
答案
String 存单值和计数:token 黑名单、接口缓存、incr 计数器。Hash 存要字段级读写的对象:用户资料,改一个字段不用整存整取。List 存有序可重复的列表:最新动态流。Set 存去重和集合运算:标签、两人的共同关注(交集)。ZSet 存带分数的排序:排行榜(分数是热度)、延迟任务(分数是执行时间戳)。
问题 2:缓存穿透、击穿、雪崩分别指什么?各自的解法?(Day 2)
答案
穿透:查根本不存在的数据,缓存永远不命中,请求全打到 DB。解法:空值缓存(短 TTL)或布隆过滤器前置。击穿:某个热点 key 过期的瞬间,大量并发同时回源。解法:回源加互斥锁只放一个请求查 DB,或热点 key 不过期由后台刷新。雪崩:大批 key 同一时刻集体过期,或 Redis 整体不可用。解法:TTL 加随机抖动错开过期,Redis 做高可用,必要时多级缓存。
问题 3:SET key value NX PX 5000 为什么必须一条命令完成?拆成 SETNX 加 EXPIRE 两步会怎样?(Day 3)
答案
拆开就不是原子的。SETNX 成功之后、EXPIRE 执行之前进程崩了,这个 key 没有过期时间,永远存在,后面所有请求都抢不到锁,等于死锁。SET 的 NX 和 PX 参数在一次命令里同时完成"不存在才设置"和"设置过期",Redis 单线程执行命令,中间不会被插队,天然原子。
问题 4:释放锁为什么必须用 Lua 脚本?直接 DEL 不行吗?(Day 3)
答案
释放锁前要先确认"这把锁是我的"。直接 DEL 不检查持有者:自己的锁过期后被别的实例拿到,自己延迟完成后一 DEL,删掉的是别人的锁。GET 判断再 DEL 分两步,判断和删除之间可能插进别人的操作,还是不安全。Lua 脚本在 Redis 里原子执行"比对 value 等于自己的 token 才 DEL",中间没有缝隙。
问题 5:加锁时 value 为什么要放随机数,不放固定值或用户 ID?(Day 3)
答案
value 是锁持有者的身份证。释放时靠"比对 value 等于自己写入的值"确认身份,这个值必须只有自己知道,所以每次加锁生成一个随机 token。放固定值,任何客户端都能通过比对,等于人人可释放任何锁;放用户 ID,同一用户的两个请求会互相释放对方的锁。这个随机数就是问题 4 里 Lua 比对的那一方。
问题 6:重试为什么要退避?退避为什么还要加随机抖动?(Day 5)
答案
退避指失败后等待时间逐次拉长(1s、2s、4s,指数退避),给下游喘息恢复的时间,别在故障期间被同一频率反复捶打。加抖动是因为固定间隔会让大量客户端在同一时刻集体重试,形成同步重试风暴。抖动把重试时刻错开。BullMQ 里配置 backoff: { type: 'exponential', delay: 1000 } 就是在做这件事。
问题 7:延迟任务有哪几种实现?各适合什么场景?(Day 5)
答案
三种常见做法。BullMQ delayed job:job 带 delay 参数,到点才进待执行队列,自带持久化和重试,适合"下单 30 分钟未支付自动取消"这类业务任务。ZSet 手写轮询:score 存执行时间戳,定时扫到点的成员,适合没有队列设施、量小、不需要重试保障的场景。Redis 过期键事件监听:fire-and-forget,通知会丢、时机不准,别用它做延迟任务。
问题 8:幂等和分布式锁都在防重复,区别是什么?只加锁不做幂等行不行?(Day 3 / Day 6)
答案
锁防并发:同一时刻只放一个请求执行,是临时管制,锁释放后下一个请求照常执行,结果可能是第二条订单。幂等防重复:同一操作不管来几次,最终效果和来一次相同,是永久承诺。锁管过程中不撞车,幂等管结果不留两份。只加锁不行:客户端超时重试时,第一次的锁早释放了,第二次照样完整执行,库里出两条记录。锁能当性能优化(挡住并发重复点击),正确性得靠幂等。
问题 9:Idempotency-Key 由谁生成?为什么不能服务端生成?(Day 6)
答案
客户端生成,放在请求头里,每次重试带同一个 key。原因:只有客户端知道"这是重试还是一次新操作"。服务端生成的 key 每次请求都是新的,幂等检查永远通过,等于没做。服务端的职责是收下这个 key:用 SET NX 登记处理状态,处理完把结果摘要存进去,重复请求直接返回上次结果。Stripe 的 Idempotency-Key 就是这套规范。
问题 10:Cache-Aside 写数据时为什么先更新 DB 再删缓存?反过来会怎样?(Day 2)
答案
反过来(先删缓存再写 DB)的窗口更大:删完缓存、DB 还没提交的空档里,并发读请求发现缓存缺失,回源读到 DB 旧值并写回缓存,旧数据从此驻留缓存直到 TTL 到期,读请求长期拿到错的。先更新 DB 再删缓存,不一致窗口只剩"DB 已更新、缓存还没删"这一小段,下次读回源拿新值回填,最终一致。极端并发下仍有小概率不一致,所以加 TTL 兜底。两相取舍,先写库后删缓存更稳。
10 题全对也别飘
全对说明第 6 周及格,不说明可以跳过复盘。图、周记、git 三件事做完,本周才算收口。
动手任务:完成本周复盘
按顺序做完五步,预计 60 到 90 分钟。
第一步:画写请求全景流程图(25 分钟)
关掉教程,打开 excalidraw.com,对照上面的文字版清单凭记忆画:八个主节点、七条主链、六条失败边、每步的组件标注。卡住的地方翻 Day 2、Day 3、Day 6 的教程确认,合上继续。画完自查两件事:失败边齐不齐,② 和 ③ 的顺序对不对。
第二步:导出存档(5 分钟)
菜单点 Export 导出 PNG,存到笔记目录,命名 week06-write-path.png,源文件一起留。
第三步:写 300 字周记(20 分钟)
按四段模板写,对照示例的密度。"还含糊"那段别敷衍,它是下周的补课清单。
第四步:做自检清单(15 分钟)
10 题逐个口头回答,答不上来的记题号回读对应 Day。回读也算复盘的一部分,不丢人。
第五步:git 提交整理(15 分钟)
零散的提交不要去改历史,rebase 重写历史的收益远小于风险。收尾两件事:未提交的内容分开提交,打一个 tag。
bash
# 未提交的内容按类型分开:
git add src/redis
git commit -m "feat(redis): 补充锁释放脚本的注释"
git add src/queue
git commit -m "feat(queue): 邮件 job 配置指数退避"
git add docs
git commit -m "docs: 第 6 周周记与流程图"
# 打标签收口:
git tag week06-done原则跟第 1 周一样:一个提交只做一件事。代码一个提交,配置一个,文档(周记、流程图)一个。
常见踩坑
流程图只画 happy path。 一条 ① 直通 ⑧ 的主干谁都会画,画了等于没画。全景图的价值在失败边:每步失败走哪、谁负责清理(锁要释放、幂等键要不要删)、客户端拿到什么。失败边画不全,说明异常路径从没想过。
画成了系统架构图。 三个大框 Redis、PostgreSQL、BullMQ,中间连几条线,这是部署图不是流程图。区别一句话:架构图回答"系统里有什么",流程图回答"一次请求先后经过谁"。今天要的是后者,主语永远是"一次写请求"。
幂等和锁的顺序画反。 先抢锁再查幂等,重复请求会真的去抢锁、进业务,走到半路才被拦下,白白消耗。幂等是门卫,站在最外面;锁是闸机,站在门卫后面。画反不算致命错误,但它暴露你还没想清楚两个组件各自挡什么。
周记写成流水账。 "周一学了五结构,周二学了缓存",这是目录不是周记。四段缺一不可,第三段"还含糊的"最值钱,写不出来多半是在回避。
自检先看答案。 题目没想就点开答案,"哦这个我知道"。认得答案和说得出来之间隔着一条鸿沟,先口头回答再对照,顺序不能反。
延伸阅读
- Excalidraw 官网:打开就能画,源文件记得一起存
- Stripe 的 Idempotent Requests 文档:幂等键设计的工业标准,Day 6 的 Idempotency-Key 请求头就是照它的思路做的,搜 "Stripe idempotent requests" 即可找到
- Martin Kleppmann 的文章 How to do distributed locking:讲 Redis 锁的能力边界和 fencing token,读完你会明白为什么问题 8 的答案是"锁替代不了幂等",搜标题即得
下周开始前,把周记第四段留的补课动作清掉。第 6 周到此收口。