MySQL 8.0 InnoDB无锁化设计的日志系统( 二 )


下面本文将分别讨论以上三个问题:
1、MTR复制日志到Redo Log Buffer的无锁化
在MySQL 8.0中,MTR的提交部分可以用如下伪代码表示:

MySQL 8.0 InnoDB无锁化设计的日志系统

文章插图
同5.7的代码相比,最明显的区别就是移除了log_sys->mutex锁和log_sys->flush_order_mutex锁,而实现Redo Log无锁化的关键在于 log_buffer_reserve(*log_sys, len) 这个函数, 其中关键的代码只有两句:
MySQL 8.0 InnoDB无锁化设计的日志系统

文章插图
可以看到,这里是通过一个原子操作std::atomic<uint64>.fetch_add(log_len)实现在Copy Redo之前在全局Redo Log Buffer中预分配空间,实现并行写入而不冲突 。
2、Log Buffer空洞问题
预分配的方式可以使多个MTR不冲突的copy数据到Redo Log Buffer,但由于有些线程快一些,有些线程慢一些,必然会造成Redo Log Buffer的空洞问题,这个使得Redo Log Buffer刷入到磁盘的行为变得复杂 。
MySQL 8.0 InnoDB无锁化设计的日志系统

文章插图
如上图所示,Redo Log Buffer中第一个和第三个线程已经完成了Redo Log的写入,第二个线程正在写入到Redo Log Buffer中,这个时候是不能将三个线程的Redo都落盘的 。MySQL 8.0中引入了一个数据结构Link_buf解决这个问题 。
Link_buf实际上是一个定长数组,并保证数组的每个元素的更新是原子性的,并以环形的方式复用已经释放的空间 。
Link_buf用于辅助表示其他数据结构的使用情况,在Link_buf中,如果一个索引位置i对应的值为非0值n,则表示Link_buf辅助标记的那个数据结构,从i开始后面n个元素已被占用 。同时Link_buf内部维护了一个变量M表示当前最大可达的LSN,Link_buf的结构示意图如下所示:
MySQL 8.0 InnoDB无锁化设计的日志系统

文章插图
在接口层面,Link_buf实际上定义了3个有效的行为:
MySQL 8.0 InnoDB无锁化设计的日志系统

文章插图
Redo Log Buffer内部维护了两个Link_buf类型的变量recent_written和recent_closed来维护Redo Log Buffer和flush list的修改信息 。
对于redo log buffer,buffer的使用情况和recent_written的对应关系如下图所示:
MySQL 8.0 InnoDB无锁化设计的日志系统

文章插图
buf_ready_for_write_lsn这个变量维护的是可以保证无空洞的最大LSN值,也就是recent_written->tail的结果,在这之前的Redo Log都是可以安全的持久化到磁盘上的 。
当第一个空洞位置的数据被写入成功后,写入数据的mtr通过调用log.recent_written.add_link(start_lsn, end_lsn)将recent_written内部状态更新为如下图所示的样子:
MySQL 8.0 InnoDB无锁化设计的日志系统

文章插图
这部分代码在log0log.cc文件的log_buffer_write_completed方法中 。
每次修改recent_written后,都会触发一个独立的线程log_writer向后扫描recent_written并更新buf_ready_for_write_lsn 值(调用recent_written->advance_tail()方法) 。log_writer线程实际上就是执行日志写入到文件的线程 。由log_writer线程扫描后的recent_written变量内部如下图所示:
MySQL 8.0 InnoDB无锁化设计的日志系统

文章插图
这样就很好的解决了MTR并发写入log_buffer造成的空洞问题 。通过新引入的Link_buf类型的数据结构,可用很方便的知道哪一部分的Redo Log可以执行写入磁盘的操作 。
关于更多落盘的细节
在MySQL 8中,Redo log的落盘过程交由两个独立的线程完成,分别 log_writer和log_flusher,前者负责将Redo Log Buffer中的数据写入到OS Cache中,后者负责不停的执行fsync操作将OS Cache中的数据真正的写入到磁盘里 。
两个线程通过一个全局的原子变量log_t::write_lsn同步,write_lsn表示当前已经写入到OS Cache的Redo log最大的LSN 。
MySQL 8.0 InnoDB无锁化设计的日志系统

文章插图
log buffer中的redo log的落盘不需要由用户线程关心,用户线程只需要在事务提交的时候,根据innodb_flush_log_at_trx_commit定义的不同行为,等待log_writer或log_flusher的通知即可 。
log_writer线程会在监听到recent_written被修改后,log_buffer中大于log_t::write_lsn小于buf_ready_for_write_lsn的redo log刷入到 OS Cache 中,并更新log_t::write_lsn 。
log_flusher线程则在监听到write_lsn更新后调用一次fsync并更新flushed_to_disk_lsn,该变量保存的是最新fsync到文件的值 。
MySQL 8.0 InnoDB无锁化设计的日志系统

文章插图


推荐阅读