Skip to content

PREPARE TRANSACTION / COMMIT PREPARED ​

上一页讲了 2PC 的协议原理,本页进入实操:PREPARE TRANSACTION、COMMIT PREPARED、ROLLBACK PREPARED 三条命令的完整语法、gid 的规则、prepared 事务的全部限制清单、报错信息与含义、悬挂事务的排查与清理流程,并以「双库转账」做一次完整的端到端演示。

前置知识

阅读本节前,建议先了解:两阶段提交原理、BEGIN / COMMIT / ROLLBACK

基础概念 ​

三条命令在事务生命周期中的位置 ​

text
普通事务:
  BEGIN ──→ SQL ──→ COMMIT / ROLLBACK          (一条路径走到头)

两阶段提交:
  BEGIN ──→ SQL ──→ PREPARE TRANSACTION 'gid'  (阶段一:冻结)
                        │
                        ├─→ COMMIT PREPARED 'gid'     (阶段二:提交)
                        └─→ ROLLBACK PREPARED 'gid'   (阶段二:回滚)

PREPARE TRANSACTION 是唯一能让事务脱离会话而存活的语句。执行之后,事务的所有效果都已经持久化、所有锁都还持有,就等最后那道指令。

gid:全局事务标识符 ​

gid 是全局事务标识符(Global Transaction Identifier),它是 prepared 事务的唯一句柄,等于一把「钥匙」。

text
规则(官方):
  1. 必须是字符串字面量(string literal)
  2. 长度必须小于 200 字节
  3. 不能与现有任何未结束的 prepared 事务重名
  4. 由外部事务管理器负责保证全局唯一

推荐命名结构:

text
{业务前缀}-{日期}-{序列}-{参与者标识}

例:
  xfer-2026-09-28-0001-A     转账业务,2026-09-28,第 1 笔,参与者 A
  xfer-2026-09-28-0001-B     同一笔转账的参与者 B
  jta-8f31a2c4-a1             外部事务管理器分配的事务 ID

好的 gid 有三个作用:可读(运维看 pg_prepared_xacts 就知道是什么业务)、可分组(同一笔转账通过前缀关联)、可追溯(日期与序列能定位到日志)。

谁执行哪条命令 ​

动作谁可以执行
PREPARE TRANSACTION任何能在该库开启事务并拥有相应对象权限的用户
COMMIT PREPARED原执行 prepare 的用户,或超级用户
ROLLBACK PREPARED同上
执行位置必须在 prepare 时所在的那个数据库
执行会话不必是原会话(这正是 2PC 的意义所在)
text
会话 A(库 shop):
  BEGIN; ...; PREPARE TRANSACTION 'g1';
会话 B(库 shop,另一个连接):
  COMMIT PREPARED 'g1';   ✓ 允许

这条「不必同会话」的规则是协调者能在崩溃后收尾的基础——它只要重连即可,不需要还原原来的连接。

与普通事务结束对照:

维度COMMITCOMMIT PREPARED
作用对象当前会话的活动事务一个 prepared 事务(按 gid 指定)
能否在事务块内能(本身就是结束语句)不能(它是独立命令)
所属会话必须是发起事务的会话任意会话
权限发起者自己原用户或超级用户
数据库当前库必须是 prepare 时的那个库
报错后的状态事务可能 abortedprepared 条目仍在,可重试

参数前提:max_prepared_transactions ​

sql
SHOW max_prepared_transactions;
text
 max_prepared_transactions
---------------------------
 0
(1 row)

默认 0,表示禁用该特性。任何 2PC 命令都会失败:

text
ERROR:  prepared transactions are disabled
HINT:  Set max_prepared_transactions to a nonzero value.

该参数只能在服务器启动时设置(postgresql.conf + 重启,或 ALTER SYSTEM SET + 重启):

ini
# postgresql.conf
max_prepared_transactions = 100
bash
# 方式一:直接改配置文件后重启
pg_ctl restart -D $PGDATA

# 方式二:ALTER SYSTEM 后重启
psql -c "ALTER SYSTEM SET max_prepared_transactions = 100;"
pg_ctl restart -D $PGDATA

standby 必须 >= primary

官方要求:standby 服务器上的 max_prepared_transactions 必须大于等于 primary 上的值,否则 standby 将不允许执行查询。这是复制环境中一个容易遗漏的配置项。

语法与用法 ​

完整语法 ​

text
PREPARE TRANSACTION transaction_id

COMMIT PREPARED transaction_id

