TT Lab
开始
学习 学习路径 课程

幂等性 — 点两次也只扣一次款

删除幂等键也意味着保证结束

在 TT Lab 中继续学习

目标

区分已完成和进行中的记录,并实现保留期限与分批清理。

为什么重要

表变得很大之后,把旧的幂等键全删掉了。结果仍在支付中的请求的记录也消失了,重试就被当作新请求进来。已完成响应的保留期限,与进行中任务的所有权,并不适用同一种过期策略。清理不是简单的 DELETE,而是一次改变保证范围的状态转换。

步骤

  1. 在 /root/work/idem-retention-lab/service.py 中,init_db(path) 以幂等方式创建 keys(id TEXT PRIMARY KEY,fingerprint TEXT NOT NULL,status TEXT NOT NULL,response TEXT,expires REAL NOT NULL)。

先做一次准备。已有的文件不会被覆盖。

mkdir -p /root/work/idem-retention-lab
test -e /root/work/idem-retention-lab/service.py || cp /opt/fixtures/ten_labs/idem-retention-lab/service.py /root/work/idem-retention-lab/service.py
cd /root/work/idem-retention-lab
  1. 在 /root/work/idem-retention-lab/service.py 中,reserve(path,key,digest,expires) 把不存在的键以 pending、response=NULL 插入并返回 True。对已有的键,无论是否过期都不改动,返回 False。

  2. 在 /root/work/idem-retention-lab/service.py 中,finish(path,key,digest,response) 只对 key 和 fingerprint 都一致且为 pending 的行,把 response 以 JSON 保存并改为 done,返回 True,其余返回 False。

  3. 在 /root/work/idem-retention-lab/service.py 中,fetch(path,key,now) 对 done 且 expires>now 的行,把 response 按 JSON 解析后返回,其他情况返回 None。

  4. 在 /root/work/idem-retention-lab/service.py 中,expired(path,now,limit=10) 检查 limit 是 1–100 的 int(不含 bool)。它按 id 升序,最多返回 limit 个 done 且 expires<=now 的 id。

  5. 在 /root/work/idem-retention-lab/service.py 中,purge(path,now,limit=10) 用与 expired 相同的 limit 校验、条件和排序挑出候选,在同一个事务中删除,并返回被删除的 id 列表。不删除 pending。

  6. 在 /root/work/idem-retention-lab/service.py 中,counts(path) 返回 {pending:数量, done:数量}。即使没有某个状态,键和 0 也必须存在。

  7. 在 /root/work/idem-retention-lab/service.py 中,retention_cycle(path,key) 以 expires=10、digest='v1' 预留,并用 {receipt:1} 完成。在 now=10 时执行 purge,然后用 digest='v2'、expires=20 对同一个键重新预留,并返回该结果的 bool。该函数以空 DB 为前提。

参考

把是否完成与过期分开

在 /root/work/idem-retention-lab/service.py 中,init_db(path) 以幂等方式创建 keys(id TEXT PRIMARY KEY,fingerprint TEXT NOT NULL,status TEXT NOT NULL,response TEXT,expires REAL NOT NULL)。

先做一次准备。已有的文件不会被覆盖。

mkdir -p /root/work/idem-retention-lab
test -e /root/work/idem-retention-lab/service.py || cp /opt/fixtures/ten_labs/idem-retention-lab/service.py /root/work/idem-retention-lab/service.py
cd /root/work/idem-retention-lab

不能只看过期时间就断定业务已经结束。

保存后用 bash /opt/lab/checks/idem-retention-lab/01-contract.sh 确认。

预留进行中的键

在 /root/work/idem-retention-lab/service.py 中,reserve(path,key,digest,expires) 把不存在的键以 pending、response=NULL 插入并返回 True。对已有的键,无论是否过期都不改动,返回 False。

删除记录与复用键要分开,并明确地加以控制。

保存后用 bash /opt/lab/checks/idem-retention-lab/02-contract.sh 确认。

只完成当前请求

在 /root/work/idem-retention-lab/service.py 中,finish(path,key,digest,response) 只对 key 和 fingerprint 都一致且为 pending 的行,把 response 以 JSON 保存并改为 done,返回 True,其余返回 False。

连指纹也一起比较,防止正文不同的 worker 覆盖已完成的响应。

保存后用 bash /opt/lab/checks/idem-retention-lab/03-contract.sh 确认。

只重放有效的已完成响应

在 /root/work/idem-retention-lab/service.py 中,fetch(path,key,now) 对 done 且 expires>now 的行,把 response 按 JSON 解析后返回,其他情况返回 None。

固定下这份契约:在 now==expires 的边界上不再重放。

保存后用 bash /opt/lab/checks/idem-retention-lab/04-contract.sh 确认。

限制清理候选

在 /root/work/idem-retention-lab/service.py 中,expired(path,now,limit=10) 检查 limit 是 1–100 的 int(不含 bool)。它按 id 升序,最多返回 limit 个 done 且 expires<=now 的 id。

一次性清空整张表会拉长锁的持有时间,也容易把进行中的行混进来。

保存后用 bash /opt/lab/checks/idem-retention-lab/05-contract.sh 确认。

把选择和删除放在同一个事务里

在 /root/work/idem-retention-lab/service.py 中,purge(path,now,limit=10) 用与 expired 相同的 limit 校验、条件和排序挑出候选,在同一个事务中删除,并返回被删除的 id 列表。不删除 pending。

不要在查询候选和删除之间,留下状态可能发生变化的缝隙。

保存后用 bash /opt/lab/checks/idem-retention-lab/06-contract.sh 确认。

实际统计各状态的数量

在 /root/work/idem-retention-lab/service.py 中,counts(path) 返回 {pending:数量, done:数量}。即使没有某个状态,键和 0 也必须存在。

必须能够观测到,清理之后未完成的行有没有消失。

保存后用 bash /opt/lab/checks/idem-retention-lab/07-contract.sh 确认。

确认删除之后保证范围就结束了

在 /root/work/idem-retention-lab/service.py 中,retention_cycle(path,key) 以 expires=10、digest='v1' 预留,并用 {receipt:1} 完成。在 now=10 时执行 purge,然后用 digest='v2'、expires=20 对同一个键重新预留,并返回该结果的 bool。该函数以空 DB 为前提。

不掩盖“清理之后键会变成新请求”这个局限,而是亲自重现它。

保存后用 bash /opt/lab/checks/idem-retention-lab/08-contract.sh 确认。