跳转到内容

ACID 与隔离级别

ACID 四个字母里,隔离性(Isolation)是最贵的一个。本章讲四性的精确含义、并发事务的经典异常、四个隔离级别与快照隔离,以及"快照隔离不等于可串行化"这个最重要的坑(→ 并发控制)。

  • 原子性(Atomicity):事务要么全做、要么全不做——由 undo 日志实现(→ 日志与恢复);
  • 一致性(Consistency):事务把数据库从一个一致状态带到另一个——由约束与应用语义保证;
  • 隔离性(Isolation):并发事务互不干扰——由并发控制实现,本章主角;
  • 持久性(Durability):提交后崩溃也不丢——由 redo 日志与 fsync 实现(→ 日志与恢复)。

"一致性"在 ACID 与 CAP 中含义不同(→ 故障模型与 CAP):ACID 的 C 是业务约束的一致(约束满足),CAP 的 C 是线性一致性(多副本像单机)——同词不同义,讨论时先定义。

两个并发事务(读写交错)的经典异常:

异常定义例子
脏读读到未提交的数据T2 读到 T1 写入但随后回滚的值
不可重复读两次读同一行值不同T2 在两次读之间提交了更新
幻读两次范围查询行集不同T2 插入了新行,出现在 T1 的第二次查询
写偏斜(write skew)各自写不冲突的行,合起来破坏约束值班医生互相请假:都读"对方在值班",都写自己请假

脏读、不可重复读、幻读在快照隔离下都消失;写偏斜是快照隔离独有的残留(见下)。

级别脏读不可重复读幻读实现
读未提交(RU)可能可能可能不加读锁
读已提交(RC)不会可能可能读已提交版本
可重复读(RR)不会不会可能*快照(事务首读)
可串行化不会不会不会锁或 SSI

* MySQL InnoDB 的 RR 用间隙锁把幻读也防了;PostgreSQL 的 RR 就是快照隔离(有写偏斜)。

默认隔离级别因数据库而异:Oracle/PostgreSQL 默认 RC,MySQL InnoDB 默认 RR,Postgres 的 SERIALIZABLE 是 SSI——跨数据库时别假设默认级别相同。

快照隔离(snapshot isolation):每个事务看到一致的历史快照(MVCC,→ 并发控制),写入时若与并发提交冲突则回滚(first-updater-wins)。读永远不加锁,性能好。

快照隔离不等于可串行化:写偏斜穿透它。值班医生的例子:两个事务读到的快照都是"对方在值班",各自写自己(写的行不同,不冲突),结果两人都走了——约束被破坏,但每个事务单独看都正确。写偏斜 = 读旧快照做出的决定 + 写入不同的行。

根治是可串行化:

  • 两阶段锁(S2PL):读加锁、写加锁到提交——性能差;
  • SSI(可串行化快照隔离):PostgreSQL 的做法——快照隔离 + 检测"危险结构"(读写依赖成环),发现即回滚一个——性能比锁好得多。

大多数数据库默认级别并非可串行化——这意味着:

  • 默认级别下,写偏斜类 bug 是你的业务逻辑要自己防的(约束在应用层检查、或显式加锁/升级级别);
  • REPEATABLE READ 的名字有误导:它防的是"同一行两次读不同",不防写偏斜与(某些实现的)幻读;
  • 高正确性要求(余额、库存、值班表)要显式选择:可串行化级别、或 SELECT ... FOR UPDATE 显式锁。
  • 原子性靠 undo 日志、持久性靠 redo 日志;隔离性是最贵的一性
  • 异常四件套:脏读、不可重复读、幻读、写偏斜;前三个在快照隔离下消失
  • 快照隔离不等于可串行化(写偏斜):读旧快照 + 写不同的行
  • 默认隔离级别因数据库而异;"一致性"在 ACID 与 CAP 中同词不同义
  • 高正确性要求显式选择:SERIALIZABLE / SSI / FOR UPDATE