使用 Postgres 事务解决分布式系统挑战
使用 Postgres 事务解决分布式系统挑战
将工作流状态与数据共置
在 Postgres 数据库中将应用程序工作流状态与数据共置,可以让开发者消除“双写”问题——即系统更新了数据库但未能通知消息队列,或者反之亦然的风险。通过将数据库视为业务数据和工作流进度的单一事实来源,开发者可以确保状态转换及其相应的副作用能够原子性地提交。
这种方法将分布式协调问题转化为本地事务问题。系统不再尝试在两个独立的系统(例如数据库和消息代理)之间协调事务,而是在单个 ACID 事务中将数据变更和预期的操作同时写入同一个数据库。
Transactional Outbox 模式
Transactional Outbox 模式是实现数据库与外部消息队列之间原子性的主要机制。它的工作原理是将数据库的角色分为两部分:主状态和“outbox”表。
- 原子写入:应用程序在单个 Postgres 事务中更新业务状态并在 outbox 表中插入一条消息。
- 异步分发:一个独立的进程轮询 outbox 表,或使用数据库触发器/UDF 来将消息推送到外部队列。
- 确认:一旦外部系统确认收到,该消息就会被标记为已处理或从 outbox 中删除。
正如社区成员所指出的,这种模式通过确保发送消息的意图与数据本身一样持久化,有效地“伪装”了跨数据库和队列的事务。
在金融系统中消除双写 Bug
在像资金转移系统这样高风险的环境中,将检查点与写入操作共置至关重要,以防止“半提交”状态。如果系统在工作流中途崩溃,恢复过程可以查看 Postgres 中持久化的工作流状态,以确定进程确切停止的位置,并从该点恢复,而不会导致事务重复或丢失。
"这是在资金转移系统中消除双写 Bug 的秘诀:将检查点与写入操作共置,这样工作流中途的崩溃就不会让你处于半提交状态。"
权衡与实现考虑因素
虽然使用 Postgres 作为工作流引擎提供了强大的保证,但它也引入了特定的架构权衡:
耦合与复杂性
将工作流进度单元与数据库提交单元对齐,会在数据库模式(schema)与应用程序工作流之间产生紧密耦合。虽然这简化了 outbox 模式,但它使得未来很难将工作流引擎与数据库分离。然而,对于许多服务来说,数据库是技术栈中最稳定的部分,因此这种权衡是可以接受的的。
幂等性与副作用
数据库事务无法回滚已经在物理世界中发生的副作用(例如发送电子邮件)。因此,对于任何外部服务交互,幂等性是强制性的。
例如,在电子邮件通知系统中,一种常见的策略是优先考虑“至少一次”交付,而非“精确一次”交付。系统在关闭移除消息从待处理列表中的事务之前,会先确认电子邮件已成功发送,并接受极少数的失败可能导致重复发送电子邮件,而不是丢失一封邮件。
中心化风险
使用单个数据库同时进行状态管理和协调,会产生单点故障。虽然这简化了运维开销——因为数据库宕机将导致整个系统一致性地切断——但它消除了独立于数据层扩展协调层的能力。
与分布式替代方案的比较
有人认为,这种方法从严格意义上讲并不是“分布式系统”,因为它依赖于一个中心化数据库。然而,它作为 Temporal 或复制状态机等复杂分布式协调工具的实用替代方案。对于步骤量可控且需要工作流进度永久记录的系统,直接在 Postgres 中实现持久化工作流通常比引入 Redis 或 Valkey 等额外基础设施更高效。