“没有收到成功”和“操作没有发生”是两件事。
识别未知结果,设计重试身份,让去重记录与副作用原子提交。
前置:状态机的有序决策、请求响应与原子比较更新。
假设递增在服务端 20 ms 时提交,调用者在 150 ms 停止等待,而响应被故意延迟至 800 ms。直接重试普通递增,就可能把同一个意图执行两遍。共识并不阻止它:两次递增完全可以是日志中的两条合法命令。
调用者 应用服务 etcd increment ───────► transaction ────────────► 提交 150 ms 截止 │ 重试 ────────────► 响应延迟 800 ms 无身份:再次执行。有身份:返回已保存结果。
幂等指重复执行同一操作,与执行一次具有相同效果。把值设置成 7 可以幂等;加 7 通常不幂等。重试键让服务把重复的非幂等意图转换为查询已提交结果。键的作用域应包含调用主体与操作,并绑定请求内容,明确保留期限与过期后的行为。
示例 HTTP 应用先读计数器,再提交一笔 etcd Txn,比较计数器 revision,并在去重模式比较操作键不存在。成功分支同时写入新计数和操作的 amount/result。并发变化让比较失败时,应用重新读后再尝试,有次数上限。
若 operation_key 已存在:
要求保存的 amount == 本次 amount
返回保存的 result
Txn:
比较 counter.mod_revision == 之前读取的 revision
比较 operation_key.version == 0
写 counter = old + amount
写 operation_key = {amount, result}持久化操作键填上“副作用已发生,但去重记录还不存在”的缝隙。若分两笔事务写,就重新引入这个窗口。本例所有效果都在 etcd 内部;发邮件、远程扣款或写另一个数据库,需要额外协议。这里不声称外部副作用恰好一次。
应用先完成提交,再把真实 HTTP 响应故意延迟 800 ms;调用者的截止时间为 150 ms。另一个正常请求重试同一操作。普通模式的最终计数应为 2;去重模式应为 1。去重模式再并发重试四次,均应返回已保存结果;同一 ID 换成 amount=9 应报告冲突。
查看客户端结果、服务端提交记录与最终计数。服务端提交是实验观察者获得的证据,不是超时客户端当时拥有的信息。故障来自真实 HTTP 服务中的延迟响应,不是生成的示意图。
动手:在下载副本中修改 lab/services.py,让去重标记在计数器之后单独写入。在两次写之间插入崩溃,预测重试如何产生重复效果。仅在自己的学习运行时操作,保留基线通过结果以供对照。基线去重记录不设过期;生产清理必须定义重试有效期与过期重放约定。
它为两条命令排序,不知道它们代表相同业务意图。应用仍需要身份和原子的效果/结果记录。
不可以。基线会识别冲突;静默返回无关的旧结果会隐藏调用者错误。