Skip to content

设置隔离级别 ​

前几节把四种隔离级别(READ COMMITTED、REPEATABLE READ、SERIALIZABLE,以及被降级的 READ UNCOMMITTED)的语义与异常边界都讲清了,但「怎么设」这件事本身在 PostgreSQL 里有五个不同的入口,且各自的作用域、生效时机、限制条件都不一样。更麻烦的是其中最常用的 SET TRANSACTION 有一条容易被忽略的硬规则:第一条查询执行之后,隔离级别就改不了了。

本节把这些入口一次性梳理清楚,讲明会话默认、事务级、配置项三种设置的优先关系与生效时机,以及跨会话共享快照(SET TRANSACTION SNAPSHOT)这类容易被忽视的能力。

前置知识

阅读本节前,建议先了解:快照隔离与可串行化异常、SERIALIZABLE

基础概念 ​

五个入口,三个作用域 ​

先建立一张全局地图。设置隔离级别的方式共有五种,它们分别落在会话级与事务级两个层次上:

入口作用域生效时机典型位置
BEGIN ISOLATION LEVEL ...单个事务事务开启时应用代码
START TRANSACTION ISOLATION LEVEL ...单个事务事务开启时应用代码
SET TRANSACTION ISOLATION LEVEL ...当前事务首条语句之前应用代码
SET SESSION CHARACTERISTICS AS TRANSACTION ...后续事务本会话之后的所有事务应用初始化
default_transaction_isolation后续事务会话/数据库/集群postgresql.conf、ALTER DATABASE
text
作用域层级(由宽到窄,窄的覆盖宽的)
────────────────────────────────────────────────────
集群级  postgresql.conf / ALTER SYSTEM
          │
数据库级  ALTER DATABASE ... SET
          │
角色级    ALTER ROLE ... SET
          │
会话级    SET SESSION CHARACTERISTICS / SET default_transaction_isolation
          │
事务级    BEGIN ISOLATION LEVEL / SET TRANSACTION

优先级规则很简单:越靠近事务的设置越优先。 事务级设置只影响当前事务,事务结束后会话默认值立刻重新接管。

隔离级别是一个「事务属性」,不是普通 GUC ​

这是最容易造成误解的一点。default_transaction_isolation 是可以在配置文件里写的普通参数,但 transaction_isolation 不是:

The current transaction's modes can similarly be set or examined via the configuration parameters transaction_isolation, transaction_read_only, and transaction_deferrable. Setting one of these parameters acts the same as the corresponding SET TRANSACTION option, with the same restrictions on when it can be done. However, these parameters cannot be set in the configuration file, or from any source other than live SQL.

两个参数名看起来对称,能力却完全不同:

参数含义可否写在 postgresql.conf可否 ALTER DATABASE ... SET
default_transaction_isolation后续事务的默认级别✅✅
transaction_isolation当前事务的级别❌ 只能靠实时 SQL 设置❌

所以 ALTER DATABASE mydb SET transaction_isolation = 'serializable'; 没有意义,不会报错,但也不会让数据库默认变成可串行化。要做这件事必须用 default_transaction_isolation。

会话默认与 SET SESSION CHARACTERISTICS 是同一件事 ​

官方原文:In fact SET SESSION CHARACTERISTICS is just a verbose equivalent for setting these variables with SET.

也就是说下面两行完全等价:

sql
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET default_transaction_isolation = 'repeatable read';

想通这一点,就理解了为什么 SET SESSION CHARACTERISTICS 会「粘住」——它改的就是会话默认值,自然对之后每一个事务生效,直到会话结束或再次修改。

text
会话生命周期
─────────────────────────────────────────────────────────
连接建立
   │  default_transaction_isolation = 'read committed'   ← 来自配置
   │
   ├─ SET SESSION CHARACTERISTICS ... = REPEATABLE READ   ← 改会话默认
   │
   ├─ BEGIN                                → RR(用会话默认)
   ├─ BEGIN ISOLATION LEVEL SERIALIZABLE   → SER(事务级覆盖)
   ├─ BEGIN                                → RR(事务级设置已失效)
   └─ ...
