# 隔离级别与可观察现象

LLMS 索引： [llms.txt](/llms.txt)

---

隔离级别描述“并发事务成功提交后允许出现什么结果”，不是给单条查询加一层缓存。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 的语句快照 {#item-10-1-1}

### 每条普通查询重新取 snapshot

默认 Read Committed 下，一条普通 `SELECT` 看见：

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

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

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

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

查看当前事务设置：

```sql
SHOW transaction_isolation;
SELECT current_setting('transaction_isolation');
```

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

```sql
BEGIN ISOLATION LEVEL READ COMMITTED;
-- work
COMMIT;
```

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

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

`UPDATE`、`DELETE`、`SELECT ... FOR UPDATE/SHARE` 搜索候选时使用语句 snapshot，但候选行可能已被并发事务修改。PostgreSQL 会等待先行 writer：

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

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

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

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

```sql
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 不会回滚：

```sql
SELECT nextval('order_id_seq');
ROLLBACK;
```

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

## 10.1.2 Repeatable Read 的事务快照与写冲突 {#item-10-1-2}

### snapshot 从首个真正语句开始

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

```text
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 不会把旧计算静默覆盖到新版本，而是：

```text
SQLSTATE 40001
could not serialize access due to concurrent update
```

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

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

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

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

### 稳定快照仍允许 write skew

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

```text
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。

因此：

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

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

## 10.1.3 Serializable、谓词冲突与序列化失败 {#item-10-1-3}

### SSI 不把所有读变成阻塞锁

PostgreSQL Serializable 在 Repeatable Read snapshot 上增加 Serializable Snapshot Isolation（SSI）依赖检测。它跟踪：

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

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

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

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

```text
mode = SIReadLock
```

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

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

```text
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：

```text
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 的特殊用途

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

```sql
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](https://www.postgresql.org/docs/18/transaction-iso.html)
- [PostgreSQL 18：Serialization Failure Handling](https://www.postgresql.org/docs/18/mvcc-serialization-failure-handling.html)
- [PostgreSQL 18：`SET TRANSACTION`](https://www.postgresql.org/docs/18/sql-set-transaction.html)
- [PostgreSQL 18：`pg_locks`](https://www.postgresql.org/docs/18/view-pg-locks.html)

---

[返回本章目录](../) · [下一节：Lost update 不是一句口号](../02/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
