11.2 并发异常、调度与可串行化
两个窗口同时修改库存后,各自看都成功,合并结果却违反了档案城的业务规则。
元数据卡
- 前置:11.1 事务边界、ACID 与失败语义
- 关键词:并发异常、调度、冲突图、可串行化、重试
- 代码语言:SQL、调度记号
“两次查询结果不一样”只是表象。分析并发问题时,要同时记录谁读了什么、谁写了什么、修改是否提交,以及业务判断依赖了哪些行或范围。
先分清几种异常
设 R1(A) 表示事务 T1 读取 A,W2(A) 表示 T2 写入 A。
| 异常 | 关键条件 | 风险 |
|---|---|---|
| 脏读 | 读取另一事务尚未提交的写入 | 依据最终会回滚的数据做决定 |
| 脏写 | 覆盖另一事务尚未提交的写入 | 回滚次序无法保持已提交结果 |
| 不可重复读 | 再读同一行时看到另一事务已提交的新版本 | 一个事务中的判断基准漂移 |
| 幻读 | 再执行同一谓词查询时,满足条件的行集合改变 | 范围不变量失效 |
| 丢失更新 | 多个事务基于旧读值计算,再相互覆盖 | 一次业务修改悄然消失 |
| 写偏斜 | 各事务读取同一约束集合,却写入不同行 | 单行无冲突,集合约束仍被破坏 |
“丢失更新”不能只按隔离级别名称判断。下面两种写法的行为不同:
-- 数据库直接在当前行上做原子增量;并发写通常会串行等待
UPDATE counter SET value = value + 1 WHERE counter_id = 1;T1: SELECT value -> 10
T2: SELECT value -> 10
T1: UPDATE ... SET value = 11
T2: UPDATE ... SET value = 11第二种是应用端读—改—写,更容易丢更新。可用原子 SQL、SELECT ... FOR UPDATE、版本条件或 Serializable 保护,而不是笼统声称某级别“自动解决所有更新丢失”。
调度是操作的交错顺序
串行调度让 T1 的所有操作完成后才运行 T2。可串行化调度允许交错,但成功提交后的效果必须等价于某个串行顺序。
判断冲突可串行化时,两项操作只有在以下条件同时满足时才冲突:
- 属于不同事务;
- 访问同一逻辑数据项;
- 至少一项是写。
因此读—读不冲突,读—写、写—读、写—写会冲突。
用优先图判定冲突可串行化
考虑这个明确写出先后次序的调度:
R1(A) W1(A) R2(A) W2(B) R1(B) C1 C2逐对找冲突:
W1(A)在R2(A)之前,得到边 T1 → T2;W2(B)在R1(B)之前,得到边 T2 → T1。
优先图出现环 T1 → T2 → T1,所以它不是冲突可串行化调度。若图无环,对图做拓扑排序即可得到一个等价串行顺序。
不要只列出每个事务内部的操作而省略全局交错次序;那样无法确定边的方向,也就无法判环。
冲突可串行化不是全部理论
视图等价还要求:
- 读取初始值的事务相同;
- 每次读取所依赖的写入事务相同;
- 每个数据项的最终写入者相同。
存在带盲写的视图可串行化调度,不是冲突可串行化调度。实际系统更常围绕冲突依赖设计协议,因为冲突图判环直接,而一般的视图可串行化判定困难得多。
还要区分“冲突可串行化的历史”与 SQL 的 Serializable 隔离接口。数据库可以用严格两阶段锁,也可以像 PostgreSQL 一样用 SSI 检测危险的读写依赖;实现不一定在运行时构造本节这张完整教学图。
可恢复性与级联回滚
可串行化描述结果是否等价于串行执行,可恢复性描述提交顺序是否安全。若 T2 读取了 T1 的写入,T2 不应在 T1 之前提交;若 T1 最后回滚,T2 也必须回滚。
生产数据库通常禁止脏写和脏读,并让写锁持有到事务结束,以避免级联回滚。MVCC 则让普通读选择已提交的可见版本。
把重试当作事务接口的一部分
Serializable 不承诺每次尝试都提交,它承诺成功提交的事务具有串行语义。PostgreSQL 序列化失败的 SQLSTATE 是 40001;死锁失败通常是 40P01。应用应重试整个事务,而不是只重放最后一条 SQL。
repeat up to retry_limit:
begin
try:
execute all reads, decisions, and writes
commit
return success
catch retryable_transaction_error:
rollback
wait with bounded exponential backoff and jitter
fail or send to recovery path重试必须满足三个条件:
- 事务外副作用可幂等或延后到 outbox;
- 每次从头重新读取,不能复用上次失败事务中的判断;
- 有次数、总时限和监控,避免热点冲突造成无限重试风暴。
练习
给定调度:
R1(A) W2(A) R3(B) W1(B) R2(B) C1 C2 C3完成以下任务:
- 列出所有事务间冲突边;
- 判断图中是否有环;
- 若无环,给出一个拓扑序;若有环,指出构成环的边;
- 说明仅交换两个不冲突操作能否消除该环。
本节检查点
- 能按“是否提交、同一行还是谓词范围、读后写还是原子更新”描述异常。
- 能从完整调度构造优先图,而不是凭事务名称猜边。
- 知道可串行化不等于永不失败,能设计整事务重试边界。
下一章进入 MVCC,解释数据库如何为一次读取选择正确版本,以及同名隔离级别为什么在不同产品中表现不同。