连接关闭(会话默认随之消失)

这个替换只对「默认值」成立

SET SESSION CHARACTERISTICS 能替换成 SET default_transaction_isolation,但 SET TRANSACTION 不能替换成 SET transaction_isolation——后者虽然效果相同,但作用对象是「当前事务」,语义上仍是事务级操作,且同样受限:必须在首条语句之前。

生效时机:一条硬规则 ​

这是本节最需要记住的一句话,官方原文:

The transaction isolation level cannot be changed after the first query or data-modification statement (SELECT, INSERT, DELETE, UPDATE, MERGE, FETCH, or COPY) of a transaction has been executed.

注意这份清单的范围很宽:

命令是否「污染」事务、锁死隔离级别
SELECT✅ 会
INSERT / UPDATE / DELETE✅ 会
MERGE✅ 会
FETCH(游标取数)✅ 会
COPY✅ 会
SET / SHOW❌ 不会
SET TRANSACTION❌ 不会(它本身就是设置动作)
DECLARE(只声明游标)❌ 不会
SAVEPOINT❌ 不会
LOCK TABLE❌ 不会(但会拿锁,见注意事项)

也就是说:只要跑过任何一条真正的读或写语句,SET TRANSACTION ISOLATION LEVEL 就会报错。

sql
BEGIN;
SELECT 1;                                          -- 已经跑过一条 SELECT
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;      -- 这条会失败
text
ERROR:  SET TRANSACTION ISOLATION LEVEL must be called before any query

为什么要有这条限制 ​

因为隔离级别决定的是事务取哪一份快照。READ COMMITTED 每条语句取新快照,REPEATABLE READ / SERIALIZABLE 在事务的第一条语句时取一份固定快照。

text
READ COMMITTED                      REPEATABLE READ / SERIALIZABLE
────────────────                    ─────────────────────────────
语句1 → 取快照1 → 执行              语句1 → 取快照 → 执行
语句2 → 取快照2 → 执行              语句2 → 复用同一快照 → 执行
语句3 → 取快照3 → 执行              语句3 → 复用同一快照 → 执行
        ↑ 每句都新取                        ↑ 全程一份,不可中途更换

一旦第一条语句已经取了快照,再改隔离级别就等价于「中途换快照」,这在语义上无法自洽。所以 PostgreSQL 直接禁止,而不是尝试兼容。

语法与用法 ​

text
-- ① 事务开启时指定(推荐)
BEGIN [ WORK | TRANSACTION ] [ transaction_mode [, ...] ]
START TRANSACTION [ transaction_mode [, ...] ]

-- ② 当前事务设置(必须在首条语句之前)
SET TRANSACTION transaction_mode [, ...]

-- ③ 导入一个已存在的快照(仅当前事务可用)
SET TRANSACTION SNAPSHOT snapshot_id

-- ④ 会话默认(等价于 SET default_transaction_*)
SET SESSION CHARACTERISTICS AS TRANSACTION transaction_mode [, ...]

where transaction_mode is one of:
    ISOLATION LEVEL { SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED }
    READ WRITE | READ ONLY
    [ NOT ] DEFERRABLE

最简可运行示例:

sql
-- 方式一:BEGIN 时直接带上所有属性
BEGIN ISOLATION LEVEL SERIALIZABLE READ ONLY DEFERRABLE;
SELECT count(*) FROM orders;
COMMIT;
text
BEGIN
 count
-------
 48213
(1 row)

COMMIT
sql
-- 方式二:先 BEGIN,再 SET TRANSACTION(必须在任何查询之前)
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY;
SELECT count(*) FROM orders;
COMMIT;
text
postgres=# SHOW transaction_isolation;
 transaction_isolation
-----------------------
 read committed
(1 row)

postgres=# SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET SESSION CHARACTERISTICS

postgres=# BEGIN;
BEGIN
postgres=#* SHOW transaction_isolation;   -- 提示符里的 * 表示在事务中
 transaction_isolation
-----------------------
 repeatable read
(1 row)

详细说明 ​

BEGIN 与 START TRANSACTION 的差异 ​

