← [ BACK_TO_NOTES ]

BodySense 数据库第 12 课:Isolation Level 与并发异常

通过两个 PostgreSQL session 复现 Read Committed、Repeatable Read、Serializable 下的 non-repeatable read、phantom、lost update 与 serialization failure。

#course #growing #backend #database #postgresql #bodysense

[!info] related notes

BodySense 数据库第 12 课:Isolation Level 与并发异常

[!abstract] 本课目标 隔离级别不是四个定义题,而是:一个事务在并发世界里能看见什么、遇到冲突时谁等待、谁重试、谁失败。

从零建立概念:为什么 Atomic 还不够

假设每个 transaction 单独看都正确。

但两个 transaction 并发:

T1                T2
read X=10         read X=10
write X=9         write X=9
commit            commit

如果业务本来希望两次都减 1:

expected X=8
actual X=9

每个 transaction 自己都 Atomic,仍然出现并发错误。

这就是 Isolation 要解决的领域。

Concurrency 为什么难:你看到的数据库不一定是“此刻最新全局状态”

如果每条 SQL 都必须锁住全库:

one transaction at a time

最容易保证正确,但并发性能极差。

数据库要在:

correctness
↔
concurrency

之间设计协议。

PostgreSQL 的核心方法是:

MVCC snapshot
+
locks
+
isolation rules

Snapshot 是什么

可以先把 Snapshot 理解成:

这个 statement / transaction 用来判断“哪些 transaction 的结果对我可见”的视图。

它不是简单复制整个数据库。

而是记录足够的 transaction visibility 信息,让 PostgreSQL 判断每个 tuple version 是否可见。

Read Committed 与 Repeatable Read 最大差异之一,就在 snapshot 生命周期。

四个经典现象

Dirty Read

T1 writes X=100
but not committed

T2 reads X=100

T1 rollback

T2 读到了一个从未真正提交的状态。

PostgreSQL 的普通 isolation level 不允许 Dirty Read;即使请求 READ UNCOMMITTED,PostgreSQL 也按 Read Committed 处理。

Non-repeatable Read

T1 SELECT X → 10

T2 UPDATE X=20
T2 COMMIT

T1 SELECT X → 20

同一个 transaction 两次读取同一逻辑 row 得到不同 committed value。

PostgreSQL Read Committed 可以发生。

Phantom Read

不是“同一行值改变”,而是 predicate 结果集合变化。

T1: SELECT count(*) WHERE status='active' → 5

T2: INSERT another active row
T2: COMMIT

T1: same query → 6

PostgreSQL Repeatable Read 提供比 SQL 标准最低要求更强的 snapshot 行为,不允许 phantom read,但仍可能存在 serialization anomaly。

Serialization Anomaly / Write Skew

两个 transaction 都基于各自 snapshot 做出局部正确决定,最终组合结果却不等价于任何一种串行执行。

经典形状:

Rule: 至少一个 doctor on_call

T1 sees A+B on_call
→ turns A off

T2 sees A+B on_call
→ turns B off

both commit
→ no doctor on_call

没有简单的“同一行 lost update”,但跨行 invariant 被破坏。

Read Committed:Statement-level Snapshot

PostgreSQL 默认。

一条普通 SELECT:

sees committed data
as of query start
+ own previous writes

同一 transaction 下一条 SELECT 会拿新的 statement snapshot。

所以:

BEGIN
SELECT → 10

other tx COMMIT X=20

SELECT → 20
COMMIT

是允许的。

这对大多数 CRUD 已经很好,因为不会读 uncommitted dirty data,同时保持高并发。

Repeatable Read:Transaction-level Stable Snapshot

PostgreSQL Repeatable Read:

first meaningful statement
→ establish snapshot
→ transaction later reads same snapshot view

外部 transaction 后续 commit 的新版本,对这个 snapshot 不可见。

但是:

Stable snapshot 不等于所有业务 invariant 自动安全。

跨行 write skew / serialization anomaly 仍需考虑。

