BodySense 数据库第 12 课:Isolation Level 与并发异常
通过两个 PostgreSQL session 复现 Read Committed、Repeatable Read、Serializable 下的 non-repeatable read、phantom、lost update 与 serialization failure。
[!info] related notes
- 课程 MOC:BodySense 数据库工程深化课程 MOC
- 前置:第 11 课
- 知识:事务隔离级别
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]
- PostgreSQL Read Committed 下同一 transaction 两次 SELECT 一定相同吗?
- Serializable 是否意味着不会出现错误、只会更慢?
- 提升 isolation level 是否能替代所有显式业务锁?
- serialization failure 应当被当作普通永久错误,还是可重试并发结果?
- 两个事务都“先检查 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 复现至少三个现象:
- Read Committed non-repeatable read;
- 一个 row lock wait;
- Serializable transaction 被 abort。
每个实验先预测 timeline 再执行。
L4 Gate
给一个并发 invariant,能选择 isolation/constraint/lock 中的主要保护机制,并写出一个能证伪自己判断的双 session 实验。