PostgreSQL 把分布式事务的提交拆成两个明确步骤:先让每个参与节点执行 PREPARE TRANSACTION,把本地事务状态固化下来,再由协调者决定是 COMMIT PREPARED 还是 ROLLBACK PREPARED。这个机制的核心在于,PREPARE TRANSACTION 不是模拟提交,而是真正把事务提交决策和提交执行分离开。普通事务在 COMMIT 之后,PostgreSQL 会立即释放锁并标记事务完成;而执行 PREPARE TRANSACTION 后,事务依旧持有锁、写入的元组仍然保持未对其他事务完全可见的状态,但事务本身已经不在原先的会话控制之下,它被登记成一个持久化的 prepared 事务。

从实现角度看,PREPARE TRANSACTION 会强制将该事务的所有 WAL 日志刷入磁盘,包括事务提交时本来可以延迟刷脏页的部分。正是因为 prepared 状态被写进了 WAL,数据库崩溃恢复时才有依据把该事务恢复到 prepared 状态并继续保留它的锁。如果 PostgreSQL 没有在 WAL 中记录 prepared 标记,节点一旦宕机,协调者就再也找不到这个事务,分布式事务的一致性就无从谈起。
一、PREPARE TRANSACTION 的事务状态转换
一个本地事务从 BEGIN 开始到 PREPARE TRANSACTION 经历的状态变化,和普通事务在前半段几乎一致。唯一的区别是执行 PREPARE TRANSACTION 时需要传入一个全局事务标识,PostgreSQL 官方称呼为 gid。这个 gid 在单个实例内必须唯一,长度不能超过 200 字节。协调者在后续发起 COMMIT PREPARED 或 ROLLBACK PREPARED 时,就是通过这个 gid 找到对应的 prepared 事务。
BEGIN; UPDATE accounts SET balance = balance - 100.00 WHERE account_no = 'A001'; UPDATE accounts SET balance = balance + 100.00 WHERE account_no = 'B001'; PREPARE TRANSACTION 'transfer_20250320_001';
上述代码执行到 PREPARE TRANSACTION 后,当前会话就不再持有这个事务,客户端也不能继续在这个事务里读写。此时未提交的数据已经被写入 WAL 和堆表,但事务状态并不是 COMMITTED,MVCC 可见性规则仍会阻止其他事务读取该事务的修改。
PREPARE TRANSACTION 成功后,pg_prepared_xacts 视图会立即出现一条记录,包含 gid、发起者、数据库名以及 prepared 时间。这是排查悬挂事务的第一入口。如果协调者随后发出 COMMIT PREPARED 'transfer_20250320_001',PostgreSQL 会把这个事务正式标记为已提交,并释放锁与资源;如果协调者决定回滚,则 ROLLBACK PREPARED 会撤消该事务的所有修改。整个过程中,真正影响一致性的不是最后执行的那一下,而是 prepare 阶段刷盘的完整性。
还需要注意,PREPARE TRANSACTION 不能脱离事务块单独执行,也不能在已经执行 PREPARE TRANSACTION 之后继续使用当前事务。如果在同一事务中先操作了临时表,或者执行了某些会改变会话级状态的语句,PostgreSQL 会拒绝 prepare。因此更推荐把两阶段提交放在独立连接、独立事务中完成,避免本地会话状态污染分布式事务。
二、故障恢复为什么依赖 WAL 保留 prepared 状态
两阶段提交最棘手的情况是参与者在 prepare 成功后宕机。如果 PostgreSQL 只在内存里记录 prepared 事务,重启后所有信息会丢失,协调者以为参与者已经准备好,实际本地事务却消失了。为了避免这种状态漂移,PostgreSQL 在 PREPARE TRANSACTION 时会把 prepared 记录作为事务提交日志的一部分写入 WAL,并且要求 fsync 刷盘完成之后才能向客户端返回成功。这样无论宕机发生在 prepare 之后、commit prepared 之前,还是 commit prepared 执行过程中,恢复流程都能从 WAL 中找到该事务。
数据库崩溃恢复时会扫描 WAL 中的 prepared 记录,把所有处于 prepared 状态但尚未最终提交或回滚的事务重新纳入 pg_prepared_xacts 视图。事务持有的行锁、表锁也会被继续保留,防止其他事务修改相关数据。这个设计保证了 prepared 事务不会因为节点重启而悄悄消失,也不会在重启后自动提交或自动回滚。它把最终决定权继续留给协调者或人工介入。
SELECT gid, prepared, owner, database FROM pg_prepared_xacts ORDER BY prepared;
管理员如果发现某个 prepared 事务长时间挂起,可以根据 gid 判断是否所有参与节点都已准备完成。若业务日志显示该全局事务已经具备提交条件,可以手动执行 COMMIT PREPARED 'gid';如果协调者已经放弃该事务,则应执行 ROLLBACK PREPARED 'gid' 释放资源。不过手动处理只是救火手段,真正的分布式一致性还需要协调者把全局决策持久化到自己的日志中,否则协调者重启后同样可能忘记自己曾经决定过什么。
三、max_prepared_transactions 与资源预留
PostgreSQL 默认关闭两阶段提交,max_prepared_transactions 的默认值是 0。这个参数控制同一时刻可以存在的 prepared 事务数量上限。因为 prepared 事务会持有锁、占用连接后端的部分内存,数据库必须提前预留共享内存和锁表空间。如果设置过小,当并发 prepared 事务超过限制时,新的 PREPARE TRANSACTION 会直接报错;如果设置过大,则会白白消耗内存。
-- 修改前查看当前值 SHOW max_prepared_transactions; -- 将参数写入配置文件 ALTER SYSTEM SET max_prepared_transactions = 100; -- 该参数需要重启 PostgreSQL 实例才能生效
另外,配置这个参数之前要评估业务规模。如果分布式事务完全由应用服务器的 XA 连接池管理,通常并发 prepared 事务数量不会特别大,但如果业务频繁超时、部分参与者无法及时收到最终指令,prepared 事务会堆积。建议结合 statement_timeout、idle_in_transaction_session_timeout 等参数控制异常会话,避免 prepared 事务无限期挂起。
从行为上看,max_prepared_transactions 只影响 PREPARE TRANSACTION 的数量,不影响普通事务。但每一个 prepared 事务仍然会在 pg_prepared_xacts 中可见,并且会占用一个事务槽位。这也是监控 prepared 事务数量和增长趋势的原因。生产环境不要随意启用过大的值,预留一点余量即可。
四、与 XA 及分布式事务中间件的配合
PostgreSQL 的两阶段提交通常不会由业务开发者直接调用 PREPARE TRANSACTION 命令,而是在 Java、Go 等语言的事务管理器中通过 XA 接口完成。PostgreSQL JDBC 驱动提供 PGXADataSource,实现了 javax.transaction.xa.XAResource 接口。事务管理器会向多个数据库发起 start、end、prepare、commit 等 XA 协议操作,本质就是把 PREPARE TRANSACTION 和 COMMIT PREPARED 封装成标准两阶段提交。
PGXADataSource dataSource = new PGXADataSource();
dataSource.setUrl("jdbc:postgresql://127.0.0.1:5432/appdb");
dataSource.setUser("app_user");
dataSource.setPassword("app_password");
XAConnection xaConn = dataSource.getXAConnection();
XAResource xaRes = xaConn.getXAResource();
Xid xid = new CustomXid(100, "gid_order_001".getBytes(), "branch_001".getBytes());
xaRes.start(xid, XAResource.TMNOFLAGS);
Connection conn = xaConn.getConnection();
try {
PreparedStatement ps = conn.prepareStatement("UPDATE inventory SET stock = stock - 1 WHERE sku = ?");
ps.setString(1, "SKU-100");
ps.executeUpdate();
ps.close();
} finally {
xaRes.end(xid, XAResource.TMSUCCESS);
}
int prepareResult = xaRes.prepare(xid);
if (prepareResult == XAResource.XA_OK) {
xaRes.commit(xid, false);
} else {
xaRes.rollback(xid);
}
使用 XA 接口时要注意,一旦某个数据库连接被纳入 XA 事务,就不能再执行本地自动提交的独立操作,也不能随意关闭连接。协调者必须保证在全局事务结束前,所有 XA 连接都保持可用。对于 PostgreSQL 来说,prepare 成功后连接是否断开并不影响事务本身,但如果连接池回收连接时没有正确结束 XA 事务,会导致资源泄漏。
在分布式事务中间件如 Atomikos、Narayana 或 Seata 中,PostgreSQL 的 PREPARE TRANSACTION 能力通常作为 XA 模式的底层支撑。中间件会在自己的事务日志中记录全局事务状态,参与者只负责完成 prepare 和最终的 commit/rollback。这种分层结构下,PostgreSQL 提供的不是分布式一致性算法,而是一个可靠的本地事务状态持久化原语。
最后要认清两阶段提交的边界:它解决的是多个参与者之间的原子提交问题,并不能消除网络分区下的阻塞风险。如果协调者在发送 commit 之前长时间不可用,所有参与者都会卡在 prepared 状态。生产环境需要结合超时、监控和人工处理流程,才能真正做到分布式事务的一致性与可用性平衡。