START TRANSACTION 是 SQL 标准命令,BEGIN 是 PostgreSQL 扩展,两者功能相同。官方原文:START TRANSACTION has the same functionality as BEGIN.

对比项BEGINSTART TRANSACTION
来源PostgreSQL 扩展SQL 标准
支持 transaction_mode✅(PG 扩展能力)✅
幂等性已在事务中会警告且不改状态同
可移植性差好

关于重复 BEGIN 的行为,官方原文:Issuing BEGIN when already inside a transaction block will provoke a warning message. The state of the transaction is not affected.

text
postgres=# BEGIN;
BEGIN
postgres=# BEGIN;
WARNING:  there is already a transaction in progress
BEGIN
postgres=# ROLLBACK;
ROLLBACK

这是好消息:重复 BEGIN 不会像 MySQL 那样静默提交前一个事务。在连接池或框架里混用事务控制时,这个行为少了一个数据丢失陷阱。

SET TRANSACTION 的三个特性 ​

可设置的事务特性共四类:隔离级别、访问模式(READ WRITE / READ ONLY)、延迟性(DEFERRABLE),以及(仅当前事务)导入快照。

官方对 DEFERRABLE 的描述:

The DEFERRABLE transaction property has no effect unless the transaction is also SERIALIZABLE and READ ONLY. When all three of these properties are selected for a transaction, the transaction may block when first acquiring its snapshot, after which it is able to run without the normal overhead of a SERIALIZABLE transaction and without any risk of contributing to or being canceled by a serialization failure. This mode is well suited for long-running reports or backups.

三个属性的组合效果:

组合DEFERRABLE 是否起作用效果
SERIALIZABLE READ ONLY DEFERRABLE✅取快照时可能等待,之后零冲突、零中止
SERIALIZABLE READ WRITE DEFERRABLE❌ 静默忽略与不带 DEFERRABLE 相同
REPEATABLE READ READ ONLY DEFERRABLE❌ 静默忽略DEFERRABLE 无效
READ COMMITTED ... DEFERRABLE❌ 静默忽略DEFERRABLE 无效

DEFERRABLE 失效时不会报错

把 DEFERRABLE 用在错误的组合上,PostgreSQL 不会给出任何提示。SHOW transaction_deferrable 仍然显示 on,但它对读写事务或低隔离级别毫无作用。这种「看起来生效实际没生效」的状态最容易误导排查方向。

只读事务约束了什么 ​

READ ONLY 不是「性能开关」,而是一份语义契约。官方原文列出的禁止清单:

When a transaction is read-only, the following SQL commands are disallowed: INSERT, UPDATE, DELETE, MERGE, and COPY FROM if the table they would write to is not a temporary table; all CREATE, ALTER, and DROP commands; COMMENT, GRANT, REVOKE, TRUNCATE; and EXPLAIN ANALYZE and EXECUTE if the command they would execute is among those listed. This is a high-level notion of read-only that does not prevent all writes to disk.

注意最后一句:它不阻止所有磁盘写入。临时表、临时文件、序列号推进、WAL 写入都仍然可能发生。

sql
BEGIN READ ONLY;
SELECT count(*) FROM orders;         -- 允许
CREATE TEMP TABLE t AS SELECT 1;     -- 允许(临时表)
UPDATE orders SET status = 'x';      -- 拒绝
CREATE TABLE new_t (id int);         -- 拒绝
text
ERROR:  cannot execute UPDATE in a read-only transaction

会话级设置:SET SESSION CHARACTERISTICS ​

sql
-- 会话默认隔离级别
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- 也可以只设访问模式
SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY;

-- 等价写法(同一件事)
SET default_transaction_isolation  = 'serializable';
SET default_transaction_read_only  = on;
SET default_transaction_deferrable = off;

SET SESSION CHARACTERISTICS 与直接 SET 配置项的唯一区别在可设置位置:前者只能实时执行,后者可以写进 postgresql.conf、ALTER DATABASE、ALTER ROLE。

把全库默认级别改成可串行化(谨慎,见注意事项):

sql
ALTER DATABASE myapp SET default_transaction_isolation = 'serializable';

