跳到内容

11.2 并发异常、调度与可串行化

两个窗口同时修改库存后,各自看都成功,合并结果却违反了档案城的业务规则。


元数据卡

  • 前置:11.1 事务边界、ACID 与失败语义
  • 关键词:并发异常、调度、冲突图、可串行化、重试
  • 代码语言:SQL、调度记号

“两次查询结果不一样”只是表象。分析并发问题时,要同时记录谁读了什么、谁写了什么、修改是否提交,以及业务判断依赖了哪些行或范围。

先分清几种异常

R1(A) 表示事务 T1 读取 A,W2(A) 表示 T2 写入 A。

异常关键条件风险
脏读读取另一事务尚未提交的写入依据最终会回滚的数据做决定
脏写覆盖另一事务尚未提交的写入回滚次序无法保持已提交结果
不可重复读再读同一行时看到另一事务已提交的新版本一个事务中的判断基准漂移
幻读再执行同一谓词查询时,满足条件的行集合改变范围不变量失效
丢失更新多个事务基于旧读值计算,再相互覆盖一次业务修改悄然消失
写偏斜各事务读取同一约束集合,却写入不同行单行无冲突,集合约束仍被破坏

“丢失更新”不能只按隔离级别名称判断。下面两种写法的行为不同:

sql
-- 数据库直接在当前行上做原子增量;并发写通常会串行等待
UPDATE counter SET value = value + 1 WHERE counter_id = 1;
text
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。可串行化调度允许交错,但成功提交后的效果必须等价于某个串行顺序。

判断冲突可串行化时,两项操作只有在以下条件同时满足时才冲突:

  1. 属于不同事务;
  2. 访问同一逻辑数据项;
  3. 至少一项是写。

因此读—读不冲突,读—写、写—读、写—写会冲突。

用优先图判定冲突可串行化

考虑这个明确写出先后次序的调度:

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

text
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;
  • 每次从头重新读取,不能复用上次失败事务中的判断;
  • 有次数、总时限和监控,避免热点冲突造成无限重试风暴。

练习

给定调度:

text
R1(A)  W2(A)  R3(B)  W1(B)  R2(B)  C1  C2  C3

完成以下任务:

  1. 列出所有事务间冲突边;
  2. 判断图中是否有环;
  3. 若无环,给出一个拓扑序;若有环,指出构成环的边;
  4. 说明仅交换两个不冲突操作能否消除该环。

本节检查点

  • 能按“是否提交、同一行还是谓词范围、读后写还是原子更新”描述异常。
  • 能从完整调度构造优先图,而不是凭事务名称猜边。
  • 知道可串行化不等于永不失败,能设计整事务重试边界。

下一章进入 MVCC,解释数据库如何为一次读取选择正确版本,以及同名隔离级别为什么在不同产品中表现不同。

Built with VitePress | Software Systems Atlas