跳至内容
10.1 隔离级别与可观察现象

10.1 隔离级别与可观察现象

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

请求级别PostgreSQL 实际语义普通快照仍可能发生
Read Uncommitted等同 Read Committed每条语句nonrepeatable read、phantom、serialization anomaly
Read Committed默认级别每条语句同上
Repeatable Readsnapshot isolation首个非事务控制语句取得事务快照serialization anomaly,例如 write skew
SerializableSerializable 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 记录:

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

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

延伸阅读


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

最后更新于