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;不能先执行业务查询再把当前事务切到更高隔离级别。
写语句会等待并重新检查目标行
UPDATE、DELETE、SELECT ... 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_start、query_start、state 与 backend_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 |
隔离级别只有与这张合同绑定,才是工程决定。
延伸阅读
- PostgreSQL 18:Transaction Isolation
- PostgreSQL 18:Serialization Failure Handling
- PostgreSQL 18:
SET TRANSACTION - PostgreSQL 18:
pg_locks