把某个报表角色默认设为可延迟只读(这种用法很安全):

sql
ALTER ROLE report_bot IN DATABASE myapp
  SET default_transaction_isolation = 'serializable';
ALTER ROLE report_bot IN DATABASE myapp
  SET default_transaction_read_only = on;
ALTER ROLE report_bot IN DATABASE myapp
  SET default_transaction_deferrable = on;

查看当前状态 ​

三个 GUC 分别对应三个特性,且都反映当前事务的真实状态:

参数类型取值
transaction_isolationenumread uncommitted / read committed / repeatable read / serializable
transaction_read_onlybooleanon / off
transaction_deferrablebooleanon / off
sql
SHOW transaction_isolation;

SELECT current_setting('transaction_isolation')     AS iso,
       current_setting('transaction_read_only')     AS ro,
       current_setting('transaction_deferrable')    AS deferrable;
text
postgres=# BEGIN ISOLATION LEVEL SERIALIZABLE READ ONLY DEFERRABLE;
BEGIN
postgres=# SELECT current_setting('transaction_isolation'),
postgres-#        current_setting('transaction_read_only'),
postgres-#        current_setting('transaction_deferrable');
 current_setting | current_setting | current_setting
-----------------+-----------------+-----------------
 serializable    | on              | on
(1 row)

在 autocommit 状态下(没有显式 BEGIN),这三个值反映的是下一个隐式事务的默认值,也就是会话默认值:

text
postgres=# SHOW transaction_isolation;      -- 尚未开事务
 transaction_isolation
-----------------------
 read committed
(1 row)

READ UNCOMMITTED 会被静默降级 ​

官方原文:The SQL standard defines one additional level, READ UNCOMMITTED. In PostgreSQL READ UNCOMMITTED is treated as READ COMMITTED.

sql
BEGIN ISOLATION LEVEL READ UNCOMMITTED;
SELECT current_setting('transaction_isolation');
text
    current_setting
-----------------------
 read committed
(1 row)

不报错、不警告,直接降级。 PostgreSQL 的 MVCC 实现从根本上不会产生脏读,所以「读未提交」无处可退,只能退到「读已提交」。把它写进代码是纯粹的可移植性负担——在其它数据库上真会变成脏读,在 PostgreSQL 上则毫无效果。

SET TRANSACTION SNAPSHOT:跨会话共享快照 ​

这是 PostgreSQL 的扩展能力,用于让一个新事务使用另一个已存在事务的快照。典型场景:需要多个并行进程读同一份一致性数据(例如并行导出、并行校验)。

sql
-- 会话 A:开启一个可导出快照的事务
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT pg_export_snapshot();
text
 pg_export_snapshot
---------------------
 00000003-0000001B-1
(1 row)
sql
-- 会话 B:在自己的事务开头导入该快照
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET TRANSACTION SNAPSHOT '00000003-0000001B-1';
SELECT count(*) FROM orders;      -- 与 A 看到完全相同的版本
COMMIT;

规则相当严格,官方原文逐条列出:

规则说明
导出方必须调用 pg_export_snapshot()它返回快照标识符
标识符必须写成字符串字面量形如 '00000003-0000001B-1'
只能在事务开头执行必须在首条查询/数据修改语句之前
导入方必须是 SERIALIZABLE 或 REPEATABLE READ否则快照会被立即丢弃
SERIALIZABLE 导入方要求导出方也是 SERIALIZABLE否则报错
非只读的可串行化事务不能从只读事务导入否则报错

为什么必须要 REPEATABLE READ 以上

官方原文:otherwise, the snapshot would be discarded immediately, since READ COMMITTED mode takes a new snapshot for each command.

READ COMMITTED 的语义就是「每条语句取新快照」,导入一份固定快照与它自相矛盾,所以 PostgreSQL 拒绝这种组合,而不是假装支持。

SET TRANSACTION SNAPSHOT 不能通过 BEGIN 的参数形式实现,官方原文:It is possible to dispense with SET TRANSACTION by instead specifying the desired transaction_modes in BEGIN or START TRANSACTION. But that option is not available for SET TRANSACTION SNAPSHOT.