ROLLBACK PREPARED transaction_id

其中 transaction_id 是字符串字面量(String Literal):

sql
PREPARE TRANSACTION 'gid-0001';        -- 正确:单引号字面量

常见错误:写成标识符(不带引号)会直接语法报错。

sql
PREPARE TRANSACTION gid_0001;
text
ERROR:  syntax error at or near "gid_0001"
LINE 1: PREPARE TRANSACTION gid_0001;
                             ^

在 PL/pgSQL 里也不能把变量直接塞进去,因为 PREPARE TRANSACTION 不接受参数,必须用 EXECUTE 动态拼成字面量:

sql
DO $$
DECLARE
    g text := 'gid-' || to_char(now(), 'YYYYMMDDHH24MISS');
BEGIN
    -- 注意:EXECUTE 在事务块内执行,PREPARE TRANSACTION 会终结该事务
    EXECUTE format('PREPARE TRANSACTION %L', g);
END $$;

最小示例 ​

sql
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
PREPARE TRANSACTION 'demo-cmd-0001';
text
BEGIN
UPDATE 1
PREPARE TRANSACTION
sql
-- 查看
SELECT gid, owner, database, prepared FROM pg_prepared_xacts;
text
       gid        | owner | database |           prepared
------------------+-------+----------+-------------------------------
 demo-cmd-0001    | app   | shop     | 2026-09-28 15:02:11.447+08
(1 row)
sql
-- 提交(改为 ROLLBACK PREPARED 'demo-cmd-0001' 即回滚)
COMMIT PREPARED 'demo-cmd-0001';
text
COMMIT PREPARED

报错信息速查表 ​

报错含义处理
prepared transactions are disabledmax_prepared_transactions = 0改参数并重启
transaction identifier 'x' is already in usegid 重复换一个唯一的 gid
prepared transaction with identifier 'x' is busy该 gid 正被其他会话处理稍后重试
prepared transaction belongs to another database连到了错误的库回到 prepare 时的库
permission denied to finish prepared transaction非原用户且非超级用户用原用户或超级用户
cannot PREPARE a transaction that has operated on temporary objects用过临时对象去掉临时表/临时命名空间
cannot PREPARE a read-only transaction只读事务只读参与者无需 2PC
cannot PREPARE a transaction that has executed LISTEN, UNLISTEN, or NOTIFY用过通知语句移出 2PC 范围

详细说明 ​

限制清单:五类不能 prepared 的事务 ​

官方对 PREPARE TRANSACTION 的禁区列得很清楚,触发时都会直接报错:

text
不可 prepare 的清单(官方):
  1. 只读事务(READ ONLY)
  2. 操作过临时表(temporary tables)或临时命名空间(pg_temp_n)
  3. 声明了 WITH HOLD 的游标(cursors declared WITH HOLD)
  4. 执行过 LISTEN / UNLISTEN / NOTIFY
  5. 无实际写入的事务(PREPARE 要求事务真正改动了数据)

限制一:只读事务。

sql
BEGIN READ ONLY;
SELECT count(*) FROM accounts;
PREPARE TRANSACTION 'ro-demo';
text
ERROR:  cannot PREPARE a read-only transaction

设计层面很好理解:2PC 的意义是「保证多个写操作原子生效」,只读事务不改变任何状态,让它持有 prepared 状态只会白占资源。协调者应只对写入方发 prepare。

限制二:临时对象。

sql
CREATE TEMP TABLE tmp_calc(x int);
BEGIN;
INSERT INTO tmp_calc VALUES (1);
PREPARE TRANSACTION 'temp-demo';
text
ERROR:  cannot PREPARE a transaction that has operated on temporary objects

原因:临时表、临时命名空间与会话强绑定;prepared 事务脱离会话存在,若允许 prepare,一个没有会话的临时表既无人能访问也无人能清理。

sql
ROLLBACK;
DROP TABLE IF EXISTS tmp_calc;

限制三:WITH HOLD 游标。 它在事务提交后仍然存活,同样是与事务生命周期矛盾的会话状态,因此被禁止:

sql
BEGIN;
DECLARE c_hold CURSOR WITH HOLD FOR SELECT * FROM accounts;
PREPARE TRANSACTION 'cursor-demo';
text
ERROR:  cannot PREPARE a transaction that has operated on temporary objects

限制四:LISTEN / UNLISTEN / NOTIFY。

sql
BEGIN;
LISTEN channel_a;
PREPARE TRANSACTION 'listen-demo';
text
ERROR:  cannot PREPARE a transaction that has executed LISTEN, UNLISTEN, or NOTIFY

