MySQL中InnoDB事务底层原理大白话剖析
InnoDB 事务底层原理直观图解 大白话重剖析用比喻 图形把核心说得清楚。重点在Redo Log、Undo Log、MVCC、间隙锁、两阶段提交、双写缓冲。一、把事务想成“花钱买菜”你手机支付买菜整个过程像数据库事务步骤数据库对应保证1. 挑菜放进购物车DML 操作INSERT / UPDATE / DELETE-2. 点击“支付”提交事务COMMIT要么全成功要么全失败原子性3. 手机显示“支付成功”事务持久化了Redo Log即使手机关机记录也在持久性4. 支付时别人也在买并发控制MVCC 锁不影响彼此隔离性5. 如果最后发现没带钱回滚事务ROLLBACK购物车清空回到原样Undo Log二、InnoDB 事务底层三大核心┌─────────────────────────────────────┐ │ InnoDB 事务引擎 │ └─────────────────────────────────────┘ │ ┌─────────────────────┼─────────────────────┐ ▼ ▼ ▼ ┌─────────┐ ┌──────────┐ ┌──────────┐ │ Redo Log│ │ Undo Log │ │ Lock │ │(重做日志)│ │(回滚日志) │ │ MVCC │ └─────────┘ └──────────┘ └──────────┘ │ │ │ 持久性、崩溃恢复 原子性、回滚、MVCC 隔离性、高并发下面我们逐个拆解只讲最需要记住的。三、Redo Log —— “账本上的流水”保证持久性大白话比喻你买了 10 元菜但是没带现金老板说“你先记在账本上Redo Log回头再结”。即使你回家后忘了给钱老板翻账本就能找到这笔记录。→数据库也是先把变化记在 Redo Log 里再慢慢改真正的数据页。图形WAL预写日志流程事务开始 │ ▼ 修改内存数据页Buffer Pool │ ├─────────────────┐ │ │ ▼ ▼ 生成 Redo Log 生成 Undo Log (记录“改了什么”) (记录“原来是什么”) │ ▼ Redo Log 刷到磁盘 ←─── 这里先写日志再写数据 │ ▼ 事务提交成功 ✓ │ (后台慢慢做) ▼ 真正的数据页写到磁盘关键点WALWrite-Ahead Logging日志先行 – 数据可以晚点落盘但日志必须立刻落盘。刷盘参数innodb_flush_log_at_trx_commit 1最安全每次提交都写盘 → 性能最差但绝不多丢。 2折中写到 OS 缓存每秒刷一次 → MySQL 挂了不丢操作系统挂了可能丢。崩溃恢复MySQL 重启后根据 Redo Log 把已提交事务重做一遍。一句话记住 Redo Log“先记账后付款账本丢了就完蛋但只要账本在钱就不会少。”四、Undo Log —— “后悔药”保证原子性 MVCC大白话比喻你买菜时放了一颗白菜INSERT后来又不想买了。→ 老板根据“你一开始什么都没拿”的记录Undo Log把白菜收回去。更厉害的是别的顾客问你刚才拿了什么老板还能把“历史状态”告诉他们。版本链核心图解每一行数据背后都有三个隐藏字段┌────────────────────────────────────────────────┐ │ 字段1: DB_TRX_ID (最后修改它的事务ID) │ │ 字段2: DB_ROLL_PTR (指向旧版本的指针) ──────┐ │ │ 字段3: DB_ROW_ID (行ID主键不存在时用) │ │ └────────────────────────────────────────────────┘ │ ▼ ┌─────────────────┐ │ Undo Log 旧版本1 │ │ (修改前的值) │ │ DB_ROLL_PTR ──────┼──► 更老的版本2 └─────────────────┘例子原来id1的余额是 100。事务 AID100把它改成 200 → 旧版本 100 被记录到 Undo Log新数据trx_id100指向旧版本。事务 BID101又改成 300 → 旧版本 200 成为新 Undo指回版本 100。→ 版本链300 ← 200 ← 100。回滚沿着链表往回走ROLLBACK;InnoDB 根据DB_ROLL_PTR找到旧值一步恢复。Purge 线程垃圾回收事务提交后如果没有任何事务需要再看那个旧版本Purge 线程才真正删除 Undo Log。一句话记住 Undo Log“不改原数据而是挂个旧版本。后悔了顺着链子往回找别人读老版本也顺着链子读。”五、MVCC —— “睁眼快照”让读写互不打扰大白话图书馆里一本书有很多历史版本。当你开始看书事务开始时你就拍了一张快照Read View告诉你“你只能看到这个时间点之前已经出版的书”。即使别人后来改了书你看到的还是你原来那张快照。Read View 结构简化版┌─────────────────────────────────────┐ │ Read View { │ │ 最小活跃事务ID 80 │ │ 最大活跃事务ID1 200 │ │ 活跃事务ID列表 [80,95,102] │ │ 自己的事务ID 99 │ │ } │ └─────────────────────────────────────┘可见性判断你只需要记这一条“大白话规则”如果一个数据版本的事务 ID小于最小活跃事务ID→ 相当于“已经结账的老顾客”可见✅在活跃事务列表里 → “还没付钱的新顾客”不可见❌顺着 Undo 链找更老的版本等于自己 → 自己改的可见✅大于等于最大活跃事务ID1→ “未来的顾客”不可见❌隔离级别的区别重点中的重点MVCC仅在以下两个隔离级别下有用隔离级别Read View 生成时机效果READ COMMITTED每一条 SELECT 都生成新 Read View每次查询都能看到别人刚提交的修改 →不可重复读REPEATABLE READ事务的第一个 SELECT 生成一个 Read View一直用到结束整个事务看到的数据快照不变 →可重复读举例事务 T1 第一次查询余额100然后 T2 把它改成 200 并提交。RC 隔离级别T1 第二次查询看到 200 ❌ 不可重复读RR 隔离级别T1 第二次查询还是 100 ✅ 可重复读靠的是“老 Read View”六、间隙锁 —— “不要让鬼影出现”解决幻读幻读是什么事务 A 查询WHERE id BETWEEN 10 AND 20发现只有 2 行。事务 B 往这个区间插入了 id15。事务 A 再查发现多了一行 →像见了鬼一样幻读。间隙锁Gap Lock的解决方式InnoDB 在 RR 级别下不仅锁住存在的行还锁住行与行之间的空隙。图形例子表usersid 已有值 10、20、30。索引记录: 10 20 30 间隙: [负无穷,10) (10,20) (20,30) (30,正无穷] ↑ ↑ ↑ 间隙锁 间隙锁 间隙锁事务 A 执行SELECT * FROM users WHERE id15 FOR UPDATE;→ 因为 15 不存在InnoDB 会锁住间隙(10,20)。→ 其他事务插入 11~19 都会被阻塞所以再也看不到“凭空冒出的行”。一句话记住间隙锁“宁可锁住空地也不让新行偷偷溜进来。”⚠️ 注意只在REPEATABLE READ及以上级别生效READ COMMITTED没有间隙锁 → 可能幻读。七、两阶段提交 —— “让 Redo Log 和 Binlog 不打架”为什么会打架Redo LogInnoDB 自己的日志用于崩溃恢复。BinlogServer 层的日志用于主从复制和时间点恢复。如果写完 Redo Log 还没写 Binlog 就宕机从库可能少数据。如果先写 Binlog 再写 Redo Log 宕机主库恢复后少数据从库却多了。两阶段提交协议简化图┌─────────┐ ┌─────────┐ ┌─────────┐ │ Prepare │ ───────► │ Write │ ───────► │ Commit │ │ (Redo) │ │ Binlog │ │ (Redo) │ └─────────┘ └─────────┘ └─────────┘ 1 2 3步骤Redo Log 写入并标记为PREPARE状态。Binlog 写入磁盘。Redo Log 标记为COMMIT状态。崩溃恢复时的判断超级重要如果 Redo Log 是 PREPARE 状态且 Binlog 里有这条记录 →提交事务。如果 Redo Log 是 PREPARE 状态但 Binlog 里没有 →回滚事务。→ 确保主库和从库在恢复后数据完全一致。一句话记住两阶段提交“红黑两道Redo 和 Binlog必须保持一致预备-写盘-提交缺一不可。”八、Doublewrite Buffer —— “防止页写到一半断电”问题页断裂InnoDB 默认页大小 16KB操作系统一次写 4KB。假设写到第 3 个 4KB 时断电 → 磁盘上是一个撕裂的页一部分新数据一部分旧数据校验和错。解决方案双写缓冲脏页 (内存) │ ├──────────────────┐ │ │ ▼ ▼ 双写缓冲 (内存) 直接写数据文件(备用) │ ▼ 双写表空间 (磁盘连续两个区) │ ▼ 数据文件的实际位置流程脏页先写入内存中的双写缓冲区。一次性把缓冲区写到双写表空间磁盘上的固定位置。最后才写到各个数据文件的真实位置。崩溃恢复如果数据文件中的页损坏就从双写表空间里找一个干净的副本覆盖。一句话记住双写缓冲“先打草稿双写区再誊写数据文件草稿万一坏了不存在的。”九、一图总览 InnoDB 事务完整流程提交回滚并发控制MVCC Read View非锁定读行锁/间隙锁当前写操作事务开始执行DML/查询生成 Undo Log 保存旧版本修改 Buffer Pool 数据页生成 Redo Log 到 Log Buffer版本链形成事务提交?Redo Log Prepare 刷盘写 Binlog 并刷盘Redo Log Commit 刷盘释放锁, 事务完成根据 Undo Log 恢复数据回滚完成Purge 线程异步清理 Undo十、总结组件一句话精髓Redo Log先记账后付款崩溃后重放保证持久性。Undo Log每行都带个“旧版本指针”回滚和 MVCC 全靠它。MVCC事务开始时拍张快照Read View之后一直看这张老照片。间隙锁RR 级别下锁住虚空区间杜绝幻读。两阶段提交Redo 和 Binlog 之间通过“预备-提交”保证一致。双写缓冲先写专用草稿区防止页断裂。MySQL 从 3.23 版本开始引入对事务的支持从MySQL 4.0及以后的版本开始InnoDB成为了MySQL的默认存储引擎。对于大多数应用来说使用MySQL 5.6或更高版本并利用InnoDB存储引擎是既安全又高效的选择。其演进的核心就是从轻量、快速的 MyISAM 向 可靠、并发的 InnoDB 转变的过程。InnoDB 通过 Redo Log、Undo Log、MVCC 和精细的锁机制为 MySQL 构建了一套完整且强大的 ACID 事务保障体系。