没有 BEGIN 时执行 SET TRANSACTION ​

官方原文:If SET TRANSACTION is executed without a prior START TRANSACTION or BEGIN, it emits a warning and otherwise has no effect.

text
postgres=# SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
WARNING:  SET TRANSACTION can only be used in transaction blocks
SET TRANSACTION

postgres=# SHOW transaction_isolation;     -- 没有任何改变
 transaction_isolation
-----------------------
 read committed
(1 row)

这是一个极易被忽略的静默失效:在自动提交模式下(PHP PDO 默认、很多 ORM 的默认状态)执行 SET TRANSACTION,只会得到一个警告,代码继续跑,隔离级别丝毫未变。

ORM 与连接池里的典型事故

应用代码写成:

php
$pdo->exec("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE");
$pdo->exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1");

这里没有显式 BEGIN,SET TRANSACTION 完全无效,UPDATE 在默认的 READ COMMITTED 下执行。正确做法是使用驱动自带的事务 API($pdo->beginTransaction()),或在 BEGIN 之后立即 SET TRANSACTION。

使用 PDO 时,PDO::beginTransaction() 默认发出的是 BEGIN,所以下面的写法是有效的:

php
$pdo->beginTransaction();
$pdo->exec("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE");

SET TRANSACTION 与锁的微妙关系 ​

LOCK TABLE 不属于官方「污染隔离级别」的清单,所以下面的写法语法上可行:

sql
BEGIN;
LOCK TABLE orders IN ACCESS EXCLUSIVE MODE;     -- 不在清单里
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;   -- 语法上通过

但这几乎总是错的:LOCK TABLE 在 READ COMMITTED 下已经按语句级语义取过锁,此时切换隔离级别得到的事务语义并不是你所预期的。正确顺序永远是先定隔离级别,再做任何事。

事务级别设置的「粘性」与连接池 ​

事务级设置在 COMMIT / ROLLBACK 时自动失效,这一点对连接池很友好:

text
连接池借出的物理连接
────────────────────────────────────────────────
BEGIN ISOLATION LEVEL SERIALIZABLE;   ← 事务级,只影响本事务
  ...
COMMIT;                                ← 设置随之失效
                                         下一个使用者拿到的是干净的默认级别

真正有风险的是会话级设置:如果中间件执行了 SET SESSION CHARACTERISTICS 或 SET default_transaction_isolation,这个改动会跟着物理连接回到池子里,污染下一个使用者。

如果必须用会话级设置,正确做法是用 SET LOCAL 或 RESET:

sql
-- 只在当前事务内生效,COMMIT/ROLLBACK 后自动恢复
SET LOCAL default_transaction_isolation = 'serializable';

-- 或显式还原
RESET default_transaction_isolation;

pg_export_snapshot() 与并行一致性读取 ​

把前面的能力串起来,就是一个可用的模式:主进程导出快照,多个工作进程并行处理同一份数据视图。

text
主进程(协调者)                    工作进程 × N
────────────────                    ─────────────────────────
BEGIN ISOLATION LEVEL
      REPEATABLE READ
pg_export_snapshot()
     → '00000003-0000001B-1'  ──┬──► BEGIN ISOLATION LEVEL REPEATABLE READ
                                │    SET TRANSACTION SNAPSHOT '...'
                                │    SELECT ... WHERE mod(id, N) = 0
                                ├──► 同上,mod = 1
                                └──► 同上,mod = 2
COMMIT                              COMMIT

关键收益:所有工作进程看到的是同一个 MVCC 快照,因此可以安全地做跨分片的聚合校验,而不必担心各进程读到不同时间点的数据。

成本:导出方的事务必须一直保持打开(xmin 无法推进,会阻塞 VACUUM,产生表膨胀),所以导出方的生命周期要尽可能短。

实战示例 ​

示例一:报表连接统一用可延迟只读 ​

为报表角色设置数据库级默认值,做到「连接即安全」:

sql
ALTER ROLE report_bot IN DATABASE myapp
  SET default_transaction_isolation = 'serializable';
ALTER ROLE report_bot IN DATABASE myapp
  SET default_transaction_read_only = on;
