Skip to content

事务的 ACID 特性 ​

ACID 是事务(transaction)的四个基本保证:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。PostgreSQL 并不是靠一句「ACID 数据库」的标签来实现它们,而是用 WAL(预写式日志)、MVCC(多版本并发控制)、约束系统、锁管理与刷盘策略共同兑现。本节把每条特性拆开,讲清它在 PostgreSQL 18 里对应的具体机制、关键参数(fsync、synchronous_commit)以及在崩溃恢复时的行为。

前置知识

阅读本节前,建议先了解:BEGIN / COMMIT / ROLLBACK、SAVEPOINT 保存点、WAL 概述

基础概念 ​

四条特性针对的是不同的问题,缺一不可:

特性回答的问题破坏后果PostgreSQL 的实现机制
原子性 Atomicity事务内的语句要么全做,要么全不做?转账扣了款却没入账事务状态 + WAL 回滚 + MVCC 可见性
一致性 Consistency事务前后数据都满足业务规则?余额为负、外键悬空约束(CHECK/UNIQUE/FK)、触发器、级联
隔离性 Isolation并发事务互不干扰?读到别人未提交的数据MVCC 快照 + 行锁/表锁 + 隔离级别
持久性 Durability提交后的数据不会丢?断电后已提交订单消失WAL 顺序写 + fsync + 检查点

一个最直观的对照——「事务能保证什么」与「事务不能保证什么」:

能保证不能保证
事务内多语句的原子性应用层逻辑本身的正确性
提交后的数据在崩溃后依然存在(默认配置下)磁盘物理损坏、RAID 卡缓存丢数据
已提交数据对并发事务的可见性规则网络分区下的跨节点一致性
约束在事务提交前强制生效两个事务加起来违反业务规则(写偏斜,见后续章节)

各特性的验证方式:

text
原子性 → 中途报错后 ROLLBACK,检查数据是否回到原状
一致性 → 插入违反 CHECK / UNIQUE / FOREIGN KEY 的数据,看是否被拒绝
隔离性 → 两个会话并发读写同一行,观察各自看到的值
持久性 → 提交后立刻 kill -9 数据库进程,重启后检查数据是否存在

语法与用法 ​

ACID 没有专属语法,它由下面这组事务控制语句与配置参数共同兑现:

text
-- 事务控制(ACID 的原子性、隔离性入口)
BEGIN [ WORK | TRANSACTION ] [ ISOLATION LEVEL level ] [ READ ONLY | READ WRITE ];
COMMIT [ WORK | TRANSACTION ];
ROLLBACK [ WORK | TRANSACTION ];
SAVEPOINT savepoint_name;
ROLLBACK TO SAVEPOINT savepoint_name;
RELEASE SAVEPOINT savepoint_name;

-- 持久性相关的服务器与事务级开关
SET synchronous_commit TO { on | off | local | remote_write | remote_apply };

-- 两阶段提交(ACID 跨库延伸)
PREPARE TRANSACTION 'gid';
COMMIT PREPARED 'gid';
ROLLBACK PREPARED 'gid';

最简可运行示例——在显式事务中做一次完整转账,并顺带查看当前隔离级别:

sql
BEGIN;

UPDATE accounts SET balance = balance - 100.00 WHERE owner = 'Alice';
UPDATE accounts SET balance = balance + 100.00 WHERE owner = 'Bob';

COMMIT;
text
BEGIN
UPDATE 1
UPDATE 1
COMMIT
sql
SHOW transaction_isolation;
text
 transaction_isolation
-----------------------
 read committed
(1 row)

详细说明 ​

原子性(Atomicity):全有或全无 ​

原子性由「事务状态机 + WAL 撤销信息」共同保证。事务执行期间,PostgreSQL 在堆表(heap)中写入新版本元组,同时把撤销所需的信息(旧版本、事务 ID、命令 ID)保留在元组头部。若事务最终 ROLLBACK,这些元组被标记为中止(aborted),任何快照都不会再看到它们;VACUUM 之后空间被回收。

sql
BEGIN;

UPDATE accounts SET balance = balance - 100.00 WHERE owner = 'Alice';
-- 假设下一步失败
UPDATE accounts SET balance = balance - 100.00 WHERE owner = 'Nobody';  -- UPDATE 0
INSERT INTO accounts (owner, balance) VALUES ('Alice', 0);               -- 唯一约束冲突