LISTEN 注册的订阅、NOTIFY 排队的通知都是会话级状态,prepared 事务没有会话可以承接。

sql
ROLLBACK;

报错信息有重叠

限制二、三触发时,报错文案都指向 temporary objects(WITH HOLD 游标在内部也关联到会话级资源)。排障时不要只看文案字面,要看事务里实际做了什么。

限制五:PREPARE 之后不能再「接着写」。 PREPARE TRANSACTION 必须在事务块内执行,但它终结了该事务块:

sql
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
PREPARE TRANSACTION 'after-demo-0001';
-- 下面这条已经是全新的隐式事务,与 prepared 事务无关!
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
text
BEGIN
UPDATE 1
PREPARE TRANSACTION
UPDATE 1

如果业务代码以为「它们是一体的」,就会产生逻辑错乱。正确姿势是让 PREPARE TRANSACTION 成为事务块的最后一条语句:

sql
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
PREPARE TRANSACTION 'after-demo-0002';   -- 到此为止

SET 的持久化行为 ​

sql
BEGIN;
SET LOCAL lock_timeout = '5s';       -- LOCAL:随事务消失,无副作用
SET search_path = app_schema;        -- 非 LOCAL:会残留!
UPDATE accounts SET balance = balance - 1 WHERE id = 1;
PREPARE TRANSACTION 'set-demo';
sql
-- 事务已 prepared,但 search_path 还改着
SHOW search_path;
text
  search_path
----------------
 app_schema
(1 row)

官方明确:使用 SET(非 LOCAL)修改的运行时会话参数会持久到 PREPARE TRANSACTION 之后。

规范:在 2PC 路径上,所有 GUC 修改一律用 SET LOCAL。实在需要非 LOCAL 的,在 prepare 前手工 RESET:

sql
RESET search_path;

gid 重复与并发 ​

sql
BEGIN;
UPDATE accounts SET balance = balance - 1 WHERE id = 1;
PREPARE TRANSACTION 'dup-demo';
text
PREPARE TRANSACTION
sql
BEGIN;
UPDATE accounts SET balance = balance - 1 WHERE id = 1;
PREPARE TRANSACTION 'dup-demo';
text
ERROR:  transaction identifier "dup-demo" is already in use

这是保护机制而非缺陷——如果允许重名,协调者后来的 COMMIT PREPARED 'dup-demo' 就不知道该提交哪个了。

若某个 gid 正被处理(例如另一个会话正在执行 COMMIT PREPARED):

text
ERROR:  prepared transaction with identifier "dup-demo" is busy

这表示存在并发访问,重试即可。

跨库与权限限制 ​

COMMIT PREPARED 只能在prepare 时所在的那个数据库里执行:

bash
psql -d bank_a -c "BEGIN; UPDATE accounts SET balance=balance-1 WHERE id=1; PREPARE TRANSACTION 'x-db';"
psql -d bank_b -c "COMMIT PREPARED 'x-db';"
text
ERROR:  prepared transaction belongs to another database
HINT:  Connect to the database where the transaction was prepared to finish it.

这个错误来自 LockGXact()(src/backend/access/transam/twophase.c)。运维含义:清理悬挂事务无法「一个连接搞定所有库」,必须逐库连接、逐库检查:

bash
# 错误的做法:以为在一个库就能看到全部
psql -d bank_a -c "SELECT gid FROM pg_prepared_xacts;"   # 只看到 bank_a 的

# 正确的做法:逐库查询
for db in bank_a bank_b shop; do
  echo "--- $db"
  psql -d "$db" -At -c "SELECT gid, prepared FROM pg_prepared_xacts;"
done

权限方面,只有原执行 prepare 的用户或超级用户能收尾:

sql
-- 以普通用户 bob 尝试收尾 alice 创建的 prepared 事务
COMMIT PREPARED 'alice-gid-1';
text
ERROR:  permission denied to finish prepared transaction
HINT:  Must be superuser or the user that prepared the transaction.
sql
\c - postgres          -- 切换到超级用户后即可
COMMIT PREPARED 'alice-gid-1';
text
COMMIT PREPARED

实践建议:协调者应使用固定的专用账户(如 xa_coordinator)在所有参与库上执行 2PC,这样权限与归属都一致,排障时不用在多个用户之间切换。

悬挂事务的完整清理流程 ​

第一步:发现。