ALTER ROLE report_bot IN DATABASE myapp
  SET default_transaction_deferrable = on;

-- 验证
\c myapp report_bot
BEGIN;
SHOW transaction_isolation;      -- serializable
SHOW transaction_read_only;      -- on
SHOW transaction_deferrable;     -- on
COMMIT;

效果:报表连接永远不会因为串行化冲突而失败(DEFERRABLE 只在取快照时可能等待),同时也写不了数据。

示例二:检测应用是否真的设上了隔离级别 ​

排查「明明设了却还是读到脏数据」这类问题,最有效的办法是把实际级别打印出来:

sql
CREATE OR REPLACE FUNCTION assert_isolation(expected text)
RETURNS void
LANGUAGE plpgsql
AS $$
DECLARE
    actual text := current_setting('transaction_isolation');
BEGIN
    IF actual <> expected THEN
        RAISE EXCEPTION 'isolation mismatch: expected %, got %', expected, actual
              USING ERRCODE = '25000';
    END IF;
END;
$$;

-- 应用在 BEGIN 之后立刻调用
BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT assert_isolation('serializable');
text
assert_isolation
------------------
(1 row)

若应用实际跑在 READ COMMITTED(例如 SET TRANSACTION 静默失效),这里会立刻抛出明确错误,而不是让业务在错误的隔离级别上悄悄产生数据问题。

示例三:按业务类型分流的隔离级别策略 ​

同一个应用里不同操作需要不同级别,用封装函数统一入口:

sql
CREATE OR REPLACE FUNCTION with_serializable(sql text) RETURNS void
LANGUAGE plpgsql AS $$
BEGIN
    SET LOCAL default_transaction_isolation = 'serializable';
    EXECUTE sql;
END;
$$;

CREATE OR REPLACE FUNCTION with_snapshot_read(sql text) RETURNS void
LANGUAGE plpgsql AS $$
BEGIN
    SET LOCAL default_transaction_isolation = 'repeatable read';
    SET LOCAL default_transaction_read_only     = on;
    EXECUTE sql;
END;
$$;

SET LOCAL 的作用范围严格限制在当前事务,事务一结束就自动恢复,不会通过连接池泄漏。

示例四:并行导出一致性快照 ​

sql
-- 协调会话
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT pg_export_snapshot();
\gset              -- 把结果存进 psql 变量

-- 在其它会话(可以并行启动)中:
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET TRANSACTION SNAPSHOT :'pg_export_snapshot';
SELECT count(*) FROM big_table WHERE mod(id, 4) = 0;
COMMIT;

-- 协调会话最后收尾
COMMIT;

示例五:定位连接池里的隔离级别污染 ​

sql
-- 找出当前所有连接上「非默认」的事务默认级别
SELECT pid,
       usename,
       state,
       application_name,
       current_setting('default_transaction_isolation') AS default_iso,
       current_setting('transaction_isolation')         AS current_iso
FROM   pg_stat_activity
WHERE  datname = current_database()
ORDER  BY application_name, pid;

若某个连接池的 default_iso 普遍是 serializable 而应用并未这样设计,说明有中间件执行了会话级 SET,且该设置正在被复用。

