mysql 8.0 源码笔记:REDO日志(1)

版本

mysql-8.0.22 社区版

概述

redo日志中记录了Innodb引擎对于数据页的修改,主要作用是用来在崩溃恢复过程中,保证数据的完整性。
Innodb引擎采用WAL机制来记录数据,即修改数据时,将redo日志优先记录到磁盘中,真正的数据修改会在后续刷脏过程中记录到磁盘。如果此时数据库意外退出,可以通过redo日志来恢复修改的数据。

源码

后续结合源码来介绍redo日志的各个模块。

文件

redo log的文件以格式ib_logfile[N]来命名,通过如下参数控制:

innodb_log_file_size = 48M 						// 每个redo文件的大小
innodb_log_files_in_group = 2					// redo文件的个数
innodb_log_group_home_dir=./					// redo文件的存放路径

redo log的文件通常在数据库初始化时创建:

create_log_files
   // 删除遗留文件,如果存在的话
| for (unsigned i = 0; i <= num_old_files; i++)
    sprintf(logfilename + dirnamelen, "ib_logfile%u", i);
    unlink(logfilename);
    
   // 创建新的redo文件, 并设置文件的大小
| for (unsigned i = 0; i < srv_n_log_files; i++) {
    err = create_log_file(&files[i], logfilename);

  // 为所有的redo log创建一个tablespace
| fil_space_create(  "innodb_redo_log", dict_sys_t::s_log_space_first_id, ..

对于innodb来说,所有的redo文件被认为是一个文件,只创建了一个名为"innodb_redo_log"的tablespace,即所有的redo文件的space_id是相同的。

Block、LSN 、 SN

redo文件是循环进行写入,每次操作文件的Block大小为固定大小的512个字节。在这512个字节中,其中header占用12个字节,tailer暂用4个字节,剩余的空间用来记录redo的日志内容

#define OS_FILE_LOG_BLOCK_SIZE 512

| header(12 byte) |  redo log record | tailer(4 byte) |

LSN为逻辑的日志序列号,是单调递增的。每次有新的redo日志记录,就会相应的增加。

在Innodb中会发现LSN的作用较多,包括用来计算redo日志的位置,用来确认redo日志是否可以重用,用来确认数据页是否可以刷脏等等。起到了一个逻辑时序的作用,LSN越小,代表本次操作越早。比如脏页落盘之前要先保证redo已经落盘,就是通过该脏页对应的LSN和已经落盘的LSN进行对比。

由于redo log是循环写入,总的日志文件大小是不变的,所以可以通过LSN和日志文件的大小,确定该LSN在redo日志中的位置。

SNLSN是相互对应的,LSN可以理解为redo日志中物理文件的相对位置,SN便可以理解为真正的redo日志内容的相对位置,两者可以相互转换。

// 将sn转换为lsn,将每一个block添加header_size和tailer_size
log_translate_sn_to_lsn
  // LOG_BLOCK_DATA_SIZE = OS_FILE_LOG_BLOCK_SIZE(512)- header_size - tailer_size
| (sn / LOG_BLOCK_DATA_SIZE * OS_FILE_LOG_BLOCK_SIZE + sn % LOG_BLOCK_DATA_SIZE + LOG_BLOCK_HDR_SIZE)

// 将lsn转换为sn, 将每一个block删除header_size和tailer_size
log_translate_lsn_to_sn
|  sn = lsn / OS_FILE_LOG_BLOCK_SIZE * LOG_BLOCK_DATA_SIZE;
|  sn = sn + lsn % OS_FILE_LOG_BLOCK_SIZE - header_size

后续会以LSN来描述redo的位置。

redo日志的写入

redo日志的写入流程如下:

  1. mtr(Mini transaction)写入函数,将redo 日志内容记录到mtr的buffer中
  2. mtr commit,将mtr buffer中的redo日志,记录到redo的log buffer
  3. log buffer中的日志内容写入到系统缓存
  4. 进行一次flush_sync将系统缓存中的日志内容刷新到系统磁盘
  5. lsn对应的脏页如果已经刷新到磁盘,对redo log进行checkpoint
  6. checkpoint成功后,之前的redo日志便可以重复使用

其中mtr为innodb内部最小的原子事务操作,不再展开讨论。上述过程中分别对应redo日志的几个状态,关联几个可见的LSN(show engine innodb status 查看)如下:

---
LOG
---
Log sequence number          164495037            // 当前最大的LSN
Log buffer assigned up to    164495037
Log buffer completed up to   164495037            
Log written up to            164495037            // 当前已经写入到系统缓存的LSN
Log flushed up to            164495037            // 当前已经刷新到磁盘的LSN
Added dirty pages up to      164495037             
Pages flushed up to          161538614            // 当前脏页刷新到的LSN
Last checkpoint at           161538614            // 当前checkpoint的LSN

log_sys

log_sys为log_t类的对象,为全局唯一的对象,redo操作的相关信息均存在该对象中。

log_t *log_sys;
| sn					// 当前最大的LSN(SN)
| write_lsn				// 当前已经写入到系统缓存的LSN
| flushed_to_disk_lsn	// 当前已经刷新到系统磁盘的LSN
| last_checkpoint_lsn	// 当前已经checkpoint的LSN
| ...
| ...

该对象在系统启动时创建,同时初始化其中的部分变量。

srv_start
| log_sys_init
| | log_sys = UT_NEW_NOKEY(...);
| | ...
| | log.m_first_file_lsn = LOG_START_LSN;

log buffer

log buffer为redo日志在系统中的缓存,redo日志先暂存在log buffer中,由事务commit时,或者后台线程统一写入到系统缓存中。

定义

log buffer的定义如下,定义在log_sys中。

log_t *log_sys;
| buf					// log buffer的buf指针
| buf_size				// log buffer的大小
| buf_size_sn			// 忽略header 和 tailer的log buffer的大小(同SN)

| buf_limit_sn          // 当前buffer 允许写入的最大SN

| recent_written        // 保证log buffer写入系统缓存时,是连续的

初始化

在log_sys对象初始化时同时初始化log buffer。

// 参数innodb_log_buffer_size控制log_buffer的大小
innodb_log_buffer_size = 16M

srv_start
| log_sys_init
| | ...
| | log_allocate_buffer(log)
| | | log.buf.create(srv_log_buffer_size);

log_buffer的大小并不是固定的,如果需要写入的长度大于log_buffer的长度,会自适应的堆log_buffer进行扩容。

写入

log buffer的写入流程如下:

  1. 分配start_len,根据写入的长度预留空间
  2. 根据mtr中存放的redo log,写入到log buffer中
  3. 写入完成后,将start_lsn 和 end_lsn 写入 recent_written

写入入口在mtr.commit时:

mtr_t::commit()   <==> mtr_t::Command::execute
| 1. 根据写入的长度len,来预留空间,并分配start_lsn
| log_buffer_reserve(*log_sys, len)
| | sn_t start_sn = log.sn.fetch_add(len)        		// 将start_sn开始 长度为len的 空间预留下来
| | if (unlikely(end_sn > log.buf_limit_sn.load())
| |   log_wait_for_space_after_reserving(log, handle);  // 空间不足,等待足够的空间
|
| 2. 根据LSN,将redo内容写入log buffer中
| log_buffer_write 
| | byte *ptr = log.buf + (start_lsn % log.buf_size);	// 根据LSN定位地址
| | if (ptr >= buf_end)  ptr -= log.buf_size;           // buffer循环使用
| | std::memcpy(ptr, str, len);
|
| 3. 将start_lsn 和 end_lsn 写入recent_written
| log_buffer_write_completed
| | log.recent_written.add_link_advance_tail(start_lsn, end_lsn);

log buffer的使用,同redo日志的文件使用类似,以Block作为写入的单位(同redo日志的block对齐,为512个字节),逻辑上上是无限的空间,内存中是循环使用。地址定位使用LSN进行定位,在操作log buffer时,只需要考虑循环使用的覆盖即可。

log buffer的空间是否不足,是通过log.write_lsn来判断。log.write_lsn之前的数据是已经写入到系统缓存的数据,这部分buffer是可以覆盖复用的。只需要保证 log.write_lsn + buf_size_sn 大于 end_lsn即可。

上一篇:MySQL如何保证数据一致性


下一篇:《MySQL》系列 - 十张图详解 MySQL 日志(建议收藏)