sql
SELECT
    gid,
    owner,
    database,
    prepared,
    now() - prepared AS age
FROM pg_prepared_xacts
ORDER BY prepared;
text
        gid        | owner | database |           prepared            |      age
-------------------+-------+----------+-------------------------------+--------------
 xfer-8f31a2-A     | xa    | bank_a   | 2026-09-28 09:12:03.771+08    | 06:02:11.882
(1 row)

第二步:评估它持有哪些锁。

sql
SELECT
    l.locktype,
    l.relation::regclass AS relation,
    l.mode,
    l.virtualtransaction
FROM pg_locks l
WHERE l.virtualtransaction = '-1/' || (
    SELECT transaction FROM pg_prepared_xacts WHERE gid = 'xfer-8f31a2-A'
);
text
 locktype |  relation  |       mode       | virtualtransaction
----------+------------+------------------+--------------------
 relation | accounts   | RowExclusiveLock | -1/759160
 transactionid|          | ExclusiveLock    | -1/759160
(2 rows)

第三步:确认全局决策。 这一步无法用 SQL 完成,必须依赖外部信息:

text
检查清单:
  □ 协调者日志里这笔全局事务的最终决定是什么?
  □ 其他参与者的状态如何(已提交 / 已回滚 / 也悬挂)?
  □ 是否有业务侧的对账数据可以佐证?

第四步:执行清理。

sql
-- 情况 A:确认全局应提交
COMMIT PREPARED 'xfer-8f31a2-A';
sql
-- 情况 B:确认全局应回滚,或信息不足(偏保守)
ROLLBACK PREPARED 'xfer-8f31a2-A';

第五步:验证。

sql
SELECT count(*) AS remaining FROM pg_prepared_xacts;
text
 remaining
-----------
         0
(1 row)

监控与告警 SQL ​

例行巡检需要三个层次的信号:总量、超期条目、影响面。

sql
-- 1. 总览:数量与最老年龄
SELECT
    count(*)                                       AS prepared_count,
    coalesce(max(now() - prepared), '0'::interval)  AS oldest_age
FROM pg_prepared_xacts;
text
 prepared_count |      oldest_age
----------------+----------------------
              2 | 00:12:44.331219
(1 row)
sql
-- 2. 超阈值告警
SELECT gid, owner, database, prepared, now() - prepared AS age
FROM pg_prepared_xacts
WHERE now() - prepared > interval '5 minutes'
ORDER BY prepared;
sql
-- 3. 每个 prepared 事务持有的锁数量(评估阻塞影响面)
SELECT ppx.gid, ppx.owner, ppx.database,
       count(l.locktype) AS held_locks,
       now() - ppx.prepared AS age
FROM pg_prepared_xacts ppx
LEFT JOIN pg_locks l ON l.virtualtransaction = '-1/' || ppx.transaction
GROUP BY ppx.gid, ppx.owner, ppx.database, ppx.prepared
ORDER BY held_locks DESC, age DESC;
text
      gid       | owner | database | held_locks |     age
----------------+-------+----------+------------+--------------
 xfer-8f31a2-A  | xa    | bank_a   |         14 | 00:12:44.331
(1 row)

防止悬挂:看门狗只告警,不自动回滚 ​

PostgreSQL 本身不提供 prepared 事务超时。可以在协调者侧或运维侧实现「看门狗」:

sql
-- 看门狗逻辑示意(谨慎使用!回滚前必须先确认全局状态)
DO $$
DECLARE
    r record;
BEGIN
    FOR r IN
        SELECT gid FROM pg_prepared_xacts
        WHERE now() - prepared > interval '30 minutes'
    LOOP
        RAISE WARNING 'found stale prepared xact: %', r.gid;
        -- 生产环境不要自动 ROLLBACK,应先告警,人工确认
        -- EXECUTE format('ROLLBACK PREPARED %L', r.gid);
    END LOOP;
END
$$;

不要盲目自动回滚

自动回滚看似省事,但如果其他参与者已经提交,回滚这一个库会造成数据不一致——原本 2PC 要防的正是这个。看门狗应当只告警不擅自动手,或者只在有充分的外部决策依据时才自动执行。

实战示例 ​

端到端:双库转账完整演示 ​

准备。

sql
-- bank_a 与 bank_b 各建表
CREATE TABLE IF NOT EXISTS accounts (
    id      integer PRIMARY KEY,
    name    text NOT NULL,
    balance numeric(14,2) NOT NULL CHECK (balance >= 0)
);
sql
-- bank_a 初始化
INSERT INTO accounts(id, name, balance) VALUES
    (1, 'alice', 1000.00), (2, 'bob', 500.00)