注意事项 ​

  • SET TRANSACTION 必须在首条语句之前。 一旦执行过 SELECT / INSERT / UPDATE / DELETE / MERGE / FETCH / COPY,就再也不能改。

    sql
    BEGIN;
    SELECT 1;
    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;   -- 错误!
    text
    ERROR:  SET TRANSACTION ISOLATION LEVEL must be called before any query
  • 没有 BEGIN 时 SET TRANSACTION 只是印一条警告。 不会报错、不会中断,隔离级别丝毫未变。错误! 在自动提交模式下执行 SET TRANSACTION ISOLATION LEVEL ...,然后以为已经生效。

  • READ UNCOMMITTED 被静默当成 READ COMMITTED。 不报错、不警告。不推荐为了「兼容其它数据库」而写它——在 PostgreSQL 上除了误导读者之外没有任何作用。

  • DEFERRABLE 只在 SERIALIZABLE READ ONLY 下有效。 其它组合会被静默忽略,而 SHOW transaction_deferrable 仍显示 on。错误! 以为 SERIALIZABLE READ WRITE DEFERRABLE 能减少冲突。

  • transaction_isolation 不能写进配置文件。 ALTER DATABASE ... SET transaction_isolation = ... 是无效的。要改默认级别必须用 default_transaction_isolation。

  • SET SESSION CHARACTERISTICS 会污染连接池。 它改的是会话默认值,物理连接归还池子后设置仍然存在。不推荐在中间件里直接用它;用 SET LOCAL 或 RESET。

  • READ ONLY 不阻止所有磁盘写入。 临时表、临时文件、序列号推进、WAL 写入都可能发生。错误! 把 READ ONLY 当作「零磁盘 I/O」的性能优化。

  • SET TRANSACTION SNAPSHOT 只能在事务开头。 且导入方必须是 SERIALIZABLE 或 REPEATABLE READ,否则快照被立即丢弃。

  • 导出快照的事务会拖住 xmin。 只要它不结束,VACUUM 就无法回收该快照之后的所有死元组。不推荐让导出会话长时间开着。

  • LOCK TABLE 不在「污染清单」里,但先锁后改级别是错的。 应当在 BEGIN 时就一次性定好隔离级别,不要在事务中途切换。

  • BEGIN 在已有事务中只产生警告。 它不会提交前一个事务(与 MySQL 行为不同),所以在连接池里误发 BEGIN 不会导致数据丢失,但会导致「事务边界与预期不符」。

  • 只读事务在 SERIALIZABLE 下仍可能被中止。 除非加上 DEFERRABLE。错误! 以为只读事务永远安全,从而不给只读查询准备重试逻辑。

最佳实践 ​

  • 优先在 BEGIN 时一次性指定所有属性,而不是 BEGIN 后再补 SET TRANSACTION。少一步操作,就少一次忘记的风险。

    sql
    BEGIN ISOLATION LEVEL SERIALIZABLE READ ONLY DEFERRABLE;
  • 用驱动或框架的事务 API 开启事务,不要手写 BEGIN 字符串再手写 SET TRANSACTION。 后者在自动提交模式下会静默失效。

    php
    $pdo->beginTransaction();
    $pdo->exec("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE");
  • 报表、备份、批量校验一律用 REPEATABLE READ READ ONLY,延迟不敏感的再升级到 SERIALIZABLE READ ONLY DEFERRABLE。

  • 只在有明确跨行不变式需求时才用 SERIALIZABLE,并配套实现 40001 / 40P01 的重试框架。

  • 需要会话级改动时用 SET LOCAL,不要用 SET / SET SESSION CHARACTERISTICS。

    sql
    SET LOCAL default_transaction_isolation = 'serializable';
  • 在连接池环境下,连接借出时显式 RESET ALL 或逐项 RESET,消除上一个使用者的残留设置。

  • 给关键事务加上隔离级别断言(如示例二),把「以为设上了」变成「确保设上了」。

  • 不要改动全库默认隔离级别。 用角色级或数据库级默认值(ALTER ROLE ... SET)定向覆盖,避免影响所有连接。

    sql
    ALTER ROLE batch_worker IN DATABASE myapp
      SET default_transaction_isolation = 'repeatable read';
  • 把隔离级别写进代码注释或配置常量,让「这个事务为什么用 SERIALIZABLE」在半年后仍可回答。

  • 为 SERIALIZABLE 事务准备幂等的重试路径,重试必须覆盖整个事务,而不是单条失败语句。

  • 监控隔离级别分布与冲突率,用 pg_stat_database 的 xact_rollback 与 pg_stat_activity 的 current_setting('transaction_isolation') 交叉观察。

    sql
    SELECT application_name,
           current_setting('transaction_isolation') AS iso,
           count(*)
    FROM   pg_stat_activity
    WHERE  datname = current_database()
    GROUP  BY 1, 2 ORDER BY 3 DESC;
  • 并行处理一致性数据时用 pg_export_snapshot() + SET TRANSACTION SNAPSHOT,并确保导出会话尽快结束。

参考链接 ​