ROLLBACK;   -- 扣款动作一并撤销
text
BEGIN
UPDATE 1
UPDATE 0
ERROR:  duplicate key value violates unique constraint "accounts_owner_key"
DETAIL:  Key (owner)=(Alice) already exists.
ROLLBACK

关键是回滚不需要「反向补偿 SQL」:PostgreSQL 不会去执行一条 +100 的 UPDATE,而是直接让数据库回到事务开始前的可见状态。这也是原子性比手写补偿更可靠的根本原因。

sql
-- 验证:回滚后 Alice 的余额没有变化
SELECT owner, balance FROM accounts WHERE owner = 'Alice';
text
 owner | balance
-------+---------
 Alice |  1000.00
(1 row)

一致性(Consistency):约束、触发器与外键级联 ​

一致性指数据库从一个合法状态迁移到另一个合法状态。PostgreSQL 把它拆成两层:数据库层强制的结构一致性(约束、类型、外键)和应用层负责的业务一致性(由你在事务中编排语句)。

数据库层的一致性工具:

sql
CREATE TABLE accounts (
  id      bigint PRIMARY KEY,
  owner   text    NOT NULL UNIQUE,
  balance numeric(12,2) NOT NULL CHECK (balance >= 0)
);

CREATE TABLE transfers (
  id         bigserial PRIMARY KEY,
  from_id    bigint NOT NULL REFERENCES accounts(id) ON DELETE RESTRICT,
  to_id      bigint NOT NULL REFERENCES accounts(id) ON DELETE RESTRICT,
  amount     numeric(12,2) NOT NULL CHECK (amount > 0),
  created_at timestamptz NOT NULL DEFAULT now()
);
text
CREATE TABLE
CREATE TABLE
sql
-- 一致性被强制:余额为负直接被拒绝
INSERT INTO accounts (id, owner, balance) VALUES (10, 'Eve', -1.00);
text
ERROR:  new row for relation "accounts" violates check constraint "accounts_balance_check"
DETAIL:  Failing row contains (10, Eve, -1.00).
sql
-- 一致性被强制:外键悬空直接被拒绝
INSERT INTO transfers (from_id, to_id, amount) VALUES (999, 10, 100.00);
text
ERROR:  insert or update on table "transfers" violates foreign key constraint "transfers_from_id_fkey"
DETAIL:  Key (from_id)=(999) is not present in table "accounts".

约束默认在语句结束时立即检查,也可以声明为 DEFERRABLE INITIALLY DEFERRED 推迟到 COMMIT 时检查——这对「先插子表再插父表」或互相引用的数据很有用:

sql
ALTER TABLE transfers
  ADD CONSTRAINT transfers_check_amount CHECK (amount <= 1000000) NOT VALID;
-- NOT VALID:只约束新数据,不扫描历史数据(后续可用 VALIDATE CONSTRAINT 校验)
text
ALTER TABLE

注意:约束只能保证「单点规则」,不能保证「跨行聚合规则」。例如「两人转账后总余额不变」这类规则,SQL 的 CHECK 无法表达(它不能读其它行),只能靠正确的 SQL 语句(同时更新双方)或触发器来维护。

隔离性(Isolation):MVCC 加锁 ​

隔离性解决的是并发事务互相干扰的问题。PostgreSQL 采用 MVCC:读操作不阻塞写、写操作不阻塞读,每个查询根据隔离级别拿到一个快照(snapshot),只看到快照时点已提交的数据。

text
-- 事务 A(写)
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE owner = 'Alice';
-- 尚未 COMMIT

-- 事务 B(读,READ COMMITTED 默认)
SELECT balance FROM accounts WHERE owner = 'Alice';
-- 看到的仍是旧值(A 未提交,MVCC 通过元组的事务状态过滤掉)

两边看到的视图之所以不同,是因为快照里的可见性规则(见 可见性规则):一个元组只有在其创建事务已提交、且删除事务未提交(或尚未开始时)才可见。

sql
-- 查看当前会话的隔离级别
SHOW transaction_isolation;
-- 也可以会话级或事务级修改
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN ISOLATION LEVEL SERIALIZABLE;

写写冲突则由行级锁兜底:第二个 UPDATE 同一行的事务会被阻塞,直到第一个事务提交或回滚。若要「读出来就锁定」,需要显式 SELECT ... FOR UPDATE:

sql
BEGIN;
SELECT balance FROM accounts WHERE owner = 'Alice' FOR UPDATE;   -- 加行锁
UPDATE accounts SET balance = balance - 100 WHERE owner = 'Alice';
COMMIT;
text
BEGIN
 balance
---------
 1000.00
(1 row)

UPDATE 1
COMMIT

隔离性是 ACID 中最容易「打折」的一条:PostgreSQL 提供了四个隔离级别,标准允许的部分异常(如幻读、写偏斜)只在更高级别下才被消除,详见后续隔离级别章节。

持久性(Durability):WAL、fsync 与检查点 ​

持久性回答「提交成功后断电会怎样」。PostgreSQL 用 WAL(Write-Ahead Logging) 实现:修改数据页之前,必须先把描述该修改的 WAL 记录写入 pg_wal 目录,并在 COMMIT 返回前把 WAL 刷到持久存储。数据页(heap)本身可以延迟落盘,因为它可以从 WAL 重放出来。

text
COMMIT 的完整落盘路径:

客户端 COMMIT
   │
   ├─ 1. 生成 commit WAL 记录,写入 WAL 缓冲区
   ├─ 2. WAL writer / backend 把记录写入 pg_wal 文件(write)
   ├─ 3. fsync(pg_wal 文件)  ← 真正的「持久化点」
   ├─ 4. 写 CLOG(提交日志)标记事务已提交
   └─ 5. 返回 command tag: COMMIT

数据页(表文件)此时可能还只在 shared_buffers 里,
由 checkpoint 或后台 writer 在之后异步刷盘。

WAL 的关键参数:

参数默认值作用
fsyncon是否把 WAL 真正 fsync 到磁盘,关闭后果自负
synchronous_commiton提交时 WAL 需要刷到什么程度才返回成功
wal_levelreplicaWAL 记录详细程度(minimal / replica / logical)
full_page_writeson检查点后首次修改整页写入 WAL,防部分写损坏
checkpoint_timeout5min检查点间隔,决定崩溃恢复需要重放多少 WAL

检查点(checkpoint)把内存中的脏页刷盘,并在 WAL 中标记一个「此后不再需要回放更早记录」的起点。检查点越频繁,崩溃恢复越快,但刷盘压力越大。

sql
-- 手动触发检查点(生产环境慎用,会产生 IO 尖峰)
CHECKPOINT;
text
CHECKPOINT

synchronous_commit:持久性与性能的旋钮 ​

fsync = on 是「WAL 一定会落盘」的总开关,而 synchronous_commit 决定事务提交时是否要等待 WAL 落盘。把它调低可以在吞吐与一致性之间做权衡:

取值提交时等待崩溃可能丢失说明
on(默认)本地 WAL 刷盘无(已提交事务不丢)最安全
remote_apply本地刷盘 + 同步备机已重放无最强,延迟最高(需同步复制配置)
remote_write本地刷盘 + 同步备机已写入备机宕机可能丢折中
local本地刷盘(不等备机)无本地丢失同步复制场景下的本机保证
off不等待极端情况下可能丢最近若干事务吞吐最高,适合可容忍少量丢失的场景
sql
-- 会话级降低持久性要求
SET synchronous_commit TO off;

BEGIN;
INSERT INTO logs (msg) VALUES ('这条记录在崩溃时可能丢失');
COMMIT;
text
SET
BEGIN
INSERT 0 1
COMMIT

这里有三个常见误解必须澄清:

  • synchronous_commit = off 不等于事务会丢失原子性。 提交后数据库不会「看起来提交了一半」;最坏情况是最近几个已报告成功的事务在崩溃后消失,数据库本身仍是一致的。
  • synchronous_commit = off 不等于「提交立即返回但 WAL 永不落盘」。 WAL 仍会在后台刷盘,只是客户端不再等待。
  • 关闭 fsync(fsync = off)是另一回事,危险得多。 它可能导致崩溃后数据库无法启动或数据文件损坏,只应在从备份重建的临时环境使用。
ini
# postgresql.conf:用牺牲少量持久性换取批量导入吞吐
synchronous_commit = off
fsync = on

组提交(group commit):为什么高并发下 fsync 没那么贵 ​