ON CONFLICT (id) DO UPDATE SET balance = EXCLUDED.balance;
sql
-- bank_b 初始化(初始余额与 bank_a 不同)
INSERT INTO accounts(id, name, balance) VALUES
    (1, 'alice', 200.00), (2, 'bob', 300.00)
ON CONFLICT (id) DO UPDATE SET balance = EXCLUDED.balance;
text
INSERT 0 2       ← 两个库分别返回一次

阶段一:两库分别 prepare。

sql
-- 库 bank_a:从 alice 扣 100
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
PREPARE TRANSACTION 'xfer-2026-09-28-0002-A';
text
BEGIN
UPDATE 1
PREPARE TRANSACTION
sql
-- 库 bank_b:给 alice 加 100
BEGIN;
UPDATE accounts SET balance = balance + 100 WHERE id = 1;
PREPARE TRANSACTION 'xfer-2026-09-28-0002-B';
text
BEGIN
UPDATE 1
PREPARE TRANSACTION

检查两边都就绪。

sql
-- bank_a
SELECT gid, owner, database, prepared FROM pg_prepared_xacts;
text
           gid            | owner | database |           prepared
--------------------------+-------+----------+-------------------------------
 xfer-2026-09-28-0002-A   | xa    | bank_a   | 2026-09-28 15:20:01.114+08
(1 row)
sql
-- bank_b
SELECT gid, owner, database, prepared FROM pg_prepared_xacts;
text
           gid            | owner | database |           prepared
--------------------------+-------+----------+-------------------------------
 xfer-2026-09-28-0002-B   | xa    | bank_b   | 2026-09-28 15:20:01.773+08
(1 row)

阶段二:两边提交,然后验证。

sql
-- bank_a
COMMIT PREPARED 'xfer-2026-09-28-0002-A';
text
COMMIT PREPARED
sql
-- bank_b
COMMIT PREPARED 'xfer-2026-09-28-0002-B';
text
COMMIT PREPARED
sql
-- 验证:bank_a 扣了 100,bank_b 加了 100
-- 在 bank_a
SELECT id, name, balance FROM accounts WHERE id = 1;   -- 900.00
-- 在 bank_b
SELECT id, name, balance FROM accounts WHERE id = 1;   -- 300.00

演示:prepare 成功后回滚,且改动不可见 ​

sql
-- 阶段一
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
PREPARE TRANSACTION 'xfer-cancel-0001-A';
text
PREPARE TRANSACTION

此时在另一个会话里查询,看到的仍是旧值——prepared 事务的改动对其他人不可见,直到 COMMIT PREPARED:

sql
SELECT balance FROM accounts WHERE id = 1;
text
 balance
---------
  900.00
(1 row)

阶段二决定回滚,数据完全未变:

sql
ROLLBACK PREPARED 'xfer-cancel-0001-A';
text
ROLLBACK PREPARED
sql
SELECT balance FROM accounts WHERE id = 1;   -- 仍是 900.00

演示:不可见,但仍持锁 ​

「不可见」的另一面是锁真实持有——同一行不能被别人改:

sql
-- 会话 A
BEGIN;
UPDATE accounts SET balance = balance - 50 WHERE id = 2;   -- 500 → 450
PREPARE TRANSACTION 'invisible-demo';
sql
-- 会话 B(独立连接):读到旧值 500,看不到 450
SELECT balance FROM accounts WHERE id = 2;
text
 balance
---------
  500.00
(1 row)
sql
-- 会话 B 想改同一行 → 被 prepared 事务的锁阻塞
BEGIN;
SET LOCAL lock_timeout = '2s';
UPDATE accounts SET balance = balance + 1 WHERE id = 2;
text
ERROR:  canceling statement due to lock timeout

结论:prepared 事务「不可见但仍持锁」,这是它既安全(不泄漏中间状态)又危险(阻塞他人)的双重性。

sql
ROLLBACK PREPARED 'invisible-demo';    -- 收尾

演示:gid 长度限制与并发收尾 ​

gid 官方要求 less than 200 bytes,超长直接报错:

sql
BEGIN;
UPDATE accounts SET balance = balance - 1 WHERE id = 1;
PREPARE TRANSACTION 'xxx...(此处省略,实际输入 250 个 x)...';
text
ERROR:  transaction identifier too long
sql
ROLLBACK;

