先补齐必要概念
适合:需要排查重复发布、恢复丢状态或并发接管。
- 幂等
- 同一业务操作重复请求时,不额外产生重复业务效果;保证范围要看服务端实现与有效期。
- 操作身份
- 表示同一次业务意图的稳定标识,与每次网络尝试的标识不同。
- Checkpoint
- 调用方保存的计算状态,不自动等于外部服务已经发生的事实。
- 未知结果
- 调用方没有拿到可靠回执,既不能断言成功,也不能断言没执行。
原理怎样一步步成立?
- 持久化意图
在发起动作前保存稳定操作身份与参数。
- 服务方提交
服务方以自身契约执行与去重。
- 调用方核对
响应丢失时查询或沿同一身份重试。
- 更新任务状态
取得可核对回执后确认,无法核对则保留未知状态。
进阶 · 定位故障
沿故障窗口检查重放边界
本层目标:能为不同故障点定义可观察不变量。
故障注入放在哪一步
执行前失败应没有服务方效果;服务方提交后但调用方确认前失败,应能沿原身份核对;确认后恢复,不应再次制造新操作。分别观察业务记录、操作身份、回执和调用方状态,不能只看最终自然语言“完成”。
租约不等于副作用去重
租约帮助决定谁负责接管,过期执行者仍可能继续发请求。资源端令牌可以防止旧执行者的某些写入,但业务操作的去重身份仍要独立设计。重复恢复请求和重复批准都要绑定同一个逻辑动作,不能因领取了新租约就生成新的业务意图。
未知状态不能被自动擦掉
当服务方没有去重、也无法查询时,调用方不能通过无限重试制造确定性。保存未知状态和现有证据,选择人工核对或契约允许的后续动作。补偿也可能失败,需要独立记录,不能把“已请求撤销”当成已恢复原状。
运行实验,观察反例
两份本地 SQLite 文件模拟独立提交与响应丢失;无真实远端、进程终止或网络故障,不证明端到端 exactly-once。
Python 3.10+ · 默认运行只使用标准库 · 在你的电脑运行
- 观察调用方 prepared 与服务方已提交的分歧
- 沿同一操作身份获取原回执
- 用同一身份改变内容,验证冲突
python3 idempotency_recovery.py查看本入口脚本
"""Two local SQLite files model independent caller/provider commits.
Not a real remote service, production queue or end-to-end exactly-once proof.
"""
import json
import sqlite3
import tempfile
from pathlib import Path
def provider(path, operation, payload):
with sqlite3.connect(path) as db:
db.execute("CREATE TABLE IF NOT EXISTS effects (operation TEXT PRIMARY KEY, payload TEXT NOT NULL, receipt TEXT NOT NULL)")
# Serializes the read/check/write in this local demonstration.
db.execute("BEGIN IMMEDIATE")
existing = db.execute("SELECT payload,receipt FROM effects WHERE operation=?", (operation,)).fetchone()
if existing:
if existing[0] != payload:
raise ValueError("same operation with different payload")
return existing[1]
receipt = "receipt:" + operation
db.execute("INSERT INTO effects VALUES(?,?,?)", (operation, payload, receipt))
return receipt
def demo():
with tempfile.TemporaryDirectory() as folder:
remote, local = Path(folder) / "provider.db", Path(folder) / "caller.db"
with sqlite3.connect(local) as db:
db.execute("CREATE TABLE intents(operation TEXT PRIMARY KEY, payload TEXT, status TEXT, receipt TEXT)")
db.execute("INSERT INTO intents VALUES('publish-1','report-v1','prepared',NULL)")
first = provider(remote, "publish-1", "report-v1")
# Simulated response loss: provider committed, caller did not get receipt.
with sqlite3.connect(local) as db:
before = db.execute("SELECT status FROM intents").fetchone()[0]
second = provider(remote, "publish-1", "report-v1")
with sqlite3.connect(local) as db:
db.execute("UPDATE intents SET status='confirmed', receipt=?", (second,))
with sqlite3.connect(remote) as db:
effects = db.execute("SELECT COUNT(*) FROM effects").fetchone()[0]
assert before == "prepared" and first == second and effects == 1
return dict(caller_before_recovery=before, same_receipt=first == second,
provider_effects=effects, caller_after_recovery="confirmed")
if __name__ == "__main__":
print(json.dumps(demo(), sort_keys=True))
本地运行的预期输出
{"caller_after_recovery": "confirmed", "caller_before_recovery": "prepared", "provider_effects": 1, "same_receipt": true}- 服务方业务效果只有一条
- 同意图重试回执一致
- 远端能力和去重期限需另行核实
继续做进阶研究实验
进程真的退出后,还能怎样继续?
从两份数据库的幂等反例,继续验证租约、检查点和独立服务方回执。解压可靠性实验 v3,在独立目录执行。
python3 cli.py submit --db crash.sqlite
python3 cli.py run --db crash.sqlite --lease-seconds 2 --fault after_collect
python3 cli.py inspect --db crash.sqlite
# 首次运行预期退出码 75;等待至少 2 秒后分别执行
python3 cli.py run --db crash.sqlite
python3 evaluate.py --db crash.sqlite保留证据,逐条核对
- 首次 inspect 显示 running,已保存 collect 检查点。
- 租约过期后 succeeded,generation=2,collect 只提交一次。
- 按 README 再跑 after_effect,核对独立 publisher 数据库只有一个 receipt。
固定 collect → draft → verify → publish 流程;验证本地持久协议与真实进程退出,不证明任意远程服务恰好一次执行。
本层验收任务
列出执行前、服务方提交后、调用方确认后三个故障点及验收条件。
完成后逐条核对
- 断言效果数量与回执一致性
- 加入同键参数冲突与重复恢复样本
- 无法核对时保持未知状态,不伪造完成
保存自己的过程、代码与结果。这里提供验收要求,暂不自动评分或保存课程掌握状态。
收起答案,检查理解
换了接管 Worker,是否应该换业务操作键?
展开参考推导
如果仍恢复同一次业务意图,就应沿用原身份;接管执行者与网络尝试可以变化,业务意图不能因此变成新操作。
延伸原理与知识练习
遇到不熟悉的原理,先阅读实现、连续追问和迁移案例,再独立说明前提与边界。作答与笔记保存到原有账号记录。
本专题的全部关联解析与练习(5 道)
依据与验证范围
原理依据来自公开资料;数字、案例和任务是本站教学设计。离线实验验证本页注明的范围,学习效果仍需通过独立任务与反馈判断。