看到「每个 COMMIT 都要 fsync」很容易担心性能,但 PostgreSQL 实现了组提交:当一个后端正在刷 WAL 时,其它并发提交的事务会把记录追加到同一批 WAL 里,一次 fsync 就为多个事务完成持久化。

text
组提交示意(同一时刻有 3 个事务要提交):

t0  T1 写入 commit 记录 ──┐
t1  T2 写入 commit 记录 ──┼──► 合并成一次 fsync ──► 三者同时返回 COMMIT
t2  T3 写入 commit 记录 ──┘

结果:3 个事务只付了 1 次 fsync 的成本。

这带来两个实践结论:

  • 高并发短事务的提交开销远低于直觉,不要因为「怕 fsync」而主动降低 synchronous_commit。
  • 串行执行的短事务最吃亏:每个提交都得单独等一次刷盘。批量写入时用「较大事务 + 较少提交次数」优于「每行一个事务」。
sql
-- 推荐:把批量写入收进一个有合理大小的事务,减少提交次数
BEGIN;
INSERT INTO metrics (name, value)
SELECT 'm' || g, g FROM generate_series(1, 10000) AS g;
COMMIT;
text
BEGIN
INSERT 0 10000
COMMIT

崩溃恢复:PostgreSQL 如何在宕机后自愈 ​

持久性的真正考验是崩溃恢复(crash recovery)。数据库重启时,会找到最后一次检查点的位置,然后重放(replay)该位置之后的所有 WAL 记录,把数据页恢复到崩溃前的一致状态:

text
崩溃恢复流程:

1. 读取 pg_control,定位最后一个 checkpoint 的 REDO 位置
2. 从 REDO 位置开始按顺序重放 WAL
3. 遇到 commit 记录 → 该事务的修改变为已提交(可见)
4. 遇到未提交记录 → 直接丢弃(原子性在恢复时同样成立)
5. 重放到 WAL 末尾 → 视为恢复完成,写入新的 checkpoint
6. 数据库进入接受连接状态

这套机制保证了原子性与持久性在崩溃场景下依然成立:已提交的事务被重放出来,未提交的事务被丢弃,不会出现「半个事务」。

bash
# 查看恢复进度(恢复过程中可用)
psql -c "SELECT pg_is_in_recovery(), pg_last_wal_replay_lsn(), pg_last_wal_receive_lsn();"
text
 pg_is_in_recovery | pg_last_wal_replay_lsn | pg_last_wal_receive_lsn
-------------------+------------------------+------------------------
 t                 | 0/1A2B3C8              | 0/1A2B3C8
(1 row)

崩溃恢复的耗时与「最后一个检查点之后积累的 WAL 量」成正比,这解释了为什么 checkpoint_timeout 与 max_wal_size 是持久性相关的性能参数。

ACID 与 CAP / BASE 的边界 ​

ACID 描述的是单个数据库实例内的事务保证,它不涉及分布式系统。谈分布式时常见的 CAP 与 BASE 是另一个层面的话题:

模型核心主张与 ACID 的关系
ACID单机事务的原子/一致/隔离/持久本文讨论的对象
CAP一致性、可用性、分区容错三者不可兼得单机 PostgreSQL 本质是 CP:网络分区时牺牲可用性
BASE基本可用、软状态、最终一致与 ACID 相反的设计取向,常见于分布式 KV

需要特别澄清一个常见误解:PostgreSQL 的「ACID」并不自动跨越多个数据库实例。在主从复制架构下,主库提交即视为持久(synchronous_commit = on 时至少保证本地落盘);备库上的数据是否持久、何时可见,取决于复制模式(异步 / 同步)与 synchronous_commit 的设置。

sql
-- 查看当前同步提交设置
SHOW synchronous_commit;
text
 synchronous_commit
--------------------
 on
(1 row)

跨库原子性:两阶段提交(2PC) ​

如果一次业务要写两个独立的 PostgreSQL 实例(或在同一实例中用不同连接写不同库),单机事务爱莫能助,此时需要两阶段提交(two-phase commit, 2PC):先把事务「准备」好并持久化,确认所有参与者都准备好之后再统一提交。

sql
-- 前置条件:max_prepared_transactions 必须大于 0(默认 0 表示禁用)
SHOW max_prepared_transactions;
text
 max_prepared_transactions
---------------------------
 0
