CHAPTER 06 / 自学试学版

保存没有回复,接下来该怎么办?

读解释、检查证据、修订自己的设计。正文与参考记录可离线阅读,真实网络观察需要联网或启动本机服务。

导出包含第 0–7 章作答。导入会替换八章全部作答,包括文件中未包含的章节;建议先导出备份。

先修:请求、事务、接口与权限 · 核心学习无需安装

6.1 没收到回复,系统可能已经做了事

前几章中,小林的笔记应用已经有了清楚的分工:浏览器发出创建请求,服务验证身份、权限和内容,数据库提交,服务再返回笔记编号。现在加入一个变化:小林点击“保存周末计划”,等了两秒,页面超时。问题不再只是“怎样让请求成功”,而是“重发会不会做两遍”。

沿原路线找断点:请求可能没到服务,服务可能仍在处理,也可能已提交但回复没有到达。三种情况都可能让浏览器等不到成功。超时只结束这次等待,不会自动撤销远端已提交的写入。取消按钮也不能天然保证撤销;若产品要支持取消,服务必须另有协议。

本章要交付一份可靠保存协议:说清操作身份、未知状态、重试边界,以及哪些保证仍需实现和实测。我们先限定为“创建一条笔记”,再对照两个独立编辑的冲突。

判断一:只看到了浏览器超时,哪句话有证据支持?

6.2 给一次业务意图一个稳定身份

小林想创建一条笔记,浏览器在首次发送前生成操作编号 op-a7,并保留它与原内容。没收到确认而重发,仍用这个编号;明天有意创建另一条,即使标题相同,也用新编号。编号识别业务意图,不是每次网络传输的序号。

这引出幂等:同一操作重复处理时,约定的业务效果与处理一次一致。这里的目标是“同一次创建最多留下一条笔记”,不是网络只发送一次,也不是保证一定完成。禁用按钮挡不住刷新和程序重试;按标题去重又会误伤有意创建的同名笔记。

承接第五章,服务先从可信身份凭据确定用户,再以“用户+操作编号”识别本接口的一次创建。下表是本课选择的协议,不是所有 API 自动遵守的规则;每次读取或重放结果仍要验证当前权限。

收到的请求本课约定业务结果
新的操作编号校验、创建,并记录内容特征与结果返回新笔记编号
保留期内,同编号、同内容找到已完成记录,返回原结果仍是同一条笔记
同编号、不同内容拒绝并解释冲突,例如返回 409不偷偷改成新的创建
判断二:第一次仍未确认,小林改了标题,继续用 op-a7 发送。服务应该怎样做?

6.3 编号只是约定,还要守住实现边界

“先查询编号,不存在就创建”仍有缝隙:两个请求可能同时查到不存在,再各建一条。连接第三章的规则,可以让数据库唯一约束协调同用户、同编号的竞争,并把“登记编号、创建笔记、保存结果”放在同一事务中。否则笔记已提交、操作记录未留下,下一次仍可能重复。

深化:竞争失败的一方怎么办?

等待获胜事务提交后,读取已存结果并核对内容。若等待超时,仍保留原编号,按协议稍后确认,不能绕过约束再插入。真正的正确性还需验证同时请求、事务回滚和等待超时;网页里的串行模拟不能代替这些测试。

保证还受保留期限制。操作记录清除后,旧编号查不到,不等于原笔记没写入;查询时尚未提交也可能暂时查不到。因此契约要写明保留多久、怎样查询已完成结果,以及超过期限后怎样人工核对。若要求长期自动恢复,就要另设计长期身份与保留成本,不能从一个短期缓存推导永久承诺。

6.4 重试要有停止条件,不是把次数用完

标题过长不会因为重发而变短;没有权限和同编号异内容也需要先处理。网络短暂中断、限流或服务暂不可用,只是候选重试场景,还要确认接口允许安全重试。错误状态按所调用 API 的约定解释,不能把所有失败一律重发。

给小林的创建操作一组教学预算:总计最多等 8 秒,每次请求最多 2 秒,最多尝试 3 次,包含首次。重试之间分别在 0–0.5 秒和 0–1 秒中选等待时间。逐渐拉长等待叫退避;让不同用户不要同时回来叫随机抖动。它们降低集中重发的压力,不负责阻止重复写入。

总预算包含请求与等待,每次准备发送前都检查剩余时间。成功、明确不可重试、次数耗尽、截止或用户取消时,停止后续发送。服务建议的等待超过剩余预算,就结束本轮,不能为了凑次数提前冲过去。停止时若结果仍未知,保留草稿、原编号和待确认入口。

下面的小实验使用另一组便于手算的时钟:总预算 10 秒,每次请求占 1 秒,两次重试前固定等待 1 秒、2 秒;三次都尝试时共推进 6 秒。它用次数上限结束,不演示随机抖动。上面的 8 秒方案是协议设计例子,两组数字都不是项目默认配置。

下面是可操作的教学模拟。选择故障位置和重发方式,先预测,再对照“实际写入”与“客户端知道什么”。模拟能揭示机制差别,不是在真实网络上制造故障,也不证明真实分布式系统的并发正确性。

可选继续使用原可靠保存样板的综合实验,检查时间预算与多客户端等待;它同样是模拟。课件实际部署与本机演示具备哪些能力,应以对应说明和实测证据为准。

6.5 两次不同编辑,不是同一次保存重发

回到第三章的双窗口:甲乙都读到笔记 v1。甲改时间并保存为 v2,乙还基于 v1 修改地点。二者有不同操作编号,分别执行一次也可能造成乙覆盖甲。去重解决“同一意图做两遍”,版本检查解决“不同意图基于旧状态争写”,不能互相替代。

若业务要求不静默丢失已确认编辑,可让修改携带读到的版本,服务原子地检查版本再更新;不匹配则保留草稿,要求重新读取或合并。HTTP 的 If-Match 是一种条件请求方式。若产品明确允许最后写入覆盖,也可以采用,但必须承认代价,不能称作冲突已消失。

错例诊断:每次都用新编号,也有事务,所以不会丢编辑?

新编号无法识别重试;事务也不会自动知道两个用户希望怎样合并内容。分别提出稳定身份规则和旧版本规则,再写对应的失败反馈,才补齐了这两个问题。

6.6 把可靠性写回自己的接口

为笔记应用写一页协议:首次发送前保留什么,服务如何识别重试,异内容如何拒绝,何时停止,以及未知结果怎样继续确认。再把场景换成“发送提醒邮件”:数据库只记录一次,是否就能保证外部邮件只发一次?标出需要另找证据的外部副作用。

提示 1:先画两列

左列写数据库可能发生什么,右列写客户端目前知道什么。只有可信结果到达后,未知才能变成已确认。

提示 2:为每次再试回答三个问题

还是原业务意图吗?仍在服务承诺范围内吗?错误类型和剩余预算允许再试吗?任一不清楚,就先补规则。

参考判断与可接受替代

一种方案是保留期内同用户、同编号、同内容返回原结果,异内容拒绝;只对允许的暂时错误有限重试,截止后待确认。需要维护操作记录,是它的成本。也可采用预先确定笔记 ID 的创建协议,但须解释唯一性、条件创建、权限与冲突。邮件服务是另一边界,仅数据库事务不足以证明邮件不重复。

勾选只保存自评。不能说明某一条件时,回到对应小节修订协议;无需为了完成本章搭建复杂后端。

精确选读