Postgres事务是分布式系统的超能力
摘要
本文解释了如何通过与应用程序数据共置的工作流状态使用Postgres事务,来消除分布式工作流中的幂等性和原子性问题,从而实现精确一次执行。
暂无内容
查看缓存全文
缓存时间: 2026/07/02 20:08
# 将工作流状态与数据共置的论据 | DBOS
来源:https://www.dbos.dev/blog/co-locating-workflow-state-with-your-data
几周前,我们撰文指出,对于持久化工作流,你应当“只用 Postgres”(https://www.dbos.dev/blog/postgres-is-all-you-need-for-durable-execution)。
那篇文章引发了大量讨论,但也产生了一个误解。我们并非仅仅指你应该使用一个将状态存储在 Postgres 中的工作流引擎。我们想表达的是,你的工作流系统可以——而且通常应该——与你的应用运行在同一个 Postgres 数据库中。
乍一看,这似乎不是个好主意。难道这些关注点不应该分离吗?难道工作流状态不应该与应用数据分别存放在不同的数据库中吗?
也许未必。
在分布式系统中,共置是一种超能力。当工作流元数据和应用数据共存于同一个 Postgres 数据库时,它们可以在同一个数据库事务中更新。这意味着部分失败不再可能发生,从而使得构建能正确处理所有边界情况的工作流变得容易得多。
在本文中,我们将解释为什么这是可行的,以及事务如何简化幂等性和原子性等棘手问题。
### 利用事务化步骤实现幂等性
分布式系统中的一个基本挑战是**幂等性**,尤其是对于修改数据库状态的操作。
持久化工作流通过在每个步骤完成后对其结果进行检查点来实现容错。如果工作流被中断,它会从最后一个已检查点的步骤恢复,而不是从头开始。但是,工作流可能在一个步骤完成之后、但在记录其检查点之前被中断。当它恢复时,它没有该步骤已运行的记录,因此会再次执行该步骤。
因此,仅靠持久化工作流本身并不能解决幂等性问题。工作流引擎通常要求步骤是**幂等的**,这样它们就可以安全地重试,而不会产生重复的副作用。例如,考虑一个向银行账户贷记(加钱)的步骤。这不是一个幂等操作:如果一个步骤向账户添加 100 美元,然后失败、重新运行并再次添加 100 美元,那么账户总共添加了 200 美元,这是不正确的。
最常见的解决方案是添加应用层面的簿记来防止这种情况。例如,你可以添加一个额外的 `applied_payments` 表来跟踪哪些付款已被应用,通过事务更新它,并检查该表以确保你永远不会重复贷记同一个账户:
应用层面簿记代码示例
当工作流状态和应用数据共置于同一个 Postgres 数据库时,我们可以消除大部分这类复杂性。与其在步骤的数据库事务提交后对其进行检查点,一个共置的工作流引擎可以**在同一个事务中写入步骤检查点并执行数据库更新**。
为此,工作流使用工作流引擎提供的数据库事务来执行该步骤。该步骤执行其数据库更新,工作流引擎记录检查点,然后整个事务原子性地提交:
工作流幂等性代码示例
通过将数据库更新和检查点写入作为同一事务的一部分,工作流引擎可以为事务化步骤提供**恰好一次**执行语义:
- 如果事务提交,数据库更新和检查点都会被持久记录,保证该步骤永远不会再次运行。
- 如果提交前发生任何故障,整个事务都会回滚,包括数据库更新和检查点。当工作流恢复时,它会从头开始安全地重新执行该步骤。
这消除了数据库更新成功但对应检查点未记录的时间窗口。因此,事务化步骤不再需要应用层面的幂等逻辑或簿记表。数据库操作要么恰好执行一次并被检查点,要么根本不执行。
### 利用事务化工作流出站组件的原子性
分布式系统中的另一个经典挑战是可靠地在多个系统中执行更新,例如,更新一条数据库记录并向另一个系统发送通知。这比听起来更棘手,因为操作需要是**原子的**:即使执行过程中出现故障(如进程崩溃或网络问题),它们要么都发生,要么都不发生。
例如,每当客户提交新订单时,我们可能还需要启动一个工作流,将订单发送到仓库进行履行。如果没有原子性,数据库和下游系统可能会变得不一致。订单可能已提交但仓库未收到通知,或者仓库可能收到了关于一个从未提交的订单的通知。
解决这个问题最常用的方案是**事务化出站组件**。其思路是在数据库中维护一个新的“outbox”表。当我们需要执行原子更新时,我们运行一个单一的数据库事务,该事务同时:
- 更新数据库记录
- 向“outbox”表写入一条消息
然后,一个独立的后台进程轮询 outbox 表,并将那里的消息投递到目标系统。
下面是一个示例,展示了可能的样子:
事务化出站组件模式 SQL 示例
在同一个事务中执行数据库记录更新和向“outbox”表写入消息,保证了原子性:要么两条记录都更新,要么都不更新。一旦消息写入 outbox,就可以异步投递,即使事务提交后发生故障也没有问题。
事务化出站组件被广泛使用,但它引入了额外的操作复杂性。你需要基础设施来轮询 outbox、投递消息、处理重试以及监控故障。如果工作流引擎是一个独立的系统,它可能会与数据库不同步。在实践中,解决不一致需要额外的基础设施,例如对账任务来检测那些已更新数据库记录但未向下游系统发送通知的情况。
通过利用数据库支持的工作流,并将工作流状态与应用数据共置,我们可以简化这种模式。我们不需要手动维护一个 outbox 表和一个独立的轮询进程,而是使用一个 Postgres 用户自定义函数(UDF)来在与应用更新相同的数据库事务中入队一个工作流:
DBOS 事务化出站组件模式原子性示例
这与事务化出站组件的原理相同。工作流由包含其名称、队列和输入的一个数据库行表示。`enqueue_workflow` UDF 在与用户数据库更新相同的事务中创建这一行,从而保证了原子性:要么更新完成且工作流入队,要么都不发生。然后,一个工作者异步出队并执行该工作流,可靠地执行所需操作。
### 了解更多
如果你喜欢构建可扩展、可靠的系统,我们很乐意听取你的意见。在 DBOS,我们的目标是让基于 Postgres 的持久化执行尽可能简单和高效。欢迎查阅:
- 快速入门:https://docs.dbos.dev/quickstart
- GitHub:https://github.com/dbos-inc
- Discord 社区:https://discord.gg/eMUHrvbu67
相似文章
使用 Postgres 作为作业队列的潜在后果
文章分析了使用 PostgreSQL 作为作业队列的可扩展性限制,特别强调了高并发下 MultiXact SLRU 争用导致的性能瓶颈。文章解释了为什么这种架构在开发环境中表现良好,但在生产环境中却会失败,并建议考虑替代方案。
禁用 Postgres FPW 实现写入性能 5 倍提升
本文介绍了 Databricks 的 Lakehouse 架构如何通过禁用全页写入(FPW)并利用无状态计算与分布式存储,使 Postgres 的写入吞吐量提升 5 倍。
你只需要PostgreSQL
一份详细指南,介绍如何使用PostgreSQL作为单一数据库来处理金融应用的方方面面,包括模式设计、状态机、触发器和性能优化。
扩展PostgreSQL以支持8亿ChatGPT用户
OpenAI分享了扩展PostgreSQL以支持8亿ChatGPT用户及每秒数百万查询的技术见解,采用了单主架构搭配50个只读副本,同时通过分片和优化策略管理写入密集型工作负载带来的挑战。
pg_durable: 微软开源数据库内持久化执行
微软开源了pg_durable,这是一个PostgreSQL扩展,支持长时间运行的SQL函数的持久化执行,具有自动检查点和容错恢复功能。