(1 row)
sql
-- 阶段一:准备(数据已持久化,但对外不可见)
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE owner = 'Alice';
PREPARE TRANSACTION 'transfer-alice-to-bob';
text
BEGIN
UPDATE 1
PREPARE TRANSACTION
sql
-- 阶段二:确认所有参与者准备完毕后提交
COMMIT PREPARED 'transfer-alice-to-bob';
text
COMMIT PREPARED

2PC 的代价与风险必须清楚:

  • 事务在 PREPARE 后长期持锁,直到 COMMIT PREPARED / ROLLBACK PREPARED,期间无人能改这些行。
  • prepare 后崩溃会留下「悬挂事务」,需要用 pg_prepared_xacts 排查并人工决断:
sql
SELECT gid, prepared, owner, database FROM pg_prepared_xacts;
text
 gid | prepared | owner | database
-----+----------+-------+----------
(0 rows)
  • 2PC 不能解决协调者单点问题:如果应用在 PREPARE 之后、COMMIT PREPARED 之前崩溃,需要外部机制(如 Saga、事务协调器)来最终决定提交还是回滚。

因此多数业务更倾向于避免跨库写(用同一个库、或用异步补偿/事件驱动),而不是默认使用 2PC。

实战示例 ​

场景:用一个完整的转账流程,逐条验证 ACID 四项保证。

第 1 步:准备数据

sql
DROP TABLE IF EXISTS transfers, accounts;

CREATE TABLE accounts (
  id      bigint PRIMARY KEY,
  owner   text    NOT NULL UNIQUE,
  balance numeric(12,2) NOT NULL CHECK (balance >= 0)
);

CREATE TABLE transfers (
  id         bigserial PRIMARY KEY,
  from_id    bigint NOT NULL REFERENCES accounts(id),
  to_id      bigint NOT NULL REFERENCES accounts(id),
  amount     numeric(12,2) NOT NULL CHECK (amount > 0),
  created_at timestamptz NOT NULL DEFAULT now()
);

INSERT INTO accounts (id, owner, balance) VALUES
  (1, 'Alice', 1000.00),
  (2, 'Bob',     500.00);
text
DROP TABLE
CREATE TABLE
CREATE TABLE
INSERT 0 2

第 2 步:正常转账(验证原子性与一致性)

sql
BEGIN;

UPDATE accounts SET balance = balance - 200.00 WHERE id = 1;
UPDATE accounts SET balance = balance + 200.00 WHERE id = 2;
INSERT INTO transfers (from_id, to_id, amount) VALUES (1, 2, 200.00);

COMMIT;
text
BEGIN
UPDATE 1
UPDATE 1
INSERT 0 1
COMMIT
sql
SELECT owner, balance FROM accounts ORDER BY id;
SELECT sum(balance) AS total FROM accounts;
text
 owner | balance
-------+---------
 Alice |  800.00
 Bob   |  700.00
(2 rows)

 total
--------
 1500.00
(1 row)

总额仍是 1500.00,说明「转出与转入必须同时生效」这条一致性规则被事务守住了。

第 3 步:失败后回滚(再次验证原子性)

sql
BEGIN;

UPDATE accounts SET balance = balance - 5000.00 WHERE id = 1;  -- 会触发 CHECK 失败
UPDATE accounts SET balance = balance + 5000.00 WHERE id = 2;
COMMIT;
text
BEGIN
ERROR:  new row for relation "accounts" violates check constraint "accounts_balance_check"
DETAIL:  Failing row contains (1, Alice, -4200.00).
-- 后续语句被忽略,COMMIT 实际等价于 ROLLBACK
sql
SELECT owner, balance FROM accounts ORDER BY id;
text
 owner | balance
-------+---------
 Alice |  800.00
 Bob   |  700.00
(2 rows)

没有出现「Alice 少了 5000、Bob 没变」这种半成品状态——约束失败触发了语句级与事务级回滚。

第 4 步:验证隔离性

text
会话 A:
BEGIN;
UPDATE accounts SET balance = balance - 100.00 WHERE id = 1;

会话 B(READ COMMITTED):
SELECT balance FROM accounts WHERE id = 1;   -- 仍看到 800.00(A 未提交)
text
-- 会话 B 输出
 balance
---------
  800.00
(1 row)

第 5 步:验证持久性与清理

sql
-- 会话 A 提交后,B 才能看到新值
COMMIT;
text
COMMIT
sql
SELECT balance FROM accounts WHERE id = 1;   -- 700.00
DROP TABLE transfers, accounts;
text
 balance
