版本
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日志中的位置。
SN 和 LSN、物理位置是相互对应的,LSN可以理解为redo日志中近似物理位置的相对位置,SN便可以理解为真正的redo日志内容的相对位置,三者可以相互转换:
- SN为redo 记录内容的递增
- LSN = HEADER *N + SN + TAILER * N
- 物理位置 = LSN + 文件头(4个BLOCK) * N
SN 和 LSN 转换
// 将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和物理位置转换
// offset 为物理位置
lsn = offset - LOG_FILE_HDR_SIZE * (1 + offset / log.file_size
// lsn为与redo size总大小取模计算后的结果
offset = lsn + LOG_FILE_HDR_SIZE *
(1 + lsn / (log.file_size - LOG_FILE_HDR_SIZE)
后续会以LSN来描述redo的位置。
redo日志的写入
redo日志的写入流程如下:
- mtr(Mini transaction)写入函数,将redo 日志内容记录到mtr的buffer中
- mtr commit,将mtr buffer中的redo日志,记录到redo的log buffer中
- 将log buffer中的日志内容写入到系统缓存
- 进行一次flush_sync将系统缓存中的日志内容刷新到系统磁盘
- lsn对应的脏页如果已经刷新到磁盘,对redo log进行checkpoint
- 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的写入流程如下:
- 分配start_len,根据写入的长度预留空间
- 根据mtr中存放的redo log,写入到log buffer中
- 写入完成后,将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即可。