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

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

用事务性发件箱保障订单与事件一致性:设计原理

在 TT Lab 中继续学习

一句话总结

把订单和要发布的事件写入同一个数据库事务,而投递造成的重复由消费者来处理。

为什么需要它

订单刚写入数据库,进程就崩溃了,事件没能发布出去,那么其他服务可能永远不知道 订单已经产生。反过来,如果先发送事件,即使订单保存失败,其他服务也可能去处理一个 并不存在的订单。向两个系统写入这件事,无法靠两行函数调用的先后顺序来解决。 在同一个数据库内以原子方式保存订单和 outbox 行,再由独立的发布器读取尚未发送的事件, 这种模式可以缩小这道缝隙。

工作原理

BEGIN → orders INSERT → 장애 지점 → outbox INSERT → COMMIT
                                       ↓
발행기: pending → publish → sent 표시
소비자: consumed 확인 + 효과 반영 → 같은 트랜잭션으로 COMMIT

如果在保存订单和保存事件之间抛出异常,两者都不应该留下。发布失败时不能标记 sent。 但如果发布成功了,却在标记 sent 之前崩溃,下一次运行就会把同一个事件再发一遍。 因此这并不是 exactly-once 的发送。在这种可能至少投递一次的设计中,为了防止效果被重复应用, 消费者也要把事件 id 的记录和合计值的更新放进同一个事务。

在现场相遇的样子

复用幂等键但改动了金额时,不应该返回已有的成功结果,而应该告知冲突。 在这个实验中,订单 id 就是事件 id,每个订单只产生一个创建事件。如果要涵盖一般的 修改、取消事件,还需要单独的事件 id 以及版本、顺序策略。 发布器按只有一个来构建。多个发布器之间的 claim、lease、毒消息(poison message)隔离以及 outbox 清理,是本实验没有实现的运维课题。

下一项实验要做什么

打开数据库文件,确认行数和 payload,并注入中途异常。最后构造这样的场景: 消费者刚处理完,发布器就收到异常,然后重新发送。 投递记录会有两条,但销售额合计只能增加一次。在报告里写上“去重”这样的措辞并不能通过。 判定依据是用另一个连接读取到的实际存储状态。

参考:SQLite 事务