InnoDB Redo Log持久化解析
背景介绍
预写日志(WAL:Write-Ahead Logging)是数据库最重要的组件之一,用于保证数据操作的原子性和持久性。WAL(在InnoDB中称为Redo Log)中记录了所有对数据页的修改操作,所有的修改在提交之前都要先写入 Log 文件中,如此既可以延迟修改后的页面刷新到磁盘的时间,又可以防止数据丢失。
当处理密集型工作负载的写入Redo Log操作时,性能会受到许多用户线程同步操作的限制,这一点在多CPU和存储设备性能较快的场景下尤其明显。
本章节将重点介绍MySQL 8.0版本的Redo Log持久化流程,主要描述Redo Log的优化点以及相关线程的交互处理逻辑。
基本流程
用户事务在更改数据时,一个数据页修改操作可能会包含多条Redo Log,每条Redo Log分配全局唯一递增的LSN(Log Sequence Number),用于标识该日志记录的顺序。
每一条Redo Log都由mini-transaction(简称为mtr)原子提交,mtr是MySQL内部用于执行数据更改操作的最小单位。在数据页上执行更改动作时,MySQL会创建若干个mtr来确保操作的原子性。随之生成的Redo Log会暂时存放在mtr的m_log中,这是一个动态分配的内存空间。
当mtr提交时,系统首先将m_log中的Redo Log拷贝至Redo Log Buffer,随后更新Log Buffer LSN 位置,最后根据参数 innodb_flush_log_at_trx_commit 的配置唤醒后台线程执行持久化操作。
- log_Writer负责将 Redo Log Buffer 中的内容写入 OS Cache。
- 写入完成后,Log Writer线程向前推进write_lsn,并唤醒Log Flusher和Log_Writer Notifier线程。
- Log Flusher负责Redo Log的刷脏,与数据脏页的Flush无关,数据脏页的Flush由Buffer Pool刷脏线程处理。当flushed_to_disk_lsn落后于write_lsn时,Log Flusher线程会执行fsync()函数,将OS Cache中的数据刷入磁盘,从而完成Redo Log的真正刷盘。
- 刷盘完成后,Log Flusher线程向前推进flushed_to_disk_lsn,并唤醒Log Flush Notifier线程。
- Log Write Notifier/Log Flush Notifier线程会唤醒等待write_lsn/flushed_to_disk_lsn的用户线程。MySQL通过innodb_flush_log_at_trx_commit参数控制事务提交的时机,并决定是由Log Write Notifier还是Log Flush Notifier线程来唤醒用户线程。
- 用户线程被唤醒后,会比较自身事务的commit_lsn与write_lsn/flushed_to_disk_lsn:
- 如果commit_lsn 小于或等于 write_lsn/flushed_to_disk_lsn,则表示对应的Redo Log已写入OS Cache/磁盘,事务可以提交。
- 如果commit_lsn仍大于write_lsn/flushed_to_disk_lsn,则用户线程继续等待下一次唤醒。
关键线程
Redo持久化主要涉及Log Writer,Log Flusher,Log Write Notifier,Log Flush Notifier四个后台线程。在InnoDB 启动时,就会启动这四个线程。
- Log Writer
Log Writer会自旋,不断检测是否有待写入OS Cache的Redo Log。在写入前,会检查当前 ib_logfile 文件是否还有足够的空间来容纳待写入的 Redo Log。
- Log Flusher
Log Writer提升write_lsn之后会通知Log Flusher线程,Log Flusher线程会调用fsync()将redo刷盘,并推进flush_up_to_lsn。Log Flusher与Log Writer之间仅通过write_lsn同步刷盘位置。
- Log Notifier
Log Notifier 包含两个关键线程:Log Write Notifier 和 Log Flush Notifier。这两个线程会定期轮询(或被唤醒)各自关注的 LSN 位置:Log Write Notifier关注 write_lsn,而 Log Flush Notifier关注 flush_up_to_lsn。当这些 LSN 值推进到用户线程等待的位置时,它们会唤醒相应的用户线程。用户线程在执行 commit时会阻塞在 log_write_up_to 函数上,直到对应的 write_lsn 或 flush_up_to_lsn 推进到预期位置后,函数才会返回。