---------
  700.00
(1 row)

DROP TABLE

注意事项 ​

  • ACID 建立在「默认配置」之上。 关闭 fsync、把 synchronous_commit 调到 off、使用非持久化的存储(如未开 O_DIRECT 的虚拟机临时盘),都会削弱持久性。错误!认为「PostgreSQL 是 ACID 数据库,所以怎么配都不会丢数据」。
sql
-- 会话级降低持久性:明确知道代价再用
SET synchronous_commit TO off;
-- 错误!把 fsync 关掉来表达「性能优先」
-- ALTER SYSTEM SET fsync = off;   -- 崩溃可能导致数据文件损坏
  • 一致性不等于「数据库帮你检查业务规则」。 CHECK / UNIQUE / FK 只能表达单行或单条引用的规则;「两人余额之和不变」「库存与订单数量匹配」这类跨行不变式,SQL 约束表达不了。不推荐把这类规则寄托于「数据库会保证」。
sql
-- 错误!以为 CHECK 能表达「转账前后总额不变」
-- ALTER TABLE accounts ADD CONSTRAINT total_ok CHECK (sum(balance) = 1500);
-- ERROR:  aggregate functions are not allowed in check constraints
  • 隔离级别决定你在 ACID 的「I」上买到多少。 默认的 READ COMMITTED 允许不可重复读与幻读;只有 SERIALIZABLE 才承诺「等价于某种串行执行」。错误!认为「用了事务就没有并发问题」。
sql
-- 默认级别下,同一事务内两次读取可能得到不同结果
BEGIN;
SELECT balance FROM accounts WHERE id = 1;
-- 期间另一个事务提交了修改
SELECT balance FROM accounts WHERE id = 1;   -- 可能与上一次不同
COMMIT;
  • 2PC 默认是关闭的,而且不能滥用。 max_prepared_transactions 默认 0,PREPARE TRANSACTION 会直接报错;开启后悬挂事务会长期持锁。
sql
-- 错误!未开启 2PC 时直接准备事务
BEGIN;
UPDATE accounts SET balance = balance - 1 WHERE id = 1;
PREPARE TRANSACTION 'tx1';
text
ERROR:  prepared transactions are disabled
HINT:  Set max_prepared_transactions to a nonzero value.

最佳实践 ​

  • 保持 fsync = on 与 synchronous_commit = on 作为默认,只在明确能接受少量丢失的场景(日志表、监控埋点、可重建的导入任务)逐会话或逐事务调低。
sql
-- 只对可容忍丢失的写入降低持久性
BEGIN;
SET LOCAL synchronous_commit TO off;
INSERT INTO metrics (name, value) VALUES ('heartbeat', 1);
COMMIT;
-- 事务结束后自动恢复为服务器默认值
  • 把业务不变式尽量下沉到约束,让数据库在 COMMIT 前就拒绝不合法数据;跨行规则用触发器或「正确的语句编排」维护,并写测试覆盖。
sql
-- 推荐:用外键 + CHECK 把结构一致性交给数据库
ALTER TABLE transfers
  ADD CONSTRAINT transfers_amount_positive CHECK (amount > 0);

ALTER TABLE transfers
  ADD CONSTRAINT transfers_distinct_accounts CHECK (from_id <> to_id);
  • 控制事务粒度:短事务 + 合理批次。 既能享受组提交带来的 fsync 摊销,又能减少锁持有时间和 VACUUM 压力。
sql
-- 推荐:分批提交,而不是百万行一个大事务
DO $$
DECLARE i int;
BEGIN
  FOR i IN 0..99 LOOP
    INSERT INTO metrics (name, value)
    SELECT 'm' || g, g FROM generate_series(i * 1000 + 1, (i + 1) * 1000) AS g;
    COMMIT;   -- 每批提交一次
  END LOOP;
END $$;
  • 谨慎使用 2PC。 若必须跨库写,先用 pg_prepared_xacts 建立监控与人工处理流程,并考虑用事件驱动 + 幂等补偿替代。
sql
-- 监控悬挂事务(应长期为 0 行)
SELECT gid, prepared, owner, database
FROM pg_prepared_xacts
ORDER BY prepared;

下一节

继续学习:事务自动提交

参考链接 ​