Go E08:双实例租约与重试为何不等于恰好一次
E07的独立消费者可以向累计任务服务提交任务;但原服务在单实例重启时把残留 RUNNING 标为 FAILED,这不等于另一个实例可以安全接手。为把差异变成可复跑的实验,本篇在同一 go_tasks 表与原统计函数旁增加显式的 taskstats lease 教学入口,由独立子进程 A/B 领取同一任务。普通 serve 的恢复策略不变,也不能与该实验模式混跑。源码、原始命令/双流和 PostgreSQL 查询在完整素材包、SHA-256和独立复跑说明中。
RUNNING 需要归属与期限,不能只看状态名
单实例原有 Claim 的 QUEUED → RUNNING 条件更新避免同时领取,却没有记录哪一个进程应完成任务。租约模式在原任务行增加 lease_owner、lease_epoch 与数据库时钟下的 lease_until。领取是一条带条件的 SQL UPDATE RETURNING:只有 QUEUED 或租约已过期的 RUNNING 才能被写成当前 worker 的所有权,并把 epoch 加一。两个连接同时争同一条新任务,十轮 SQL 测试每轮只有一个成功;这是一组已执行输入的结果,不能代替任意时序的正确性证明。
受控的第一种崩溃:lease submit --text 'Go one' 写入 QUEUED,A 使用 2 秒租约领取后以进程退出码 72 直接退出,不写效果,也不提交终态。保存实际任务 ID 的查询仍是 RUNNING/epoch=1/effect_rows=0;B 在租约内领取返回 3、未更改该记录。超过租约后 B 把同一 ID 的 epoch 更新为 2,再执行原统计函数,数据库核账为 SUCCEEDED/words=2/epoch=2/effect_rows=1。查询、进程退出与实际任务 ID 分开留档;进程消失不等于 SQL 已经宣告失败或成功。
先写效果、后写终态,仍有两处提交点
第二种崩溃刻意放在中间:A 计算成功,在独立数据库事务里把 words=2 写入测试用 go_task_effects,该表以 task ID 为主键;随后 A 以 73 退出,任务仍处在 RUNNING。B 租约过期后接手:它在事务中先锁住任务行核对 owner、epoch 和有效期限,再对效果记录执行 ON CONFLICT DO NOTHING。这次返回 new=false,效果表仍仅一行,B 才在自己的有效租约下提交 SUCCEEDED/words=2。第一种崩溃里 B 得到 new=true,两组的最终效果行数都为 1。这是两个独立实验,不以最终状态替代去重证据。
过期 worker 即使未死,也不能再使用旧凭据写数据库效果与终态:真实 SQL 测试在 B 领到 epoch=2 后强行用 A 的 epoch=1 写两处,均得到 errLeaseLost,终态保持 B 的结果。受控外部动作却不受本数据库 epoch 自动约束;如果 A 在租约失效后仍向其他系统发送请求,就可能和 B 同时触发动作。租约限制的是本数据库的写权限,不是停下旧进程的魔法。
结果未知窗口不能写成“恰好一次”
数据库里的效果行与业务终态不是同一事务:在效果提交后、终态提交前崩溃,B 必须查询已有行并收敛状态。连接在 COMMIT 应答前断掉,客户端还可能不知道事务实际是否提交;不能据一次网络报错断言没有效果。测试效果表的唯一键只去重这份数据库记录;若换成外部支付、邮件或 HTTP 调用,外部动作成功但本地效果行尚未写入时崩溃,重试仍可能重复。那时要由外部接口提供可核验的幂等键/查询协议,或承认「结果未知」。本章没有运行任何外部副作用,不作恰好一次声称。普通 serve 仍采用原有单实例恢复语义;E08 是隔离的可观察租约试验,不是生产级多实例部署指南。
练习:①为何 B 不能在 A 进程退出后立即把 RUNNING 改成 SUCCEEDED?答:进程退出只说明 A 不会继续当前进程的计算;需要到期后条件领取、重做计算、在有效 epoch 下提交,不能跳过结果来源。②A 的旧 epoch 若仍然写效果成功,会出现什么?答:可能与 B 并行制造不一致;应在效果事务中检查锁住的任务归属与有效期限,测试必须强制调用旧 epoch 得到拒绝。③数据库效果行数为一能否证明外部支付只发生一次?答:不能,外部系统与本地事务间没有原子提交,尤其在外部动作成功但本地确认丢失的窗口;需要另外的外部幂等协议和失败注入。
独立解包在新数据库和双缓存重新运行两次进程级崩溃、十轮所有权测试、累计 Go/SQL 与 race 以及旧 CLI,另外单独检查实际页面和下载。-race 无报告只限执行路径;掉电、网络分区、外部 API、其他平台、全站构建与发布均 NOT_RUN。本篇补齐的是有界局部实验与可追溯边界,而非「恰好一次任务服务」。
参考资料
- Go 1.27.1
database/sql.DB.QueryRowContext、BeginTx;PostgreSQL 16UPDATE RETURNING与INSERT ON CONFLICT:本地固定 Go 文档、PostgreSQL 16psql \h帮助、工程文件 SHA 和真实 SQL 命令见evidence/E08/。