更新已被并发修改的 row 时,也可能得到:

could not serialize access due to concurrent update

应用必须重试整个 transaction。

Serializable:目标是“结果等价于某种串行顺序”

注意不是:

所有 transaction 真正一个接一个执行

PostgreSQL 使用 Serializable Snapshot Isolation (SSI):

允许并发执行
→ 跟踪危险 read/write dependency
→ 如果一组提交无法与任何 serial order 等价
→ abort one transaction

所以 Serializable 的代价不仅是“更慢”,还包括:

serialization failure is normal outcome
→ application retry required

Retry 为什么必须重跑整个 Transaction

假设 T1 在旧 snapshot 上:

read A
compute B
write C

Serializable 失败后不能只重试最后一个 UPDATE。

因为:

A 可能已经变化
→ B 应重新计算
→ C 也可能不同

所以:

abort
→ begin new transaction
→ repeat reads + decisions + writes

Isolation Level 不是越高越专业

选择要基于:

business invariant
conflict frequency
retry cost
lock/blocking cost
query complexity

有时:

Read Committed + UNIQUE

就比全局 Serializable 更清晰。

有时:

SELECT FOR UPDATE

能准确锁定热点 aggregate。

有时:

Serializable + transaction retry

能让复杂 predicate invariant 更容易证明。

不要把 isolation level 当作“安全等级拉满”的按钮。

1. PostgreSQL 的现实语义

PostgreSQL 常用:

Read Committed  → 默认;每条 statement 获得新的 snapshot
Repeatable Read → transaction 内稳定 snapshot,PostgreSQL 基于 MVCC
Serializable    → 最强隔离;可能主动 abort 事务要求重试

PostgreSQL 的 READ UNCOMMITTED 实际按 Read Committed 处理。

2. 并发异常不要只背名字

重点复现:

Non-repeatable read
Phantom / predicate change
Lost update / stale write
Write skew
Serialization failure

同名隔离级别在不同数据库实现上的具体行为可能不同,所以课程以 PostgreSQL 实验为准。

3. Read Committed 的一个关键直觉

同一 transaction 内:

SELECT A → snapshot 1
其他 transaction commit
SELECT A → snapshot 2

因此两次 SELECT 可能看到不同结果。

4. Serializable 并不等于“所有人排队串行执行”

PostgreSQL Serializable 可以允许并发执行,但检测到无法与某个 serial order 等价时,让某个事务失败并要求应用重试。

Prediction

[!question]

  1. PostgreSQL Read Committed 下同一 transaction 两次 SELECT 一定相同吗?
  2. Serializable 是否意味着不会出现错误、只会更慢?
  3. 提升 isolation level 是否能替代所有显式业务锁?
  4. serialization failure 应当被当作普通永久错误,还是可重试并发结果?
  5. 两个事务都“先检查 count < 1,再各自 INSERT”,单独行锁可能为什么不够?

[!success]- Answer / 解析 1. 不一定。 每条 statement 通常读取语句开始时的 committed snapshot。

2. 不是。 Serializable 为保持可串行化可能 abort 其中一个 transaction,应用必须准备 retry。

3. 不能机械替代。 有些 invariant 用行锁、唯一约束或 advisory lock 更直接;应按冲突对象设计。

4. 通常是可重试并发结果。 但重试必须重新执行整个 transaction,且外部副作用要幂等。

5. 冲突可能是 predicate/range 上的“尚不存在行”,没有现成 row 可锁;可考虑 UNIQUE/Serializable/advisory 等。

Lab

用两个 psql session 复现至少三个现象:

  1. Read Committed non-repeatable read;
  2. 一个 row lock wait;
  3. Serializable transaction 被 abort。

每个实验先预测 timeline 再执行。

L4 Gate

给一个并发 invariant,能选择 isolation/constraint/lock 中的主要保护机制,并写出一个能证伪自己判断的双 session 实验。

创建于 2026/9/18 更新于 2026/9/18
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本