ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

MySQL redo 日志深度解析

2026/8/13 8:13:30 拓冰建站 浏览量
MySQL redo 日志深度解析 MySQL redo 日志深度解析说过的话就一定要办到摘要ATM 机已经提示转账成功但服务器下一秒就挂了钱到底去哪了本文从 MySQL 为什么要发明 redo 日志讲起用生活化的比喻带你理解 redo 日志的格式、Mini-Transaction、log buffer、LSN、checkpoint 与崩溃恢复彻底搞懂 InnoDB 是如何做到说过的话一定要办到的持久性承诺。一、为什么需要 redo 日志假设你去 ATM 机转账机器已经吐出小票说转账成功你满意地走了。可就在那一瞬间银行服务器啪的一声——断电了。等工程师修好机器重启数据库问题来了你的转账到底生效了没有这就是数据库持久性Durability要解决的问题一旦事务提交即使系统随后崩溃数据修改也不能丢失。最简单粗暴的做法是事务提交时把修改过的所有数据页从内存刷到磁盘。但这个做法有两个致命问题1.1 问题一刷新完整页面太浪费InnoDB 以页默认 16KB为单位管理磁盘空间。你可能只修改了一个字节但刷盘时必须把整个 16KB 的页写回磁盘。只改1个字节却要刷16KB 内存中的数据页16KB -------------------------------- | | | | | | |★| | | | | | | | | | ← 只改了这1个字节 -------------------------------- | v 却要刷整个16KB到磁盘 浪费 16KB - 1字节 ≈ 16KB1.2 问题二随机 IO 太慢一个事务可能修改多个页面而且这些页面在磁盘上并不相邻。把分散在磁盘各处的页面一个个刷回去产生大量随机 IO对机械硬盘来说是噩梦。随机 IO vs 顺序 IO 随机 IO直接刷数据页 页A在磁道1 → 页B在磁道100 → 页C在磁道20 磁头来回奔波慢 顺序 IO写 redo 日志 所有日志追加写到文件末尾 磁头几乎不动快那怎么办MySQL 的答案是别刷整个页只把改了什么记下来。二、redo 日志的本质只记改动不刷全页redo 日志重做日志的核心思想极其简单既然我们只要保证事务提交后修改不丢失那完全没必要在提交时就把所有修改过的页面刷盘。只需要记录一下我改了什么将来崩溃恢复时按记录重新做一遍就行。redo 日志 vs 直接刷数据页 直接刷数据页 UPDATE balance100 → 把包含这行的整个16KB页刷到磁盘 → 事务提交成功 写 redo 日志 UPDATE balance100 → 记一条 redo 日志 表空间1第200页偏移量1024处把值从200改成100 → 把这条 redo 日志刷到磁盘很小顺序写 → 事务提交成功redo 日志相比直接刷数据页的优势对比项直接刷数据页写 redo 日志写入量整个页16KB几十到几百字节IO 类型随机 IO顺序 IO速度慢快对事务提交速度的影响大小三、redo 日志的格式物理日志与逻辑日志3.1 通用结构一条 redo 日志本质上记录了对哪个页做了什么修改基本结构如下redo 日志通用格式 ----------------------------------------------- | type | space ID | page num | data | ----------------------------------------------- | | | | | | | └─ 具体修改内容 | | └─ 页号 | └─ 表空间ID └─ redo日志类型MySQL 5.7有53种之多3.2 简单 redo 日志物理日志如果修改非常简单比如只是在页面的某个偏移量处改了几个字节redo 日志直接记录在哪个偏移量改了什么值。简单 redo 日志示例MLOG_8BYTE 场景更新 Max Row ID占8字节 redo 日志内容 type MLOG_8BYTE space ID 0系统表空间 page number 7 offset 某个偏移量 data 新的8字节值 含义在系统表空间第7页偏移量XXX处写入8字节的新值根据写入字节数不同分为MLOG_1BYTE写 1 字节MLOG_2BYTE写 2 字节MLOG_4BYTE写 4 字节MLOG_8BYTE写 8 字节MLOG_WRITE_STRING写一串数据带 len 字段3.3 复杂 redo 日志物理 逻辑但实际情况远比改几个字节复杂。比如向 B 树插入一条记录不仅要写入记录本身还要更新Page Directory 的槽信息Page Header 中的各种统计信息PAGE_N_DIR_SLOTS、PAGE_HEAP_TOP、PAGE_N_HEAP 等上一条记录的 next_record 指针可能还要做页分裂如果每个改动都记一条物理日志redo 日志量可能比整个页面还大如果记第一个改动的字节到最后一个改动的字节之间的所有数据又会包含大量未改动的数据依然浪费。于是 InnoDB 设计了更高级的 redo 日志类型比如MLOG_COMP_REC_INSERT插入一条紧凑行格式的记录MLOG_COMP_REC_DELETE删除一条记录MLOG_COMP_PAGE_CREATE创建一个新页面这些日志的特点是既包含物理信息哪个表空间的哪个页又包含逻辑信息调用哪个函数来恢复。复杂 redo 日志MLOG_COMP_REC_INSERT的含义 物理层面 → 指定了表空间ID和页号 逻辑层面 → 记录了插入记录所需的参数字段长度、偏移量等 → 崩溃恢复时调用插入记录函数传入这些参数 → 函数执行后Page Header、Page Directory 等自然就被恢复了 而不是直接记录 PAGE_N_DIR_SLOTS 5 PAGE_HEAP_TOP 0x2F0 ...那样太啰嗦了一句话总结简单 redo 日志是物理日志直接记录改哪几个字节复杂 redo 日志是逻辑日志记录调用什么函数、传什么参数。InnoDB 根据场景选择最合适的格式核心目标只有一个省空间。四、Mini-Transaction原子性的保证4.1 为什么需要原子性向 B 树插入一条记录有时只需要改一个叶子页乐观插入有时却需要分裂页面、新建页面、在内节点添加目录项记录悲观插入。无论哪种情况这些对页面的修改必须是原子的——要么全做完要么全没做。否则 B 树的结构就会被破坏。乐观插入 vs 悲观插入 乐观插入 页b有空位 ------------------ | 1 | 3 | 5 | | | | ← 插入10 ------------------ → 直接在页b里插入产生少量redo日志 悲观插入 页b满了 ------------------ | 1 | 3 | 5 | 7 | 9 | 11| ------------------ → 分配新页c → 把部分记录搬到页c → 在新页插入目标记录 → 修改链表指针 → 在内节点添加目录项 → 产生大量redo日志4.2 Mini-TransactionmtrInnoDB 把对底层页面的一次原子访问过程称为一个Mini-Transaction简称 mtr。一个 mtr 内产生的所有 redo 日志是一个不可分割的整体——崩溃恢复时要么全部恢复要么全部不恢复。如何实现这个原子性InnoDB 用了一个很聪明的办法多 redo 日志的情况在最后一 redo 日志后面加一条特殊的MLOG_MULTI_REC_END日志作为结束标记。恢复时只有看到这条结束标记才认为这组日志是完整的。mtr 的 redo 日志组 redo 日志1 redo 日志2 redo 日志3 ... redo 日志N MLOG_MULTI_REC_END ← 结束标记 崩溃恢复时 看到 MLOG_MULTI_REC_END → 整组恢复 ✅ 没看到结束标记 → 说明事务崩溃时没写完整组丢弃 ❌单条 redo 日志的情况利用 type 字段的第一个比特位。如果为 1表示这条日志本身就是一个完整的 mtr。事务、mtr、redo 日志的层级关系 事务Transaction | -- 语句1 | -- mtr_1 → redo日志组A | -- mtr_2 → redo日志组B | -- 语句2 | -- mtr_3 → redo日志组C | -- COMMIT 一个事务 多个语句 多个 mtr 多组 redo 日志五、redo log buffer日志先写内存5.1 redo log blockredo 日志在内存中以512 字节为单位组织成一个个block块就像这样redo log block 结构512 字节 ---------------------------------------------------------- | block header | block body | block trailer | | (12 字节) | (496 字节) | (4 字节) | ---------------------------------------------------------- | | | | | └─ 校验和 | └─ 真正存储 redo 日志的地方 └─ HDR_NO、DATA_LEN、FIRST_REC_GROUP、CHECKPOINT_NO字段含义LOG_BLOCK_HDR_NOblock 的唯一编号LOG_BLOCK_HDR_DATA_LEN已使用了多少字节初始 12写满为 512LOG_BLOCK_FIRST_REC_GROUP第一个 mtr 日志组在此 block 中的偏移量LOG_BLOCK_CHECKPOINT_NOcheckpoint 序号后面讲LOG_BLOCK_CHECKSUM校验和5.2 redo log buffer服务器启动时会申请一大片连续内存空间称为redo log buffer重做日志缓冲区默认大小16MB。它被划分成若干个连续的 512 字节 block。redo log buffer 结构 ------------------------------------------------------- | block 0 | block 1 | block 2 | block 3 | ... | block N | ------------------------------------------------------- ↑ | buf_free ← 全局变量指向下一个可写入的位置5.3 mtr 如何写入 log buffer关键点不是每产生一条 redo 日志就写一次 buffer而是等一个 mtr 结束时把整组日志一次性写入 log buffer。这样能保证 mtr 的日志在 buffer 中是连续的。不同事务的 mtr 可能是交替执行的所以 log buffer 里会交替出现不同事务的 mtr 日志log buffer 中的内容简化示意 -------------------------------------------------------- |mtr_T1_1|mtr_T2_1| mtr_T1_2跨3个block |mtr_T2_2| -------------------------------------------------------- 不同事务的 mtr 交替写入但同一个 mtr 的日志是连续的六、redo 日志刷盘与文件组6.1 什么时候刷盘log buffer 里的 redo 日志并不会一直在内存里待着以下情况会触发刷盘redo 日志刷盘时机 1. log buffer 满了约50%满时就开始刷 | 2. 事务提交时最重要为了保证持久性 | 3. 将某个脏页刷新到磁盘前 → 必须先保证该脏页对应的 redo 日志已经刷盘 | 4. 后台线程每秒刷一次 | 5. 正常关闭服务器时 | 6. 做 checkpoint 时后面详讲6.2 redo 日志文件组磁盘上的 redo 日志默认存储在数据目录下文件名为ib_logfile0、ib_logfile1。它们组成一个日志文件组循环使用。redo 日志文件组循环写 ib_logfile048MB ib_logfile148MB ---------------- ---------------- | 管理信息(2KB) | | 管理信息(2KB) | ---------------- ---------------- | redo日志... | ──→ | redo日志... | | | | | ---------------- ---------------- │ │ └───────────────────────┘ 写满后回到开头循环写 总容量 innodb_log_file_size × innodb_log_files_in_group 48MB × 2 96MB默认6.3 日志文件格式每个 redo 日志文件的前2048 字节4 个 block是管理信息block作用block 0log file header记录 redo 日志版本、起始 LSN、创建者等block 1checkpoint1记录 checkpoint 信息block 2未使用block 3checkpoint2和 checkpoint1 结构相同交替写入从第 2048 字节往后就是真正的 redo 日志 block 镜像了。七、LSNredo 日志的年龄7.1 什么是 LSNLSNLog Sequence Number是 InnoDB 用来标记已经产生了多少 redo 日志的全局变量可以理解为 redo 日志的累计年龄。初始值为8704。LSN 的增长 初始LSN 8704 mtr_1 产生 200 字节 redo 日志 → LSN 8704 12(block header) 200 4(block trailer) 8920 mtr_2 产生 1000 字节 redo 日志跨2个block → LSN 8920 1000 12×2 4×2 9944 注意LSN 增长量 实际日志字节 新占用的 block header/trailer7.2 flushed_to_disk_lsn与lsn对应还有一个flushed_to_disk_lsn表示已经刷新到磁盘的 redo 日志量。lsn vs flushed_to_disk_lsn 内存log buffer 磁盘redo日志文件 ------------------- ------------------- | mtr_1 日志 | ───已刷──→ | | | mtr_2 日志 | ───已刷──→ | | | mtr_3 日志 | ──未刷──→ | | ------------------- ------------------- lsn 10000已写入 buffer 的总量 flushed_to_disk_lsn 9948已刷盘的总量 差值 52还在 buffer 里没刷盘的日志当lsn flushed_to_disk_lsn时说明 log buffer 中的所有 redo 日志都已安全落盘。7.3 flush 链表中的 LSNBuffer Pool 中的脏页会按修改时间顺序挂在flush 链表中每个控制块记录两个重要属性属性含义oldest_modification第一次修改该页时的 LSNnewest_modification最近一次修改该页时的 LSNflush 链表与 LSN flush 链表按 oldest_modification 排序 头部 ← 页d(newest10000, oldest9948) ← 页c(9948, 8916) ← 页b(10000, 8916) ← 页a(8916, 8716) → 尾部 修改较晚 修改最早八、checkpoint空间不够了怎么办8.1 为什么需要 checkpointredo 日志文件组的容量是有限的比如默认只有 96MB不可能无限增长。当写到最后一个文件的末尾时必须回到第一个文件的开头继续写——这就是循环写。但如果直接覆盖就会丢失旧的 redo 日志。问题是这些旧的 redo 日志还需要吗如果某条 redo 日志对应的脏页已经刷新到磁盘了那么这条 redo 日志就没用了——因为崩溃恢复时可以直接从磁盘读取最新页面不需要 redo 日志来重建。所以 InnoDB 需要知道哪些 redo 日志可以安全地覆盖8.2 checkpoint 的原理checkpoint检查点的核心逻辑找到 flush 链表中最早修改的脏页尾节点读取它的oldest_modification所有 LSN 小于这个值的 redo 日志对应的脏页都已经刷盘了可以被覆盖把这个值记录为checkpoint_lsn并写入日志文件的管理信息checkpoint 过程 步骤1找到最早修改的脏页 flush 链表尾部 → 页aoldest_modification 8716 步骤2设置 checkpoint_lsn 8716 → LSN 8716 的 redo 日志都可以被覆盖 步骤3将 checkpoint 信息写入 ib_logfile0 的管理信息 → checkpoint_lsn 8716 → checkpoint_offset 该 LSN 对应的文件偏移量 → checkpoint_no 1 checkpoint_no 为偶数 → 写入 checkpoint1 checkpoint_no 为奇数 → 写入 checkpoint2双保险8.3 checkpoint 后的 LSN 关系checkpoint 后的 redo 日志文件状态 redo 日志文件组 可覆盖区 checkpoint_lsn 需保留区 ├──────────┤←──8716──┤←──────────────────────────→┤ ↑ 从这里开始恢复 | LSN 8716 之前的 redo 日志对应的脏页都已刷盘可以被覆盖 LSN 8716 之后的 redo 日志可能还有用必须保留8.4 查看系统 LSN 状态SHOWENGINEINNODBSTATUS\G-- 在输出中可以看到Log sequence number124476971-- 当前 lsnLog flushed upto124099769-- flushed_to_disk_lsnPages flushed upto124052503-- flush 链表尾 oldest_modificationLastcheckpointat124052494-- checkpoint_lsn九、持久性的取舍innodb_flush_log_at_trx_commit事务提交时刷 redo 日志到磁盘能保证持久性但也会带来性能开销。如果你愿意在极端情况下的数据安全和性能之间做取舍可以调整innodb_flush_log_at_trx_commit参数innodb_flush_log_at_trx_commit 三种模式对比 ┌──────────┬────────────────────────────────────────────────────────┐ │ 值 │ 行为 │ ├──────────┼────────────────────────────────────────────────────────┤ │ 0 │ 事务提交时不刷盘交给后台线程每秒刷一次 │ │ │ ⚠️ 崩溃时可能丢失最近1秒的数据 │ │ │ 性能最好 │ ├──────────┼────────────────────────────────────────────────────────┤ │ 1 │ 事务提交时立即调用 fsync 刷到磁盘默认 │ │ │ ✅ 完全保证持久性 │ │ │ 性能最差每次提交都要等磁盘IO │ ├──────────┼────────────────────────────────────────────────────────┤ │ 2 │ 事务提交时写到操作系统缓冲区但不强制刷盘 │ │ │ ⚠️ MySQL 挂了数据还在操作系统/机器挂了会丢失 │ │ │ ⚡ 性能较好 │ └──────────┴────────────────────────────────────────────────────────┘-- 查看当前设置SELECTinnodb_flush_log_at_trx_commit;-- 修改仅当前会话SETSESSIONinnodb_flush_log_at_trx_commit2;-- 修改全局SETGLOBALinnodb_flush_log_at_trx_commit2;生产建议对数据一致性要求高的场景金融、支付用1对性能要求高、可接受秒级数据丢失的场景日志、统计用2或0。十、崩溃恢复重启后如何重建数据redo 日志平时是个累赘但数据库崩溃时它就是救命稻草。10.1 确定恢复起点checkpoint_lsn重启时InnoDB 从 redo 日志文件组的管理信息中读取checkpoint1和checkpoint2比较两者的checkpoint_no取较大的那个代表最近的 checkpoint。从中得到checkpoint_lsn——这就是恢复的起点。确定恢复起点 checkpoint1: checkpoint_no 100, checkpoint_lsn 50000 checkpoint2: checkpoint_no 101, checkpoint_lsn 80000 ← 更新 恢复起点 80000checkpoint_lsn → 只需恢复 LSN 80000 的 redo 日志10.2 确定恢复终点redo 日志是顺序写的。最后一个 block 的LOG_BLOCK_HDR_DATA_LEN字段如果不等于 512说明这个 block 没有写满它就是恢复的终点。确定恢复终点 block N: DATA_LEN 512写满 block N1: DATA_LEN 340没写满← 恢复终点 block N2: DATA_LEN ???可能是空的不用管10.3 怎么恢复方法1用哈希表加速将需要恢复的 redo 日志按space IDpage number做哈希相同的页面日志放到同一个槽里用链表按 LSN 顺序连接。哈希表加速恢复 槽0 → 页a的redo日志LSN 80001 → 80015 → 80030 槽1 → 页b的redo日志LSN 80005 → 80020 槽2 → 页c的redo日志LSN 80010 | v 遍历哈希表逐个页面恢复 好处同一个页面的日志一次性处理减少随机IO方法2跳过已经刷盘的页面checkpoint_lsn 之后的 redo 日志对应的脏页可能已经被后台线程刷盘了。怎么判断每个数据页的File Header中有一个FIL_PAGE_LSN属性记录最近一次修改该页时的 LSN。如果FIL_PAGE_LSN redo 日志的 LSN → 说明该页在崩溃前已经刷盘这条 redo 日志不用重放如果FIL_PAGE_LSN redo 日志的 LSN → 说明该页没刷盘需要重放这条 redo 日志跳过已刷盘页面的判断 redo 日志页aLSN 85000把某值改为100 | v 读取磁盘上的页a查看 FIL_PAGE_LSN | ├── FIL_PAGE_LSN 90000 85000 │ → 页a在崩溃前已经刷盘跳过这条redo日志 ✅ | └── FIL_PAGE_LSN 80000 85000 → 页a没刷盘执行 redo 日志恢复 ✅十一、总结与实战速查11.1 redo 日志核心流程图完整的数据修改与 redo 日志流程 1. 执行 UPDATE/INSERT/DELETE | v 2. 在 Buffer Pool 中修改数据页 | v 3. 生成 redo 日志mtr 过程中暂存 | v 4. mtr 结束 → 将一组 redo 日志写入 log buffer | v 5. 事务提交 → 根据 innodb_flush_log_at_trx_commit 设置刷盘 | v 6. 后台线程持续将脏页刷到数据文件 | v 7. checkpoint → 标记可以覆盖的旧 redo 日志11.2 核心概念速查表概念一句话解释redo 日志记录对哪个页做了什么修改用于崩溃后恢复物理日志直接记录偏移量X处改为Y如 MLOG_8BYTE逻辑日志记录调用什么函数、传什么参数如 MLOG_COMP_REC_INSERTmtrMini-Transaction对页面的一次原子访问产生一组 redo 日志MLOG_MULTI_REC_ENDmtr 日志组的结束标记log bufferredo 日志的内存缓冲区默认 16MBlog block512 字节的 redo 日志存储单元ib_logfile0/1磁盘上的 redo 日志文件默认 2 个LSN日志序列号标记已产生的 redo 日志总量初始 8704flushed_to_disk_lsn已刷新到磁盘的 redo 日志量checkpoint_lsn可以安全覆盖的 redo 日志边界oldest_modification脏页第一次被修改时的 LSNnewest_modification脏页最近一次被修改时的 LSN11.3 关键参数速查参数名默认值含义innodb_log_buffer_size16MBredo log buffer 大小innodb_log_file_size48MB单个 redo 日志文件大小innodb_log_files_in_group2redo 日志文件个数innodb_flush_log_at_trx_commit1事务提交时 redo 刷盘策略11.4 常见面试题Q1redo 日志和 binlog 有什么区别Aredo 日志是 InnoDB 引擎层的物理/逻辑日志用于崩溃恢复binlog 是 MySQL Server 层的逻辑日志用于主从复制和 point-in-time 恢复。redo 日志是循环写的binlog 是追加写的。Q2为什么 redo 日志比刷数据页快A1redo 日志量小只记改动2redo 是顺序 IO追加写刷数据页是随机 IO页分散在磁盘各处。Q3事务提交时 redo 日志一定刷盘吗A取决于innodb_flush_log_at_trx_commit。为 1 时一定刷盘fsync为 0 时不刷盘靠后台线程为 2 时写到 OS 缓存但不保证刷盘。Q4checkpoint 是做什么的A解决 redo 日志文件空间有限的问题。把对应脏页已刷盘的 redo 日志标记为可覆盖实现循环使用。延伸阅读MySQL 官方文档InnoDB Redo Log