跳转到主要内容

10.1 隔离级别与可观察现象

隔离级别描述“并发事务成功提交后允许出现什么结果”,不是给单条查询加一层缓存。PostgreSQL 的实现关系是:

请求级别 PostgreSQL 实际语义 普通快照 仍可能发生
Read Uncommitted 等同 Read Committed 每条语句 nonrepeatable read、phantom、serialization anomaly
Read Committed 默认级别 每条语句 同上
Repeatable Read snapshot isolation 首个非事务控制语句取得事务快照 serialization anomaly,例如 write skew
Serializable Serializable Snapshot Isolation 事务快照 + read/write dependency 检测 事务可能以 40001 被拒绝

PostgreSQL Repeatable Read 比 SQL 标准最低要求更强:它不允许 phantom read;但“看见稳定快照”仍不等于“所有成功事务可排成某个串行顺序”。

10.1.1 Read Committed 的语句快照

每条普通查询重新取 snapshot

默认 Read Committed 下,一条普通 SELECT 看见:

  • 该语句开始前已提交的数据;
  • 当前事务自己先前的写入;
  • 看不见其他事务未提交的写入;
  • 看不见该语句执行过程中才提交的新版本。

同一事务中的下一条 SELECT 会取得新 snapshot,因此可能看到并发提交:

T1                                      T2
BEGIN;                                  BEGIN;
SELECT available;  -- 100
                                        UPDATE ... SET available=90;
                                        COMMIT;
SELECT available;  -- 90
COMMIT;

这不是“不可重复读 bug”,而是 Read Committed 的合同。需要一个稳定跨语句视图时,要重新设计事务、锁或隔离级别,而不是假设 BEGIN 自动冻结所有读。

查看当前事务设置:

SHOW transaction_isolation;
SELECT current_setting('transaction_isolation');

显式设置应在 transaction 第一条 query 前:

BEGIN ISOLATION LEVEL READ COMMITTED;
-- work
COMMIT;

不能先执行业务查询再把当前事务切到更高隔离级别。

写语句会等待并重新检查目标行

UPDATEDELETESELECT ... FOR UPDATE/SHARE 搜索候选时使用语句 snapshot,但候选行可能已被并发事务修改。PostgreSQL 会等待先行 writer:

先行事务 rollback
  → 后行事务可处理原版本

先行事务 commit update
  → 后行事务在新版本上重新检查 WHERE
  → 仍满足才执行

先行事务 commit delete
  → 后行事务跳过该行

这解释了为什么原子条件更新安全:

UPDATE inventory
SET available = available - $2
WHERE sku_id = $1
  AND available >= $2
RETURNING available;

若另一事务先扣减并提交,后行 UPDATE 会在最新 row version 上重新检查 available >= $2,不会拿语句开始时的旧值硬算。

但它也意味着一条复杂 Read Committed 写语句可能观察到“目标行的新版本”,却看不见同一并发事务在其他行的变化。对预先确定的单行原子写通常正合适;对跨行不变量必须更谨慎。

snapshot 不覆盖所有数据库对象

sequence 的变化立即对其他事务可见,且 abort 不会回滚:

SELECT nextval('order_id_seq');
ROLLBACK;

被取走的值不会“归还”。因此序列 gap 不是事务隔离失败,ID 连续性也不应作为业务不变量。外部 API、文件、消息系统同样不受 PostgreSQL snapshot/rollback 管理。

10.1.2 Repeatable Read 的事务快照与写冲突

snapshot 从首个真正语句开始

Repeatable Read transaction 看见首个非事务控制语句开始前已提交的数据,之后普通查询保持相同视图:

T1 (RR)                                  T2
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT available;  -- snapshot=100
                                         UPDATE ... SET available=90;
                                         COMMIT;
SELECT available;  -- 仍为 100
COMMIT;

事务开始的 wall-clock 时刻不一定是 snapshot 时刻;只执行 BEGIN 后长时间空闲,再执行首个 query,snapshot 才建立。诊断时同时看 xact_startquery_startstatebackend_xmin,不要把它们混成一个时间。

修改 snapshot 后已被改过的行会拒绝

若 RR 事务想更新/锁定一个在 snapshot 建立后被其他事务实际更新或删除并提交的 row,PostgreSQL 不会把旧计算静默覆盖到新版本,而是:

SQLSTATE 40001
could not serialize access due to concurrent update

本章两个 worker 都先读 available=100,再分别准备写 90 和 80。协调屏障放行后:

one transaction commits
the other exits 40001
final = 90 or 80 / version=1

哪个事务赢不是合同;“一提交、一拒绝、无静默覆盖”才是。

应用必须 ROLLBACK 并从 BEGIN 前重放整个逻辑。只重试失败的 UPDATE 会继续使用旧 snapshot、旧决策或旧 application state。

稳定快照仍允许 write skew

两名医生都在值班,规则是“至少一人 on call”。两个 RR 事务分别:

T1 reads count(on_call)=2       T2 reads count(on_call)=2
T1 turns doctor 1 off           T2 turns doctor 2 off
T1 commits                      T2 commits

它们写不同 row,没有 same-row write conflict;各自在自己的 snapshot 中都满足规则,最终却是 0。PostgreSQL RR 阻止 phantom,但仍允许这种 snapshot-isolation serialization anomaly。

因此:

“我的事务里连续两次读一样”
“所有提交结果都等价于事务逐个执行”

跨行不变量可以用锁住共同 guard row、锁定完整决策集合、显式 table lock、可验证的数据约束或 Serializable;选择取决于冲突率和模型。

10.1.3 Serializable、谓词冲突与序列化失败

SSI 不把所有读变成阻塞锁

PostgreSQL Serializable 在 Repeatable Read snapshot 上增加 Serializable Snapshot Isolation(SSI)依赖检测。它跟踪:

transaction A read something
transaction B wrote something that would have changed A's result

并分析这些 read/write dependency 是否组成无法串行化的危险结构。必要时拒绝一个事务:

SQLSTATE 40001
could not serialize access due to read/write dependencies among transactions

它不是传统的“所有 predicate read 都阻塞 writer”。SSI predicate lock 在 pg_locks 中显示为:

mode = SIReadLock

这种锁用于依赖检测,不造成常规 blocking,也不参与 deadlock。锁粒度取决于实际 plan:可能是 tuple、page 或 relation;资源紧张时还会提升到更粗粒度。因此索引/计划会影响 predicate-lock footprint 和 abort rate,但不能为了减少 40001 就盲目强制 index scan。

本章 Serializable 医生 case 在两个事务都读到 on_call=2 后捕获:

pg36-ch10-write-skew-ser-a / relation / SIReadLock / ch10_doctor
pg36-ch10-write-skew-ser-b / relation / SIReadLock / ch10_doctor

随后一事务提交,另一事务 40001,最终仍有一人值班。SSI 保证的是成功提交的集合可串行化;被 abort 事务里读到的任何结果都不能对外生效。

40001 是正常控制流,但不是无限重试许可

正确 handler:

BEGIN new transaction
  → obtain new snapshot
  → re-read every decision input
  → recompute
  → redo only idempotent/database-contained effects
COMMIT

还必须有:

  • max attempts;
  • total elapsed deadline;
  • exponential backoff + jitter;
  • cancellation/request deadline;
  • retry/abort metrics;
  • final error contract;
  • idempotency key;
  • 对外部副作用的隔离。

高 40001 比率不是“把 attempts 调大”。它可能表示事务太长、连接过多、热点冲突、predicate lock 过粗或数据模型缺少更自然的协调点。

除 40001 外,文档建议在某些应用中也把 40P01 deadlock failure 作为 whole-transaction retry 候选;23505/23P01 有时也可能与 serializable interleaving 有关,但它们通常首先是业务冲突,只有应用能基于完整协议判断是否重试。禁止“所有数据库错误都重试”。

read-only deferrable 的特殊用途

长时间一致性报表可以显式:

BEGIN TRANSACTION
ISOLATION LEVEL SERIALIZABLE
READ ONLY
DEFERRABLE;

它可能在开始读取前等待一个已证明安全的 snapshot;一旦取得,便可避免 serialization failure。它适合能接受启动等待的只读批处理,不适合低延迟请求,也不能包含写入。

选级别的顺序

不要从“统一把数据库设成 Serializable”开始。逐个 transaction family 记录:

问题 示例答案
invariant available 不得为负;至少一名医生值班
decision read set SKU row;所有 on-call rows
write set 同一 SKU;各自 doctor row
acceptable blocking 20 ms / 不允许
acceptable abort 可 40001 重试 3 次 / 不可
external effect 无 / payment API + message
chosen mechanism atomic update / Serializable + outbox

隔离级别只有与这张合同绑定,才是工程决定。

延伸阅读


返回本章目录 · 下一节:Lost update 不是一句口号 · 查看全书目录 · 查看索引中心