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

ACK前停机的零食售货机

订单送达两次,零食只计一次

在 TT Lab 中继续学习

一句话总结

不是不接收重复消息,而是把接收记录与业务放在一起提交,使同一业务 ID 的效果不会被再次应用。

为什么需要它

仓库准备好了 7 份零食并发出了 ACK,但受理端的连接断了。受理端手里留下两种假设:仓库可能什么都没做成,也可能已经做完了,只是应答丢了。把超时设得更长,并不能让这个窗口彻底消失。总有一天连接会断开,进程会重启。把明确的失败与不知道结果的失败当作同一回事,就会导致重复出库或遗漏。

受理端要重发不确定的条目,仓库就必须能认出同一个业务。把 ID 放进内存中的 set,在运行期间可以防止重复,但下一个进程没有这份记忆。如果先把 ID 写进文件、再更新库存,中途终止时库存就会漏掉;如果先写库存、再记录 ID,库存就可能增加两次。接收一侧也需要一个原子边界。

工作原理

inbox 与业务一起保存

仓库的 inbox 中保存 id 和 qty。stock 的单行中有累计数量 total。receive 用 BEGIN IMMEDIATE 打开写事务,查找相同 ID 的记录。如果是第一次见到的 ID,就插入 inbox、增加 total,然后提交。如果 ID 和数量都相同,说明该业务已经应用过,不再增加,直接结束。如果 ID 相同但数量不同,就以 Conflict 拒绝,并保留之前的业务。

这里 receive 的返回值 True 表示本次调用应用了新的效果。False 表示确认这是一个有效的重复,而不是失败。HTTP 接收端在这两种情况下都会返回相同 ID 的 accepted=true ACK。如果因为没有新效果就对重复请求一直回复失败,发布方就会永远重试。重要的是不要把内部函数的“是否新应用了”与通信响应中的“是否接受了这个业务”混为一谈。

처음 snack-7 / 7 → inbox 1행, total=7, 새 적용 True, ACK 성공
다시 snack-7 / 7 → inbox 1행, total=7, 새 적용 False, ACK 성공
다시 snack-7 / 8 → 내용 충돌, 기존 total=7 보존, 성공 ACK 없음
다른 snack-8 / 7 → inbox 2행, total=14, 별개 업무 ACK 성공

这张表说明,不能因为内容相同就把不同的业务合并。即使附加 SHA-256 之类的内容指纹,定义“同一业务”的责任也不会消失。指纹是用来比较同一个键下的内容是否发生变化的手段,并不是决定客户是否下了两次订单的业务规则。在这个小实验中,内容只是一个整数数量,所以直接比较。扩展到结构化内容时,还必须考虑规范化和 schema 版本。

什么时候可以生成 ACK

ACK 在 receive 提交完成之后才生成。发布方不会仅凭 HTTP 状态 200 就认为所请求的业务已得到确认。要检查响应 JSON 的 id 是否与请求的 id 相同,以及 accepted 是否严格是布尔值 True。字符串 "true" 或数字 1 不能当作真值放行。因为不能把其他订单的成功响应当作这个订单的证据。在真实服务中,还必须另外确认对方是否经过认证,以及响应的协议版本。

这次的服务器仅用于教学,只在 127.0.0.1 的临时端口上运行。它没有认证和 TLS,也不是对外公开的 API。使用 HTTP 的原因,是为了观察真实的请求与响应以及连接终止,而不只是看函数调用之间的模拟错误。不要把同一台主机上的两个独立 DB 夸大解释为网络另一端的两个服务。公共网络延迟、服务器之间的时钟误差和设备丢失不在这次测试范围内。

如果相同的 ID 同时到来会怎样

重复检查与写入必须放在同一个事务中的另一个原因是竞争。如果两个请求都读到“还没有”,各自执行业务后才想保存 ID,检查与执行之间就会出现缝隙。SQLite 的 BEGIN IMMEDIATE 用来把写竞争串行化。如果其他写入已在进行,就会等待,也可能出现锁错误。不要把这种情况解读成“这个业务已完成”。

唯一键约束有助于阻止重复行,但无法撤回另外的外部副作用。这个实验中“在与 inbox 插入相同的事务里修改 stock”的性质,不能原封不动地套用到外部支付调用上。外部操作需要对方的幂等性契约、补偿、状态查询等另外的设计。此外,如果不确定接收 ID 要保留多久,过旧的重发就可能被当作新业务处理。

在现场相遇的样子

通知、客户数据对接和实时事件消费者,常常败在“重发很少见”这一假设上。部署之后进程被替换,或代理切断了响应,正常业务也会产生重发。分别观测通信请求数和实际业务应用数,就能区分这两种情况。2 个请求对应 1 个业务,可能是防重复机制起作用的正常结果。而 1 个请求对应 0 个业务,则不能简单地用平均延迟较低这一指标掩盖过去。

在错误日志中记录业务 ID 会有帮助,但无条件把客户姓名、地址和支付信息也一并记录,则会带来其他风险。本实验只使用虚构的 ID 和数量。在生产设计中,只应保留必要的关联信息,并确定保留期限和访问权限。不要用把所有重发内容都转储到日志里的办法来代替幂等性存储。

下一项检查要做什么

紧接着的测验要区分有效重复、内容冲突、独立业务以及 ACK 的含义。下一个模块将学习发布方的完成标记应当放在哪里,以及失败批次的恢复顺序。实验中会分别在 inbox 插入之后、total 更新之后、提交之后终止进程,并用独立连接读取状态。

通过官方文档进一步阅读