深入 ACID 与事务隔离级别
在第二阶段的事务入门篇中我们认识了事务的基本概念学习了COMMIT和ROLLBACK并初步体验了脏读现象。那时我们留下了一个伏笔ACID 到底是如何实现的四种隔离级别背后发生了什么从本篇开始我们将进入 MySQL 最精彩的部分之一——事务进阶、锁机制与并发控制。本文作为第五阶段的开篇将系统性地回答ACID 四个特性在 InnoDB 中分别由哪些技术实现四种隔离级别的严格定义与区别脏读、不可重复读、幻读的本质与重现隔离级别的设置与选择实战亲手验证READ UNCOMMITTED下的脏读读完本文你将能够自信地解释“为什么 MySQL 能保证事务一致性”并根据业务需求灵活选择隔离级别。1. ACID 的实现原理谁在背后干活ACID 是事务正确性的四个基石但它们并非“天生就有”而是由 InnoDB 的多个组件协同实现的。1.1 原子性Atomicity—— Undo Log 负责原子性要求一个事务中的所有操作要么全部成功要么全部失败。已经执行了一半的操作必须能被撤销。InnoDB 通过Undo Log实现原子性在修改任何行之前先将旧值记录到 Undo Log 中。如果事务执行ROLLBACK就沿着 Undo Log 将数据恢复成旧版本。Undo Log 记录的是逻辑变化如“这行之前的值是 X”而不是物理页的字节翻转。1.2 持久性Durability—— Redo Log 负责持久性要求一旦事务提交其修改就永久保存哪怕系统崩溃。InnoDB 通过Redo Log WALWrite-Ahead Logging实现持久性修改数据页之前先把“做了什么修改”记录到 Redo Log并确保 Redo Log 落盘。事务提交时Redo Log 必须已经写入磁盘由innodb_flush_log_at_trx_commit控制。崩溃恢复时从最后一次 Checkpoint 开始重做 Redo Log恢复所有已提交的修改。1.3 隔离性Isolation—— 锁 MVCC 负责隔离性要求并发事务之间互不干扰。一个事务执行过程中不应该看到其他事务未提交的修改。InnoDB 通过锁机制Lock和多版本并发控制MVCC共同实现隔离性行锁写操作会对行加排他锁防止其他写操作同时修改同一行。MVCC读操作不加锁通过 Undo Log 构建的版本链和 ReadView 实现“无锁读”读到的是该事务应该看到的版本。不同隔离级别决定了 ReadView 的生成时机以及锁的粒度从而在并发性和一致性之间做出权衡。1.4 一致性Consistency—— 由其他三者 应用层保证一致性要求事务执行前后数据库必须处于一致状态所有约束都满足业务规则不被打破。一致性是 ACID 的最高层目标由原子性、隔离性、持久性以及数据库的约束主键、外键、CHECK 等和应用程序的业务逻辑共同实现。如果应用层写入的逻辑本身不正确比如转账时只扣钱不加钱数据库无法自动阻止因此一致性也依赖于应用开发者的正确实现。2. 四种事务隔离级别严格定义与区别SQL 标准定义了四种隔离级别依次更严格地解决三类并发问题。2.1 三类并发问题回顾在事务入门篇我们介绍过这里再严格定义一遍脏读Dirty Read事务 T1 读到了事务 T2未提交的修改数据。如果 T2 回滚T1 读到的就是“脏数据”。不可重复读Non-Repeatable Read事务 T1 内两次读取同一行数据结果不同。因为 T2 在 T1 两次读取之间修改并提交了该行。幻读Phantom Read事务 T1 内两次执行同一范围查询返回的行数不同。因为 T2 在两次查询之间插入或删除了符合条件的行并提交了。不可重复读关注的是行内容的修改幻读关注的是行数量的变化。2.2 四种隔离级别对比隔离级别脏读不可重复读幻读READ UNCOMMITTED可能可能可能READ COMMITTED不可能可能可能REPEATABLE READ不可能不可能可能InnoDB 通过 Next-Key Lock 防止大部分幻读SERIALIZABLE不可能不可能不可能READ UNCOMMITTED读未提交最宽松的级别。事务可以读到其他事务未提交的数据。几乎没有任何并发控制一致性极差极少在生产中使用。READ COMMITTED读已提交大多数数据库的默认级别如 Oracle、PostgreSQL。一个事务只能读到其他事务已提交的修改。解决了脏读但仍然存在不可重复读和幻读。InnoDB 在 RC 级别下每次快照读都会生成新的 ReadView因此可以在同一事务中看到不同时间的已提交数据。REPEATABLE READ可重复读MySQL InnoDB 的默认级别。同一个事务内的多次读取结果一致解决了不可重复读。对于幻读InnoDB 通过Next-Key Lock行锁 间隙锁在很大程度上进行了抑制但并不是完全不可能。在 RR 级别InnoDB 在事务第一次读取时生成一个 ReadView之后一直沿用这个视图因此总能看到事务开始时的数据快照。SERIALIZABLE可串行化最高隔离级别。所有事务串行执行通过加锁实现读操作也会加共享锁。完全避免了脏读、不可重复读和幻读但并发性能极差几乎不用。2.3 InnoDB 中 REPEATABLE READ 的“半”幻读理论上 RR 无法防止幻读但 InnoDB 通过Next-Key Lock将行锁和间隙锁组合使得其他事务无法在已查询的范围内插入新行。这阻止了典型的幻读场景比如SELECT ... FOR UPDATE的范围查询。然而如果先快照读再当前读或者在同一事务中先快照读、其他事务插入并提交当前事务再执行UPDATE可能影响原本看不到的行。这种“半幻读”现象偶尔会在特殊逻辑中遇到后续《幻读与 Next-Key Lock》篇会深入分析。3. 设置隔离级别-- 查看当前会话隔离级别SELECTtransaction_isolation;-- 查看全局隔离级别SELECTglobal.transaction_isolation;-- 设置当前会话的隔离级别SETSESSIONTRANSACTIONISOLATIONLEVELREADUNCOMMITTED;SETSESSIONTRANSACTIONISOLATIONLEVELREADCOMMITTED;SETSESSIONTRANSACTIONISOLATIONLEVELREPEATABLEREAD;SETSESSIONTRANSACTIONISOLATIONLEVELSERIALIZABLE;-- 设置全局重启后恢复为配置文件中的值SETGLOBALTRANSACTIONISOLATIONLEVELREADCOMMITTED;全局设置影响之后新建立的连接已有连接不受影响。生产环境更改需谨慎可以通过配置文件transaction-isolation永久设置。4. 实战验证 READ UNCOMMITTED 下的脏读我们来直观地感受一下脏读的危害。需要开启两个会话按照以下步骤操作。4.1 准备测试表CREATEDATABASEIFNOTEXISTSiso_test;USEiso_test;CREATETABLEaccount(idINTPRIMARYKEY,balanceINTNOTNULL)ENGINEInnoDB;INSERTINTOaccountVALUES(1,1000);4.2 会话 A修改但未提交SETSESSIONTRANSACTIONISOLATIONLEVELREADUNCOMMITTED;STARTTRANSACTION;UPDATEaccountSETbalance888WHEREid1;-- 不提交4.3 会话 B脏读SETSESSIONTRANSACTIONISOLATIONLEVELREADUNCOMMITTED;SELECT*FROMaccountWHEREid1;结果balance 888—— 会话 B 读到了会话 A 尚未提交的修改这就是脏读。4.4 会话 A 回滚ROLLBACK;4.5 会话 B 再次查询SELECT*FROMaccountWHEREid1;结果balance 1000—— 会话 B 之前读到的 888 变成“假数据”基于它的业务决策如扣款将导致严重错误。4.6 切换到 READ COMMITTED 避免脏读会话 B 将隔离级别改为READ COMMITTEDSETSESSIONTRANSACTIONISOLATIONLEVELREADCOMMITTED;重新执行上述步骤会话 B 在会话 A 未提交时读到的仍然是1000脏读消失。4.7 清理DROPDATABASEiso_test;5. 隔离级别选择指南在实际项目中隔离级别并非越高越好需要权衡一致性和并发性能REPEATABLE READInnoDB 默认兼顾数据一致性和并发性能适合大多数业务场景。不用担心不可重复读Next-Key Lock 也能防止大多数幻读。READ COMMITTED适合对一致性要求稍低、但需要更高并发的场景如互联网高并发写入。缺点是同一事务内两次读可能不同并且无法完全防止幻读。在基于语句的复制SBR中RC 可能导致主从数据不一致需要切换为基于行的复制RBR。READ UNCOMMITTED几乎不用。除非是纯只读且允许脏数据的场景极少。SERIALIZABLE极少使用仅在对数据一致性要求绝对严格且并发量极低的场景下考虑。6. 小结本文我们从实现原理和并发问题两个维度彻底吃透了 ACID 和隔离级别ACID 的实现Undo Log 保障原子性Redo Log WAL 保障持久性锁 MVCC 保障隔离性三者加上约束与业务逻辑共同实现一致性。三种并发问题脏读读未提交、不可重复读同一行被修改提交、幻读范围查询行数变化。四种隔离级别READ UNCOMMITTED问题全开放→ READ COMMITTED防脏读→ REPEATABLE READ防脏读和不可重复读→ SERIALIZABLE全防。InnoDB 的默认 RR通过 Next-Key Lock 在很大程度上抑制了幻读。通过实战亲手验证了READ UNCOMMITTED下的脏读现象并对比了READ COMMITTED的正确行为。事务的隔离性离不开锁。下一篇我们将深入到锁的分类表锁、行锁、页锁、意向锁以及共享锁与独占锁的区别为后续理解 Next-Key Lock 和 MVCC 打下基础。思考题如果在REPEATABLE READ隔离级别下事务 A 先读取了一条数据然后事务 B 修改并提交事务 A 再次读取该数据读到的是什么值为什么为什么SERIALIZABLE级别并发性能差它使用了什么机制尝试将你的测试环境隔离级别设为READ COMMITTED模拟一下不可重复读的场景两个会话交替读写。参考资料MySQL 8.0 Reference Manual - Transaction Isolation LevelsMySQL 8.0 Reference Manual - InnoDB Multi-VersioningMySQL 8.0 Reference Manual - ACID Model