基础篇

F33|重试、幂等和事务:为什么超时不是失败证明

基础篇约 2.8 千字1 组分步图解

从具体对象与小例子开始,理解计算过程与适用条件。

本章目录
动态图解 · F33

超时告诉你没收到,不告诉你没发生

为什么自动重试有时会把一次工作做两遍?

外部服务可能已经完成动作,只是成功消息没回到调用方。此时“未知”是精确状态,不是系统不够聪明。对于会产生新记录、发送消息等副作用的动作,恢复需要稳定操作标识、结果核对或服务支持的幂等语义。只读查询的重试通常不需要同样复杂的设计。

幂等重复同一逻辑操作不产生额外副作用。
操作ID标识同一次逻辑动作,重试不随意换新ID。
事务在某个明确系统边界内保证一致性的机制。

输入与适用条件:构造的外部写入场景;不承诺跨数据库、文件系统和远程API天然具备“恰好一次”事务。

1 / 6 · 请求 op-1

第1步,共6步

窄屏可左右滑动图形,或点击“放大图形”。文字解释紧随图后。

超时告诉你没收到,不告诉你没发生:请求 op-1请求 op-1创建一条记录;客户端等待回执服务端提交记录已真实创建,副作用已发生回执丢失客户端超时,无法由此推断失败错误做法换新操作ID重试,可能创建第二条安全恢复使用稳定操作ID核对或幂等重试明确结果返回原结果,或确认未执行后再重试

创建一条记录;客户端等待回执

教学状态

默认静止。用“下一步”看完解释再继续;播放到最后一步自动停止。键盘聚焦播放器后,可用左右键逐步查看、空格播放/暂停。图中颜色辅助区分,含义同时用文字注明。

为什么需要

在不可靠网络下维持正确副作用次数。

怎样实现

先判断操作是否可重复,再决定重试、查状态、对账或人工确认。

可能的代价与误解

只在本地记一个成功标志,不能消除“远端已提交、本地未记录”的窗口。

如何判断是否做对

故意丢回执并重试,核对最终仅存在一条逻辑记录。

先看最危险的一条时间线

宿主发出“修改文件”→执行器成功写入→返回成功的网络响应丢失→宿主等待超时。如果此时直接重试,可能重复修改;若直接回滚,也可能覆盖后来变化。

所以除了成功与失败,还需要unknown(结果未知)。它描述观察者缺少确认,不是说外部世界处于一种神秘状态。真实动作可能已成功,也可能未发生,需要核验。

幂等不是“重试时最好别出错”

幂等idempotent表示在指定语义下重复相同操作,与执行一次产生相同预期效果。把值设置成10通常比“加10”容易幂等;发邮件通常会造成重复发送,除非服务端另有去重机制。

幂等键idempotency key标识同一逻辑操作意图。重试同一意图复用同一键;两个不同意图不应因为参数偶然相同而被当成一个操作。

服务端需在合适边界持久化键、请求意义与结果,并处理并发、有效期和冲突。客户端自己存一张表,不能自动保证远端动作恰好执行一次。

数据库事务保证到哪里

事务transaction将一组数据库操作作为受控单元处理,常讨论原子性、一致性、隔离性和持久性。这里的“原子”是外部观察者不看到这组操作的半完成状态,具体保证取决于数据库与隔离等级。

数据库提交了记录,不代表同一时刻外部文件或邮件系统也一起提交。两个系统没有共享事务边界时,需要额外协议、核验或补偿,而不是把它们放在同一个函数里就获得原子性。

CAS怎样避免覆盖新版本

CAS(compare-and-swap,比较后交换)表达“只有当前状态仍与我读到的一样,才写入新状态”。例如读取文件版本7,准备补丁;提交前发现已变8,就拒绝覆盖并重新检查。

比较必须与更新在相应边界上受保护。普通代码先检查再写,中间若允许别人修改,仍可能发生TOCTOU(检查到使用之间的竞态)。

原子替换能做什么,不能做什么

本地文件常见方法是写临时文件,再在支持的条件下原子重命名替换目标,避免读者看到半截文件。但持久化到磁盘、跨文件系统、符号链接及与数据库状态同步都有额外条件。

“原子替换”不是“任何断电都绝无丢失”,也不等于多个文件与远端API一起形成事务。需要明确具体平台保证。

重试、恢复、重放和补偿

Retry再尝试某操作;Resume从已保存进展继续;Replay根据事件重建或重新执行;Compensation用另外的动作抵消先前效果。

补偿不一定能真正还原:已发邮件不能让收件人没看过。因此高风险不可逆动作应更谨慎地确认和核验,不能把“出错再回滚”当作通用安全方案。

最小可用恢复记录

至少知道逻辑操作身份、目标、预期版本、当前已知执行状态、结果或核验方式。重启后先确认unknown动作,再决定继续;不要仅凭模型摘要“上次似乎做过”行动。

这类机制的复杂度要与副作用相称。只读搜索通常无需完整事务设计;生产部署、发送、删除和跨系统写入则不能仅靠Prompt保证。

运行示例 · F33

超时之后:重试同一个操作,还是意外执行第二次

输入与条件。对教学“计数器加1”发出操作op7。服务器完成加1,但回执在返回途中丢失。

步骤与对象内部动作中间结果 / 新状态
1调用前计数器10客户端记op7为running。
2服务器执行计数器变11真实副作用已经发生。
3客户端超时不知道服务器是否执行标记unknown,不能由超时推导failed。
4危险重试换个新身份op8再加1计数器变12,重复副作用。
5安全方向用同一业务幂等身份op7查询或重试若服务端保证该身份只结算一次,返回原结果11;否则先核验,不能假定本地去重足够。

把过程连起来。正确性取决于产生副作用的系统怎样认同“同一个操作”,不取决于客户端怎样命名一次网络请求。

边界与反例。示例没有发送真实请求。跨系统exactly-once需要额外协议与事务前提,不能仅靠一张客户端表保证。

依据与适用范围:[DERIVE]。示例输入和场景由本书明确设定。

课后自检

自检 1

“设置余额为100”和“余额增加100”在重复执行效果上有何不同?

查看参考答案收起参考答案
前者在相同语义下重复设置仍为100;后者每次增加,会改变结果,不能按同样方式盲目重试。
自检 2

数据库事务提交,能否证明邮件恰好发送一次?

查看参考答案收起参考答案
不能。邮件系统是另一副作用边界,需结合服务端幂等、结果记录和核验等机制。

机制依据:[IDEMPOTENT][SQLITE]。本节数值与情景除另注外均为原创教学示例;来源不代替中文讲解。

记录保存在本机浏览器。

输入关键词即可检索。

↑ ↓ 选择 Enter 打开Esc 关闭 · 仅本地搜索

阅读设置

阅读设置仅保存在本机浏览器,不上传任何数据。