Mysql-InnoDB 两次写(Doublewrite):为什么 Redo Log 救不了 “破损的页”
要理解 Doublewrite 的必要性,得先搞懂一个致命问题:Redo Log 能修复 “内容错误”,但治不了 “页结构错误”。而 InnoDB 的 “两次写” 机制,正是为了解决 “页结构被破坏” 的灾难(比如断电导致 16KB 数据页只写了 4KB)。
一、Redo Log 的 “能力边界”:只能修内容,不能修结构
Redo Log 记录的是 “对页的物理操作”(比如 “在页的偏移量 800 处写 'abc'”),但它有个前提:页本身必须是完整的。
如果发生 “部分页写入(partial page write)”—— 比如 16KB 的页只写了前 4KB 就断电,页的结构已经被破坏(剩下 12KB 是旧数据或乱码),这时 Redo Log 就像 “往破损的纸上写字”,根本无法恢复:
破损页的偏移量 “800” 可能已经不是原来的位置(因为页结构乱了);
即使强行应用 Redo Log,写入的 “abc” 也会被破损页的乱码覆盖,导致数据彻底错误。
二、Doublewrite 的核心逻辑:先写 “完整页副本”,再写数据文件
Doublewrite 的本质是 “给页买保险”—— 在写数据文件前,先把完整的页副本存到 “共享表空间的安全区”,这样即使写数据文件时断电,也能用副本还原页结构。
流程拆解(以 “修改某行数据” 为例):
修改缓冲池里的页:用户执行
UPDATE t SET name='Alice' WHERE id=1
,InnoDB 先在缓冲池里找到 id=1 所在的数据页,修改 name 为 'Alice'(此时页变成 “脏页”)。记录 Redo Log:把 “在页 X 的偏移量 100 处写 'Alice'” 的操作记录到 Redo Log(保证内容可恢复)。
复制到 Doublewrite Buffer(内存):用
memcpy
把整个脏页(16KB)复制到内存中的 Doublewrite Buffer(容量 2MB,可存 128 个页)。写入共享表空间的 Doublewrite 区域(磁盘):把 Doublewrite Buffer 里的页顺序写入系统表空间(ibdata)的连续区域(2MB,128 个页),然后调用
fsync
确保落盘。写入数据文件(磁盘):再把脏页写入表的.ibd 文件(可能是随机写,因为数据文件的页分布零散)。
三、崩溃恢复时的 “双保险”:先修结构,再补内容
如果在步骤 5(写数据文件)时断电,恢复流程是:
用 Doublewrite 副本还原页结构:从共享表空间的 Doublewrite 区域找到页 X 的完整副本,覆盖数据文件中破损的页 X。
用 Redo Log 补内容:应用 Redo Log 中 “在页 X 偏移量 100 处写 'Alice'” 的操作,确保数据正确。
四、为什么两次写是 “必须的”?
页大小与操作系统的 “原子写” 不匹配:InnoDB 页是 16KB,而操作系统(如 Linux)的页是 4KB,写 16KB 需要拆成 4 次 4KB 写入。如果在第 2 次写入时断电,页就会 “前 4KB 新,后 12KB 旧”,结构破损。
Redo Log 无法修复破损页:如前所述,Redo Log 是 “往页上写操作”,但页本身破损时,这些操作无效。
Doublewrite 的顺序写优化:共享表空间的 Doublewrite 区域是连续的 2MB 空间,写入时是顺序 IO(速度快);而数据文件的页是零散的(随机 IO,速度慢)。先写顺序的 Doublewrite,再写随机的数据文件,既保证可靠性,又没损失太多性能。
五、类比理解:给 “重要文件” 先备份再修改
假设你要修改一份 16 页的合同(对应 InnoDB 页),但打印机每次只能打 4 页(对应 OS 页)。为了防止打印到第 8 页时断电,导致合同前 8 页新、后 8 页旧(结构破损),你会:
先复印完整合同(对应写 Doublewrite Buffer);
再逐页修改原件(对应写数据文件);
若修改到第 8 页时断电,用复印件还原合同(Doublewrite 副本),再重新修改第 8 页(Redo Log)。
六、总结:Redo Log 和 Doublewrite 的分工
Redo Log:负责 “内容恢复”—— 记录页的具体修改,保证事务提交后数据不丢。
Doublewrite:负责 “结构恢复”—— 保存页的完整副本,解决 “部分页写入” 导致的结构破损。
两者配合,才让 InnoDB 在宕机后既能恢复数据内容,又能保证数据结构的完整性,成为 MySQL 最可靠的存储引擎之一。