实践中建议把 gid 控制在 64 字节以内,便于日志与监控展示。

并发收尾同一 gid 时,只有一个会话能成功,另一个会得到 busy:

sql
-- 会话 B 与 会话 C 同时执行
COMMIT PREPARED 'busy-demo';
text
-- 其中一个成功:
COMMIT PREPARED

-- 另一个可能得到:
ERROR:  prepared transaction with identifier "busy-demo" is busy

这不是致命问题——说明有并发收尾。协调者应在收到 busy 后重试(通常重试时会发现条目已消失,即视为成功)。

排查脚本:一键汇总所有库的悬挂事务 ​

bash
#!/usr/bin/env bash
set -uo pipefail

THRESHOLD="${1:-5 minutes}"

for db in $(psql -At -c \
  "SELECT datname FROM pg_database
    WHERE datallowconn AND NOT datistemplate ORDER BY datname"); do
  cnt=$(psql -d "$db" -At -c "SELECT count(*) FROM pg_prepared_xacts;")
  [ "$cnt" = "0" ] && continue

  echo "=== database: $db (prepared: $cnt) ==="
  psql -d "$db" -P pager=off -c "
    SELECT gid,
           owner,
           prepared,
           now() - prepared AS age,
           (SELECT count(*) FROM pg_locks l
             WHERE l.virtualtransaction = '-1/' || ppx.transaction) AS held_locks
    FROM pg_prepared_xacts ppx
    WHERE now() - prepared > interval '$THRESHOLD'
    ORDER BY prepared;"
done
text
=== database: bank_a (prepared: 1) ===
        gid        | owner |           prepared            |     age      | held_locks
-------------------+-------+-------------------------------+--------------+------------
 xfer-8f31a2-A     | xa    | 2026-09-28 09:12:03.771+08    | 06:22:11.882 |         14
(1 row)

注意事项 ​

  • 错误!在未修改参数的情况下使用 2PC。 默认 max_prepared_transactions = 0,会直接报 prepared transactions are disabled。
  • 错误!把 PREPARE TRANSACTION 写在事务块的中间。 它是终结语句,其后的语句属于新事务,会导致逻辑错乱。
  • 错误!在只读事务上 prepare。 报 cannot PREPARE a read-only transaction;只读参与者无需参与 2PC。
  • 错误!在含有临时表、WITH HOLD 游标、LISTEN/NOTIFY 的事务上 prepare。 这些会被明确拒绝。
  • 错误!在错误的数据库执行 COMMIT PREPARED。 报 prepared transaction belongs to another database,必须回到 prepare 时的库。
  • 错误!用非原用户、非超级用户收尾他人的 prepared 事务。 报 permission denied to finish prepared transaction。
  • 不推荐用非 LOCAL 的 SET 修改 GUC 后 prepare。设置会残留在会话上,污染后续语句。
  • 错误!让 prepared 事务长期悬挂。 会阻碍 VACUUM、持续持锁,极端情况可能因事务 ID 回绕导致数据库关闭。
  • 不推荐写自动回滚悬挂事务的脚本。其他参与者可能已提交,盲目回滚会造成不一致。
  • 错误!gid 超过 200 字节。 报 transaction identifier too long,且必须是字符串字面量。

最佳实践 ​

  • gid 规范命名:{业务}-{日期}-{序列}-{参与者},并保留在协调者日志中,便于关联排查。
  • 专用账户执行 2PC:所有参与库使用同一协调账户,避免权限与归属混乱。
  • prepared 窗口极短:prepare 与最终裁决之间不插入任何网络调用或耗时操作。
  • 只用 SET LOCAL:2PC 路径上的 GUC 修改一律用 SET LOCAL,杜绝会话污染。
  • 逐库监控:因为 prepared 事务仅在本库可见,监控必须遍历所有数据库。
  • 告警优先于自动化:对 age 超阈值的 prepared 事务告警,人工评估后再 COMMIT/ROLLBACK PREPARED。
  • held_locks 一并监控:用它衡量悬挂事务的影响面,held_locks 大意味着阻塞面广。
  • 复制环境核对参数:确认 standby 的 max_prepared_transactions >= primary,否则 standby 拒绝查询。
  • 优先考虑替代方案:能用 Outbox / Saga 实现的最终一致,就不要引入 2PC 的运维负担。

下一节

继续学习:分布式事务场景——XA/JTA 集成、Saga、TCC、Outbox 本地消息表与 CDC 的选型决策。

参考链接 ​