浅析MySQL 8.0 redo log
Posted 深入浅出数据库内核
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅析MySQL 8.0 redo log相关的知识,希望对你有一定的参考价值。
本文结合mysql 8.0.19 代码,分析redo log的类型,mtr的原理,smo过程中的mtr,以及redo log无锁优化
# redo类型
innodb的日志是具有逻辑意义的物理日志,所以,日志记录的格式就不完全是物理信息,而是有一定逻辑意义,基本的格式如下:
type(日志类型),space(表空间ID值),offset(前面space所指定的文件中的页面号,以页面大小为单位),data(表示这条日志记录对应的数据,这个数据是不确定的,根据不同的type值而不同)
可以将redo分为三种:
- **作用于Page的REDO**:比如MLOG_REC_INSERT,MLOG_REC_UPDATE_IN_PLACE,MLOG_REC_DELETE三种类型分别对应于Page中记录的插入,修改以及删除。
- **作用于Space的REDO**:这类REDO针对一个Space文件的修改,如MLOG_FILE_CREATE,MLOG_FILE_DELETE,MLOG_FILE_RENAME分别对应对一个Space的创建,删除以及重命名。
- **提供额外信息的Logic REDO**:还有少数的几个REDO类型不涉及具体的数据修改,只是为了记录一些需要的信息,比如最常见的MLOG_MULTI_REC_END就是为了标识一个REDO组,也就是一个完整的原子操作的结束。
比较常用的redo有:
- mlog_1byte、mlog_2bytes、mlog_4bytes、mlog_8bytes:这四个类型,表示要在某个位置,写入一个(两个、四个、八个)字节的内容;
- mlog_write_string:这种类型的日志,其实和mlog_ibyte是类似的,只是mlog_ibyte是要写一个固定长度的数据,而mlog_write_string是要写一段变长的数据。
- mlog_undo_insert:可以简单理解为写undo时候产生的redo
- mlog_init_file_page:这个类型的日志比较简单,只有前面的基本头信息,没有data部分;
- mlog_comp_page_create:这个类型只需要存一个类型及要创建的页面的位置即可;
- mlog_multi_rec_end:这个类型的记录是非常特殊的,它只起一个标记的作用,其存储的内容只有占一个字节的类型值。
- mlog_comp_rec_clust_delete_mark:这个类型的日志是表示,需要将聚集索引中的某个记录打上删除标志;
- mlog_comp_rec_update_in_place:这个类型的日志记录更新后的记录信息,包括所有被更新的列的信息。
- mlog_comp_page_reorganize:这个类型的日志表示的是要重组指定的页面,其记录的内容也很简单,只需要存储要重组哪一个页面即可;
redo log的通用结构如下:
![image-20210108172059453](img/pic/redo/log_record_view.png)
下面将依次介绍各种redo(不断补充中):
## MLOG_1BYTE、MLOG_2BYTES、MLOG_4BYTES、MLOG_8BYTES
在页面上修改N个字节,可以看做物理log。各种页链表指针修改以及文件头、段页内容的修改都是以这几种方式记录日志。具体格式如下:
这四个类型,表示要在某个位置,写入一个(两个、四个、八个)字节的内容;
![image-20210108172229179](img/pic/redo/MLOG_nBYTE.png)
1. MLOG_1BYTE:type字段对应的十进制为1,表示在页面的某个偏移量处写入一个字节
2. MLOG_2BYTES:type字段对应的十进制为2,表示在页面的某个偏移量处写入两个字节
3. MLOG_4BYTES:type字段对应的十进制为4,表示在页面的某个偏移量处写入四个字节
4. MLOG_8BYTES:type字段对应的十进制为8,表示在页面的某个偏移量处写入八个字节
```
case MLOG_1BYTE:
case MLOG_2BYTES:
case MLOG_4BYTES:
case MLOG_8BYTES: {
uint16_t page_offset;
buf_ptr = read_buffer_n(&page_offset, mtr_buf, 2);
if (!buf_ptr) goto done;
print_log(0, "page offset: %" PRIu16 " ", page_offset);
if (type == MLOG_8BYTES) {
uint64_t val;
buf_ptr = read_compressed_64(&val, mtr_buf);
if (!buf_ptr) goto done;
print_log(0, "value: %" PRIu64 "\n", val);
} else {
uint32_t val;
buf_ptr = read_compressed(&val, mtr_buf);
if (!buf_ptr) goto done;
print_log(0, "value: %" PRIu32 "\n", val);
}
break;
}
```
>fixed_size=type+space_id+page_no+page_offset
>
>最大字节为11字节
## **MLOG_MULTI_REC_END**
记录格式:
TYPE 1字节
这个类型标识一个mtr产生多条redo记录已经结束,当数据恢复时候,分析mtr时候,只有分析到该类型时候,前面的redo记录才会去做REDO操作.
## MLOG_SINGLE_REC_FLAG
记录格式:
TYPE 1字节
用来标识一个mtr只产生一条redo记录,该标志与一条记录的类型进行"或"运算.当数据恢复时候,判断redo记录是否存在该类型标志.
```cpp
// 如果发现某一个 record 无 MLOG_SINGLE_REC_FLAG 标记说明其是 multi-record mtr 的起点
// 一直寻找其终点(MLOG_MULTI_REC_END),如果在发现 MLOG_MULTI_REC_END 前得到了具有
// MLOG_SINGLE_REC_FLAG 的 record,说明 log corrupt,但目前的处理方式是,将这段不完整的
// multi-record mtr 日志丢弃(不加入到 hash table),继续解析。但同时会提示用户运行 CHECK
// TABLE(ER_IB_MSG_698)。
```
## MLOG_COMP_REC_INSERT
表示插入一条使用compact行格式记录时的redo日志类型.
![image-20201228194249256](img/pic/redo/MLOG_COMP_REC_INSERT1.png)
![image-20201228173309838](img/pic/redo/MLOG_COMP_REC_INSERT.png)
```c++
|--> page_cur_insert_rec_write_log
| |--> mlog_open_and_write_index
| | |--> //初始化日志记录
| | |--> mlog_write_initial_log_record_fast
| | | |--> //mini-transaction相关的函数,用来将redo条目写入到redo log buffer
| | | |--> //写入type,space,page_no
| | | |--> mlog_write_initial_log_record_low
| | |--> //写入字段个数filed no,2个字节
| | |--> mach_write_to_2(log_ptr, n);
| | |--> //写入行记录上决定唯一性的列的个数 u_uniq,2个字节
| | |--> mach_write_to_2(log_ptr, dict_index_get_n_unique_in_tree(index))
| | |--> /*loop*/
| | |--> //写入每个字段的长度 filed_length
| | |--> mach_write_to_2(log_ptr, len);
| | |--> /*end loop*/
| |--> //写入记录在page上的偏移量 current rec off,占两个字节
| |--> mach_write_to_2(log_ptr, page_offset(cursor_rec));
| |--> //mismatch len
| |--> mach_write_compressed(log_ptr, 2 * (rec_size - i));
| |--> //将插入的记录拷贝到redo文件 body,同时关闭mlog
| |--> memcpy
| |--> mlog_close
```
## MLOG_REC_UPDATE_IN_PLACE,MLOG_COMP_REC_UPDATE_IN_PLACE
记录了对Page中一个Record的修改,方法如下:
> (Page ID,Record Offset,(Filed 1, Value 1) ... (Filed i, Value i) ... )
其中,PageID指定要操作的Page页,Record Offset记录了Record在Page内的偏移位置,后面的Field数组,记录了需要修改的Field以及修改后的Value。
![image-20210110211515332](img/pic/redo/MLOG_REC_UPDATE_IN_PLACE.png)
![image-20201229153112661](img/pic/redo/MLOG_COMP_REC_UPDATE_IN_PLACE.png)
Space ID和Page Number唯一标识一个Page页,这三项是所有REDO记录都需要有的头信息,后面的是MLOG_REC_UPDATE_IN_PLACE类型独有的,其中Record Offset用给出要修改的记录在Page中的位置偏移,Update Field Count说明记录里有几个Field要修改,紧接着对每个Field给出了Field编号(Field Number),数据长度(Field Data Length)以及数据(Filed Data)。
## MLOG_INIT_FILE_PAGE2、MLOG_INIT_FILE_PAGE :
```
/** this means that a file page is taken into use.
We use it to replace MLOG_INIT_FILE_PAGE. */
MLOG_INIT_FILE_PAGE2 = 59,
```
| Type 1B | compressed Space_id 1-5B | compressed Page_no 1-5B |
| -------------------- | ------------------------ | ----------------------- |
| MLOG_INIT_FILE_PAGE2 | | |
含义:申请空间,在分配一个bufpool的page空间时候就初始化这个page的space_id和page_no.
当数据恢复时候,该类型的日志作用是从buffer pool分配一个page,赋值对应的space_id,page_no为对应的值.
## MLOG_COMP_PAGE_CREATE
| Type 1B | compressed Space_id 1-5B | compressed Page_no 1-5B |
| --------------------- | ------------------------ | ----------------------- |
| MLOG_COMP_PAGE_CREATE | | |
```
|--> page_create_write_log
| |--> mlog_write_initial_log_record(MLOG_COMP_PAGE_CREATE)
| | |--> mlog_open
| | |--> mlog_write_initial_log_record_fast
| | |--> mlog_close
```
含义:初始化page,新建一个compact索引数据页初始化page的最大值、最小值等
当数据恢复时候,该类型的日志作用是初始化page的最大值、最小值、N_HEAP等.
## MLOG_UNDO_INSERT
在redo中,写入一条undo相关redo记录
```
|--> trx_undof_page_add_undo_rec_log
| |--> mlog_write_initial_log_record_fast
| |--> mach_write_to_2(log_ptr, len)
| |--> //将插入的记录拷贝到redo文件 body,同时关闭mlog
| |--> memcpy
| |--> mlog_close
```
| Type 1B | Space ID 4B | Page Number 4B | undo rec len 2B | undo rec data (data len) |
| ---------------- | ------------ | --------------- | --------------- | ------------------------- |
| MLOG_UNDO_INSERT | | | | |
字段解释:
undo rec len:表示一条undo日志记录的长度
生成undo日志记录的语句都会产生该类型的redolog.
当数据恢复时候,该类型的日志作用是INSERT一条undo记录.
## MLOG_COMP_REC_CLUST_DELETE_MARK
记录格式如下:
![image-20201229153112661](img/pic/redo/MLOG_COMP_REC_CLUST_DELETE_MARK.png)
字段解释:
逻辑删除。先存储11字节的fixed_size,然后是两字节的索引字段个数,接着是唯一索引键的个数2字节,接着就是索引的每个字段的定义长度,接着是2字节0x0001,接着存储TRX_ID在聚簇索引的下标,接着是ROLL_PTR 7字节,接着是TRX_ID的值,最后是记录的page_offset.
当在聚簇索引记录上打上delete标志时候会生成该类型的redolog记录.
## MLOG_COMP_REC_DELETE
| Type 1B | Space ID 4B | Page Number 4B | file no 2B | n_uniq 2B | filed length | current rec off |
| -------------------- | ------------ | --------------- | ---------- | --------- | ------------ | --------------- |
| MLOG_COMP_REC_DELETE | | | | | | |
字段解释:
磁盘删除。先存储type,space id,page no,然后是两字节的索引字段个数,接着是唯一索引键的个数2字节,接着就是索引的每个字段的定义长度,最后是记录在page中的偏移量.
当purge时候会产生该类型的redo log记录,所以当数据恢复时候,这种类型的日志作用就会purge某个数据记录.
```c++
|--> page_cur_delete_rec_write_log
| |--> mlog_open_and_write_index
| | |--> //初始化日志记录
| | |--> mlog_write_initial_log_record_fast
| | | |--> //mini-transaction相关的函数,用来将redo条目写入到redo log buffer
| | | |--> //写入type,space,page_no
| | | |--> mlog_write_initial_log_record_low
| | |--> //写入字段个数filed no,2个字节
| | |--> mach_write_to_2(log_ptr, n);
| | |--> //写入行记录上决定唯一性的列的个数 u_uniq,2个字节
| | |--> mach_write_to_2(log_ptr, dict_index_get_n_unique_in_tree(index))
| | |--> /*loop*/
| | |--> //写入每个字段的长度 filed_length
| | |--> mach_write_to_2(log_ptr, len);
| | |--> /*end loop*/
| |--> //写入记录在page上的偏移量 current rec off,占两个字节
| |--> mach_write_to_2(log_ptr, page_offset(cursor_rec));
```
## MLOG_WRITE_STRING
MLOG_WRITE_STRING::type字段对应的十进制为30,表示在页面的某个偏移量处写入一串数据
| Type 1B | compressed Space_id 1-5B | compressed Page_no 1-5B | page_offset 2B | string (data len) |
| ----------------- | ------------------------ | ----------------------- | -------------- | ------------------ |
| MLOG_WRITE_STRING | | | | |
```c++
case MLOG_WRITE_STRING: {
uint16_t page_offset, len;
buf_ptr = read_buffer_n(&page_offset, mtr_buf, 2);
if (!buf_ptr) goto done;
buf_ptr = read_buffer_n(&len, mtr_buf, 2);
if (!buf_ptr) goto done;
print_log(0, "page offset: %" PRIu16 ", len: %" PRIu16 "\n",
page_offset, len);
buf_ptr = read_buffer_n(NULL, mtr_buf, len);
if (!buf_ptr) goto done;
mtr_buf->buffer_offset += len;
hexdump(buf_ptr, len);
break;
}
```
## MLOG_REC_MIN_MARK, MLOG_COMP_REC_MIN_MARK
| Type 1B | compressed space_id 1-5B | compressed page_no 1-5B | page_offset 2B |
| ---------------------- | ------------------------- | ----------------------- | -------------- |
| MLOG_COMP_REC_MIN_MARK | | | |
这个类型的日志,是在将一条记录设置为页面中的最小记录(这个涉及到页面管理的内容,在一个页面中只有一个最小记录,它指向的是B树下一层的最左边的节点)时产生的,因为只是打个标记,存储内容比较简单,除了基本的日志头外,在Data内容中只存储了这条最小记录在页面内的偏移位置。当数据恢复时候,该类型的日志作用是标识对应的位置的记录为最小用户记录.
```
|--> btr_set_min_rec_mark
| |--> btr_set_min_rec_mark_log(MLOG_COMP_REC_MIN_MARK)
```
## MLOG_UNDO_HDR_CREATE,**MLOG_UNDO_HDR_REUSE**
| fiexd_size 11B | compressed trx_id 5-9B |
| ------------------------------------------------- | ----------------------- |
| MLOG_UNDO_HDR_CREATE+space_no_page_id+page_offset | |
在生成undo log hdr或者reuse undo log header过程中会产生该类型的redolog.
```
|--> trx_undo_header_create_log
| |--> mlog_write_initial_log_record(MLOG_UNDO_HDR_CREATE)
```
```
|--> trx_undo_insert_header_reuse_log
| |--> mlog_write_initial_log_record(MLOG_UNDO_HDR_REUSE)
```
## **MLOG_UNDO_ERASE_END**
| type 1B | compressed space_id 1~5B | compressed page_no 1~5B |
| ------------------- | ------------------------ | ----------------------- |
| MLOG_UNDO_ERASE_END | | |
插入undo page记录时候对剩余不足的page空间进行设置0xFF,会产生UNDO_ERASE_END类型的redolog
```
|--> trx_undo_erase_page_end
| |--> mlog_write_initial_log_record(MLOG_UNDO_ERASE_END)
```
## MLOG_FILE_CREATE,MLOG_FILE_DELETE,MLOG_FILE_RENAME
这类REDO针对一个Space文件的修改,如MLOG_FILE_CREATE,MLOG_FILE_DELETE,MLOG_FILE_RENAME分别对应对一个Space的创建,删除以及重命名。由于文件操作的REDO是在文件操作结束后才记录的,因此在恢复的过程中看到这类日志时,说明文件操作已经成功,因此在恢复过程中大多只是做对文件状态的检查,以MLOG_FILE_CREATE来看看其中记录的内容:
![image-20201229153112661](img/pic/redo/MLOG_FILE_CREATE.jpg)
# MTR 设计与实现
InnoDB会将事务执行过程拆分为若干个Mini Transaction(mtr),mtr是保证若干个page原子性变更的单位。一个mtr中包含若干个日志记录——mlog,每个日志记录都是对某个page——mblock;每个mtr包含一系列如加锁,写数据,写redo,放锁等操作。修改一个页需要获得该页的x-latch,访问一个页是需要获得该页的s-latch或者x-latch,持有该页的latch直到修改或者访问该页的操作完成。
redo log通常先写在mtr的cache里, 在mtr提交时, 将cache中的redo log刷入log buffer(公共buffer),
一般来说在一个MTR中会做两个事情.
- 写redo log
- 挂载脏页到flush list.
## 不同的 mini-transaction 如何互斥?
在操作数据前,会根据锁类型,加不同类型的锁,之后将`object`和锁类型存入`m_memo`:
```
mtr_memo_push(mtr, object, type);
```
## mini-transaction 插入数据
- `byte *mlog_open(mtr_t *mtr, ulint size)`: 打开`mtr`的`m_log`
- `mlog_write_initial_log_record_low()`函数向`m_log`中写入`type`,`space id`,`page no`,并增加`m_n_log_recs`的数量
- `mtr->get_log()->push()`按不同的类型写数据
- `mlog_close()`: 更新 m_log 中的位置
![image-20201229153112661](img/pic/redo/mtr.png)
## 数据结构
```c++
struct mtr_t{
struct Impl{
//mtr 持有锁的栈
/** memo stack for locks etc. */
mtr_buf_t m_memo;
//mtr产生的日志
/** mini-transaction log */
mtr_buf_t m_log;
//是否产生buffer pool脏页
/** true if mtr has made at least one buffer pool page dirty */
bool m_made_dirty;
//insert buffer 是否修改
/** true if inside ibuf changes */
bool m_inside_ibuf;
//是否修改buffer pool pages
/** true if the mini-transaction modified buffer pool pages */
bool m_modifications;
// 一个MTR已产生log记录个数
/** Count of how many page initial log records have been
written to the mtr log */
ib_uint32_t m_n_log_recs;
//MTR的日志模式包括
//MTR_LOG_ALL:默认模式,记录所有会修改磁盘数据的操作;
//MTR_LOG_NONE:不记录redo,脏页也不放到flush list上;
//MTR_LOG_NO_REDO:不记录redo,但脏页放到flush list上,如临时表的修改;
//MTR_LOG_SHORT_INSERTS:插入记录操作REDO,在将记录从一个page拷贝到另外一个新建的page时用到,此时忽略写索引信息到redo log中。
//(参考函数page_cur_insert_rec_write_log)
/** specifies which operations should be logged; default
value MTR_LOG_ALL */
mtr_log_t m_log_mode;
//mtr状态
//MTR_STATE_INIT MTR_STATE_ACTIVE
//MTR_STATE_COMMITTING
//MTR_STATE_COMMITTED
/** State of the transaction */
mtr_state_t m_state;
/** Flush Observer */
FlushObserver *m_flush_observer;
/** Owning mini-transaction */
mtr_t *m_mtr;
}
private:
Impl m_impl;
/** LSN at commit time */
lsn_t m_commit_lsn;
/** true if it is synchronous mini-transaction */
bool m_sync;
class Command;
friend class Command;
}
```
mtr的执行总的来说分为3步:
- mtr_start(&mtr);
- do work(MTR & Btree)
- mtr_commit(&mtr);
下面将依次介绍。
## mtr_t::start
接下来我们来看MTR的启动函数mtr_t::start。这个函数包含两个参数,第一个sync表示是否当前的mtr是同步,第二个是read_only,这个表示当前mtr 是否只读.默认情况下sync=true, read_only=false.
这里的初始化可以看到state会被初始化为MTR_STATE_ACTIVE,其他的参数都是初始化为默认值.
```c++
void mtr_t::start(bool sync, bool read_only) {
m_sync = sync;
m_commit_lsn = 0;
new (&m_impl.m_log) mtr_buf_t();
new (&m_impl.m_memo) mtr_buf_t();
m_impl.m_mtr = this;
m_impl.m_log_mode = MTR_LOG_ALL;
m_impl.m_inside_ibuf = false;
m_impl.m_modifications = false;
m_impl.m_made_dirty = false;
m_impl.m_n_log_recs = 0;
m_impl.m_state = MTR_STATE_ACTIVE;
m_impl.m_flush_observer = NULL;
}
```
## mtr_t::commit
Start完毕之后,就是提交修改了(commit)。提交主要做两件事:
1. 按照buf_free将记录复制到Log Buffer中
2. 将脏页放到flush list中
所以如果mtr没有产生redo且没有修改数据页,就不用执行commit逻辑,仅仅释放资源即可。
1. 首先会设置m_state为COMMITTING状态
2. 然后进行判断是否需要执行所做的修改.
这里可以看到只要满足下面两个条件之一就会去执行MTR.
1. m_n_log_recs(表示当前mtr redo log数目) 大于0.
2. 当前mtr修改buffer pool pages并且不生成redo log操作.
mtr::commit的总体流程如下:
```c++
void mtr_t::commit() {
//1.首先会设置m_state为COMMITTING状态
m_impl.m_state = MTR_STATE_COMMITTING;
Command cmd(this);
//2.然后进行判断是否需要执行所做的修改.
if (m_impl.m_n_log_recs > 0 ||
(m_impl.m_modifications && m_impl.m_log_mode == MTR_LOG_NO_REDO)) {
cmd.execute();
} else {
cmd.release_all();
cmd.release_resources();
}
}
```
其中mtr提交最重要的是cmd.execute(),首先通过prepare_write得到最终要写入的日志长度,分为5步:
1. `log_buffer_reserve`:等待recent_written的空间,预留logbuffer的空间,如果空间不够,会调用log_write_up_to清理LogBuffer空间;log_write_up_to通过设置writer_event,异步触发log_writer写。
2. `write_log`:将m_log的内容memcpy到LogBuffer中,然后在**recent_written**加一个link。
3. `log_wait_for_space_in_log_recent_closed`:等待recent_closed的空间
4. `add_dirty_block_to_flush_list`:将该mtr对应的脏页添加到flushlist中
5. `log_buffer_close`:在**recent_closed**中加一个link。
具体流程如下,其中 **LinkBuf**,**recent_written** ,**recent_closed** 等结构体涉及到redo log的无锁优化,后文会具体介绍。
```c++
|--> mtr_t::Command::execute
| |--> //在excecute中会先进行写之前的操作,主要是进行一些校验以及最终返回将要写入的redolog长度
| |--> mtr_t::Command::prepare_write
| |--> //进入excecute阶段,首先构造mtr_write_log_t结构
| |--> mtr_write_log_t write_log;
| |--> //1.分配log buf以及初始化write_log.
| |--> //不同的mtr会首先调用log_buffer_reserve函数获得起始和终点lsn位置
| |--> log_buffer_reserve
| | |--> log_buffer_s_lock_enter
| | | |--> log.sn_lock.s_lock
| | |--> //用自己的REDO长度,原子地对全局偏移log.sn做fetch_add,得到自己在Log Buffer中独享的空间
| | |--> log.sn.fetch_add
| | |--> //不同mtr并行的将自己的m_log中的数据拷贝到各自独享的空间内。
| | |--> log_wait_for_space_after_reserving
| | | |--> //确保这条 redo log 能完整的写入 redo log Buffer,而且回环后不会覆盖尚未写入磁盘的 redo log.
| | | |--> log_wait_for_space_in_log_buf
| | | | |--> log_write_up_to
| | | | | |--> log_wait_for_write
| | | | | | |--> //log_write_notify会唤醒这个条件变量
| | | | | | |--> os_event_wait_for(log.write_events[slot],stop_condition);
| | | | | |--> log_wait_for_flush
| |--> //2.从mtr的buffer中写入内容到redolog的buffer.
| |--> m_impl->m_log.for_each_block(write_log)
| | |--> mtr_write_log_t::operator
| | | |--> //写入到redo log buffer(log->buf).
| | | |--> log_buffer_write
| | | |--> //拷贝完成后触发LinkBuf更新,更新recent_written字段
| | | |--> log_buffer_write_completed
| | | | |--> //更新本次写入的内容范围对应的LinkBuf内特定的数组项值
| | | | |--> log.recent_written.add_link
| |--> //3.在加脏页之前需要判断是否link buf已满。
| |--> log_wait_for_space_in_log_recent_closed
| |--> //4.加脏页到flush list中.
| |--> add_dirty_blocks_to_flush_list
| | |--> add_dirty_page_to_flush_list
| |--> log_buffer_close
| | |--> //5.更新recent_closed字段
| | |--> log.recent_closed.add_link(start_lsn, end_lsn);
```
# MTR & btree
DML过程需要redo来记录数据的变化,DML本质上是对btree进行增删查改操作,以及smo。下面将从乐观和悲观的角度进行分析MTR和bree之间的互动。
## optimistic
下面将分析乐观insert过程中,涉及到的mtr。
在INSERT过程中,共有如下5个mtr(如果有索引可能会有额外的mtr),每个mtr中有若干个记录。
- mtr_1:写数据,贯穿整个执行阶段
- mtr_2:用于记录 undo log
- mtr_3:分配或复用一个 undo log
- mtr_4:2pc-prepare
- mtr_5:2pc-commit
![image-20201229153112661](img/pic/redo/btr_cur_optimistic_insert.png)
### 执行阶段
```c++
//此函数会打开cursor,定位到插入的page。由btr_cur_optimistic_insert执行正在的插入
|--> row_ins_clust_index_entry_low
| |--> // mtr_1 贯穿整条insert语句
| |--> mtr_start(mtr_1);
| |--> btr_pcur_t::open
| | |--> btr_cur_search_to_nth_level
| | | |--> //对index加s锁
| | | |--> mtr_s_lock(dict_index_get_lock(index), mtr_1);
| | | |--> buf_page_get_gen
| | | | |--> mtr_add_page(block);
| | | | | |--> //Pushes an object to an mtr memo stack.
| | | | | |--> mtr_memo_push(mtr_1, block, fix_type);
| |--> btr_cur_optimistic_insert(mtr_1)
| | |--> rec_get_converted_size//计算物理记录的大小
| | |--> btr_cur_get_page_cur
| | |--> //完成与lock 和 undo 有关的操作,并且赋值roll_ptr给filed
| | |--> btr_cur_ins_lock_and_undo
| | | |--> //写undo记录
| | | |--> trx_undo_report_row_operation
| | | | |--> // mtr_2 用于记录 undo log
| | | | |--> mtr_start(mtr_2)
| | | | |--> //分配undo空间
| | | | |--> trx_undo_assign_undo
| | | | | |--> //mtr_3 分配或复用一个 undo log
| | | | | |--> mtr_start(mtr_3)
| | | | | |--> //这里先尝试复用,如果复用失败,则分配新的 undo log
| | | | | |--> trx_undo_reuse_cached
| | | | | | |--> trx_undo_page_get
| | | | | | | |--> //对复用(也可能是分配)的 undo log page 加 RW_X_LATCH 入栈
| | | | | | | |--> mtr_memo_push(mtr_3)
| | | | | | |--> //写undo log header
| | | | | | |--> if(type == TRX_UNDO_INSERT)
| | | | | | |--> //insert的undo页在提交的时候就没有用了,只是在insert事务回滚的时候才用上;
| | | | | | |--> //所以,insert的undo分配每次都是重用之前的cache,只是修改头部数据即可
| | | | | | |--> trx_undo_insert_header_reuse(mtr_3)
| | | | | | | |--> /* Write the log record MLOG_UNDO_HDR_REUSE */
| | | | | | | |--> trx_undo_insert_header_reuse_log(mtr_3)
| | | | | | | | |--> mlog_write_initial_log_record(undo_page, MLOG_UNDO_HDR_REUSE, mtr_3);
| | | | | | |--> else
| | | | | | |--> //而update就是需要创建一个undopage header文件块
| | | | | | |--> trx_undo_header_create(undo_page, trx_id, mtr_3);
| | | | | | |--> endif
| | | | | | |--> //在undo header 中预留 XID 空间
| | | | | | |--> trx_undo_header_add_space_for_xid(mtr_3)
| | | | | | | |--> mlog_write_ulint(MLOG_2BYTES)
| | | | | | | |--> mlog_write_ulint(MLOG_2BYTES)
| | | | | | | |--> mlog_write_ulint(MLOG_2BYTES)
| | | | | |--> //提交 mtr_3
| | | | | |--> mtr_commit(mtr_3)
| | | | |--> buf_page_get_gen
| | | | | |--> //即将写入的 undo log page 加 RW_X_LATCH 入栈
| | | | | |--> mtr_memo_push(mtr_2)
| | | | |--> //undo log 记录 insert 操作
| | | | |--> trx_undo_page_report_insert(mtr_2)
| | | | | |--> trx_undo_page_set_next_prev_and_add(mtr_2)
| | | | | | |--> trx_undof_page_add_undo_rec_log(mtr_2)
| | | | | | | |--> //MLOG_UNDO_INSERT:在redo中,写入一条undo相关redo记录
| | | | | | | |--> mlog_write_initial_log_record_fast(undo_page,MLOG_UNDO_INSERT,log_ptr,mtr_2);
| | | | |--> //提交 mtr_2
| | | | |--> mtr_commit(mtr_2)
| | |--> //执行插入逻辑
| | |--> page_cur_tuple_insert
| | | |--> page_cur_insert_rec_low
| | | | |--> //记录此次 INSERT 的 redo 日志
| | | | |--> page_cur_insert_rec_write_log(mtr_1)
| | | | | |--> mlog_open_and_write_index(insert_rec,MLOG_COMP_REC_INSERT,log_ptr,mtr)
| |--> //提交 mtr_1
| |--> mtr_commit(mtr_1);
```
### 提交阶段
```c++
|--> ha_commit_trans
| |--> MYSQL_BIN_LOG::prepare
| | |--> ha_prepare_low
| | | |--> //进入innodb层
| | | |--> innobase_xa_prepare
| | | | |--> trx_prepare_for_mysql
| | | | | |--> trx_prepare
| | | | | | |--> trx_prepare_low
| | | | | | | |--> mtr_start_sync(mtr_4)
| | | | | | | |--> //主要是设置Prepare阶段的undo的状态;包括tid与tid状态
| | | | | | | |--> trx_undo_set_state_at_prepare(mtr_4)
| | | | | | | | |--> trx_undo_page_get(mtr_4)
| | | | | | | | | |--> buf_page_get_gen
| | | | | | | | | | |--> //undo page 加 RW_X_LATCH 入栈
| | | | | | | | | | |--> mtr_memo_push(mtr_4)
| | | | | | | | |--> //Write GTID information if there
| | | | | | | | |--> trx_undo_gtid_write(mtr_4)
| | | | | | | | |--> //写入TRX_UNDO_STATE
| | | | | | | | |--> mlog_write_ulint(seg_hdr+TRX_UNDO_STATE, undo->state, MLOG_2BYTES,mtr_4);
| | | | | | | | |--> //写入TRX_UNDO_FLAGS
| | | | | | | | |--> mlog_write_ulint(undo_header+TRX_UNDO_FLAGS,undo->flag,MLOG_1BYTE, mtr_4)
| | | | | | | | |--> //undo 写入 xid,设置undo的xid
| | | | | | | | |--> trx_undo_write_xid(undo_header, &undo->xid, mtr_4);
| | | | | | | | | |--> mlog_write_ulint(MLOG_4BYTES)
| | | | | | | | | |--> mlog_write_ulint(MLOG_4BYTES)
| | | | | | | | | |--> mlog_write_ulint(MLOG_4BYTES)
| | | | | | | | | |--> mlog_write_string--MLOG_WRITE_STRING
| | | | | | | |--> mtr_commit(mtr_4)
| |--> MYSQL_BIN_LOG::commit
| | |--> MYSQL_BIN_LOG::ordered_commit
| | | |--> MYSQL_BIN_LOG::process_commit_stage_queue
| | | | |--> ha_commit_low
| | | | | |--> //进入innodb层
| | | | | |--> innobase_commit
| | | | | | |--> innobase_commit_low
| | | | | | | |--> trx_commit_for_mysql
| | | | | | | | |--> trx_commit
| | | | | | | | | |--> // mtr_5 用于 commit transaction
| | | | | | | | | |--> mtr_start_sync(mtr_5);
| | | | | | | | | |--> trx_commit_low(mtr_5)
| | | | | | | | | | |--> trx_write_serialisation_history(mtr_5)
| | | | | | | | | | | |--> trx_undo_set_state_at_finish(mtr_5)
| | | | | | | | | | | | |--> trx_undo_page_get(mtr_5)
| | | | | | | | | | | | | |--> buf_page_get_gen
| | | | | | | | | | | | | |--> //undo page 加 RW_X_LATCH 入栈
| | | | | | | | | | | | | |--> mtr_memo_push(mtr_5)
| | | | | | | | | | | | |--> //设置事务结束时的undo状态,这里是 TRX_UNDO_CACHED
| | | | | | | | | | | | |--> mlog_write_ulint(TRX_UNDO_STATE,state, MLOG_2BYTES,mtr_5);
| | | | | | | | | | | |--> trx_sys_update_mysql_binlog_offset(mtr_5)
| | | | | | | | | | | | |--> // 更新偏移量信息到系统表空间,设置binlog的位点
| | | | | | | | | | | | |--> write_binlog_position(mtr_5)
| | | | | | | | | | | | | |--> mlog_write_ulint(MLOG_4BYTES)
| | | | | | | | | | |--> mtr_commit(mtr_5);
```
## pessimistic
### smo--split
页面分裂全程在mtr_1中执行
```c++
|--> btr_cur_pessimistic_insert
| |-->//root页面分裂
| |--> btr_root_raise_and_insert
| |-->//普通页面分裂
| |--> btr_page_split_and_insert
```
#### 普通页面分裂
**普通页面分裂的流程:**
1. 分配新page的空间
2. 初始化btr page
3. 将该page加到父亲节点上
4. 将要分裂的page的记录转移到新节点中
5. 插入数据
```c++
|--> btr_page_split_and_insert(mtr_1)
| |--> //1.分配新page的空间
| |--> btr_page_alloc(mtr_1)
| | |--> btr_page_alloc_low
| | | |--> fseg_alloc_free_page_general
| | | | |--> mtr_x_lock_space(space, mtr_1);
| | | | |--> fseg_alloc_free_page_low
| | | | | |--> fsp_alloc_free_page
| | | | | | |--> fsp_page_create
| | | | | | | |--> fsp_init_file_page
| | | | | | | | |--> mlog_write_initial_log_record(MLOG_INIT_FILE_PAGE2,mtr_1)
| |--> //2.初始化btr page
| |--> btr_page_create
| | |--> page_create
| | | |--> page_create_write_log
| | | | |--> mlog_write_initial_log_record(MLOG_COMP_PAGE_CREATE)
| | | |--> page_create_low
| |--> //3.将该page加到父亲节点上
| |--> btr_attach_half_pages
| | |--> btr_insert_on_non_leaf_level
| | | |--> btr_cur_optimistic_insert
| | | | |--> page_cur_tuple_insert
| | | | | |--> page_cur_insert_rec_low
| | | | | | |--> page_cur_insert_rec_write_log
| | | | | | | |--> mlog_open_and_write_index(MLOG_COMP_REC_INSERT)
| | |--> //设置老node的next
| | |--> btr_page_set_next
| | | |--> mlog_write_ulint(MLOG_4BYTES)
| | |--> //设置新node的pre
| | |--> btr_page_set_prev
| | | |--> mlog_write_ulint(MLOG_4BYTES)
| | |--> //设置新node的next
| | |--> btr_page_set_next
| | | |--> mlog_write_ulint(MLOG_4BYTES)
| |--> //4.将要分裂的page的记录转移到新节点中
| |--> page_move_rec_list_end
| | |--> page_copy_rec_list_end_to_created_page
| | | |--> page_copy_rec_list_to_created_page_write_log
| | | | |--> //结束复制
| | | | |--> mlog_open_and_write_index(MLOG_COMP_LIST_END_COPY_CREATED)
| | |--> page_delete_rec_list_end
| | | |--> //删除原page中的记录
| | | |--> page_delete_rec_list_write_log(MLOG_COMP_LIST_END_DELETE)
| |--> //5.插入数据
| |--> page_cur_tuple_insert
| | |--> page_cur_insert_rec_low
| | | |--> page_cur_insert_rec_write_log
| | | | |--> mlog_open_and_write_index(MLOG_COMP_REC_INSERT)
```
#### root页面分裂
**root页面分裂的流程:**
1. 申请一个新page,将root的记录,转移到新page中;
2. 重建旧root并将其作为新的root,即清空旧数据,并维护与叶子节点的指针
3. 然后对新page进行节点分裂,新root变为索引节点,其中插入记录指向新page。而新page的分裂则是走下面描述的普通页面分裂流程。
```c++
|--> btr_root_raise_and_insert
| |--> //分配新page的空间
| |--> btr_page_alloc(mtr_1)
| | |--> btr_page_alloc_low
| | | |--> fseg_alloc_free_page_general
| | | | |--> mtr_x_lock_space(space, mtr_1);
| | | | |--> fseg_alloc_free_page_low
| | | | | |--> fsp_alloc_free_page
| | | | | | |--> fsp_page_create
| | | | | | | |--> fsp_init_file_page
| | | | | | | | |--> mlog_write_initial_log_record(MLOG_INIT_FILE_PAGE2,mtr_1)
| |--> //1.初始化btr page
| |--> btr_page_create
| | |--> page_create
| | | |--> page_create_write_log
| | | | |--> mlog_write_initial_log_record(MLOG_COMP_PAGE_CREATE)
| | | |--> page_create_low
| |--> //设置新node的next
| |--> btr_page_set_next
| | |--> mlog_write_ulint(MLOG_4BYTES)
| |--> //设置新node的pre
| |--> btr_page_set_prev
| | |--> mlog_write_ulint(MLOG_4BYTES)
| |--> //2.将root节点的记录转移到新的节点中
| |--> page_move_rec_list_end
| | |--> page_copy_rec_list_end_to_created_page
| | | |--> page_copy_rec_list_to_created_page_write_log
| | | | |--> //结束复制
| | | | |--> mlog_open_and_write_index(MLOG_COMP_LIST_END_COPY_CREATED)
| | |--> page_delete_rec_list_end
| | | |--> //删除原page中的记录
| | | |--> page_delete_rec_list_write_log(MLOG_COMP_LIST_END_DELETE)
| |--> //重建root节点
| |--> btr_page_empty
| | |--> page_create
| | | |--> page_create_low
| | | | |--> mlog_write_initial_log_record(MLOG_COMP_PAGE_CREATE)
| |--> //设置老root的next
| |--> btr_page_set_next
| | |--> mlog_write_ulint(MLOG_4BYTES)
| |--> //设置老root的pre
| |--> btr_page_set_prev
| | |--> mlog_write_ulint(MLOG_4BYTES)
| |--> //在root节点中,插入node_ptr
| |--> page_cur_tuple_insert
| | |--> page_cur_insert_rec_low
| | | |--> page_cur_insert_rec_write_log
| | | | |--> mlog_open_and_write_index(MLOG_COMP_REC_INSERT)
| |--> //3.对新page进行节点分裂
| |--> btr_page_split_and_insert
```
### smo--merge
1. 将符合merge的兄弟节点加载到内存merge_block
2. 将当前记录拷贝到兄弟节点
3. 维护页面前后指针
4. 维护父节点指针
5. 释放磁盘page
```c++
|--> btr_cur_pessimistic_delete(0)
| |--> btr_cur_compress_if_useful
| | |--> btr_compress
| | | |--> //1.将符合merge的兄弟节点加载到内存merge_block
| | | |--> btr_can_merge_with_page
| | | |--> //2.将当前记录拷贝到兄弟节点
| | | |--> page_copy_rec_list_end(merge_block, block)
| | | |--> //3.维护页面前后指针
| | | |--> btr_level_list_remove
| | | | |--> //设置node的next
| | | | |--> btr_page_set_next
| | | | | |--> mlog_write_ulint(MLOG_4BYTES)
| | | | |--> //设置node的pre
| | | | |--> btr_page_set_prev
| | | | | |--> mlog_write_ulint(MLOG_4BYTES)
| | | |--> //4.维护父节点指针
| | | |--> btr_node_ptr_set_child_page_no
| | | | |--> mlog_write_ulint(MLOG_4BYTES)
| | | |--> //5.释放磁盘page
| | | |--> btr_page_free
| | | | |--> btr_page_free_low
| | | | | |--> fseg_free_page
| | | | | | |--> fseg_free_page_low
| | | | | | | |--> fseg_set_nth_frag_page_no
| | | | | | | | |--> mlog_write_ulint(MLOG_4BYTES)
| | | | | | | |--> fsp_free_page
| | | | | | | | |--> //XDES_FREE_BIT
| | | | | | | | |--> xdes_set_bit
| | | | | | | | | |--> mlog_write_ulint(MLOG_1BYTE)
| | | | | | | | |--> //XDES_CLEAN_BIT
| | | | | | | | |--> xdes_set_bit
| | | | | | | | | |--> mlog_write_ulint(MLOG_1BYTE)
| | | | | | | | |--> //更新frag_n_used
| | | | | | | | |--> mlog_write_ulint(MLOG_4BYTES)
```
# redo无锁优化
MySQL 8.0如何做到无锁?
- 并发写入redo log buffer--移除 log_sys->mutex
高并发的环境中,会同时有非常多的*min-transaction(mtr)*需要拷贝数据到Log Buffer,如果通过锁互斥,那么毫无疑问这里将成为明显的性能瓶颈。为此,从MySQL 8.0开始,设计了一套无锁的写log机制,其核心思路是引入recent_written,允许不同的mtr,同时并发地写Log Buffer的不同位置。不同的mtr会首先调用*log_buffer_reserve*函数,这个函数里会用自己的REDO长度,原子地对全局偏移*log.sn*做*fetch_add*,得到自己在Log Buffer中独享的空间。之后不同mtr并行的将自己的m_log中的数据拷贝到各自独享的空间内。
- 并发加入flush list--移除log_sys_t::flush_order_mutex
移除了了锁结构`log_sys_t::flush_order_mutex`, 这就使得并发写flush list的LSN递增性质保证不了。但是依然要保证WAL,以及flush list上的刷脏策略仍然是从oldest page开始。如何解决dirty page加入flush list的空洞问题?引入另一个无锁结构体的变量`recent_closed`。维护buffer_dirty_pages_added_up_to_lsn(recent_closed.tail()),保证小于当前buffer_dirty_pages_added_up_to_lsn的脏页都已经加入到flush list中。允许局部乱序的加入flush list中。
具体的实现可以参考 [官方文档](https://mysqlserverteam.com/mysql-8-0-new-lock-free-scalable-wal-design/) 。下面简单分析涉及到的数据结构和后台线程。
## redo log buffer 结构体
```c++
struct alignas(INNOBASE_CACHE_LINE_SIZE) log_t {
//通常用这个log.sn ,通过log_get_lsn去换算获得当前的lsn
atomic_sn_t sn;
aligned_array_pointer<byte, OS_FILE_LOG_BLOCK_SIZE> buf; // log buffer的内存区
Link_buf<lsn_t> recent_written; // 解决并发插入Redo Log Buffer后刷入ib_logfile存在空洞的问题
Link_buf<lsn_t> recent_closed; // 解决并发插入flush_list后确认checkpoint_lsn的问题
atomic_lsn_t write_lsn; // write_lsn之前的数据已经写入系统的Cache, 但不保证已经Flush
atomic_lsn_t flushed_to_disk_lsn; // 已经被flush到磁盘的数据
size_t buf_size; // log buffer缓冲区的大小
lsn_t available_for_checkpoint_lsn;
// 在此lsn之前的所有被添加到buffer pool的flush list的log数据已经被flsuh,
//下一次checkpoint可以make在这个lsn. 与last_checkpoint_lsn的区别是该lsn尚未被真正的checkpoint.
lsn_t requested_checkpoint_lsn; // 下次需要进行checkpoint的lsn
atomic_lsn_t last_checkpoint_lsn; // 目前最新的checkpoint的lsn
uint32_t write_ahead_buf_size; // write ahead的Buffer大小
lsn_t current_file_lsn; // 当前正在fsync到的LSN,不断增加并且不回环的,它是redo log实际内容在逻辑上的增长
uint64_t current_file_real_offset; //代表在所有redo 物理文件的偏移
uint64_t current_file_end_offset; // 当前ib_logfile文件末尾的offset
uint64_t file_size; // 当前ib_logfile的文件大小
uint64_t files_real_capacity //表示2个文件实际大小总和
}
```
### recent_written
MySQL 8.0 通过直接计算每一条 redo 在 redo log Buffer 的 offset 来并发插入 redo log Buffer, 所以这里是允许 redo log Buffer 存在空洞的,而写入`ib_logfile`不允许,所以利用`recent_written.tail` 来保证在此 lsn 之前的 redo log Buffer 是不存在空洞的,从而完成`ib_logfile`的完整写入.
### recent_closed
为了能安全的进行 checkpoint,需要选择一个数据已经被 Flush 的 redo log 的lsn,因为可以并发的将脏页插入 Buffer Pool 中的`flush_list`, 所以选择所有 Buffer Pool 的`flush_list`中头部最小的一个 Dirty Page 的 lsn, 与`log.recent_closed.tail()`比对选择一个较小的 lsn, 可以认为是一个安全的`checkpoint_lsn`. `log.recent_closed`记录着并行插入`flush_list`的 Page lsn.
### redo写入涉及到的lsn
**log.write_lsn**
这个lsn 是到这个lsn 为止, 之前所有的data 已经从log buffer 写到log files了, 但是并没有保证这些log file 已经flush 到磁盘上了, 下面log.fushed_to_disk_lsn 指的才是已经flush 到磁盘的lsn 了.
这个值是由log writer thread 来更新
**log.flushed_to_disk_lsn**
到了这个lsn 为止, 所有的写入到redo log 的数据已经flush 到log files 上了
这个值是由log flusher thread 来更新
> log.flushed_to_disk_lsn <= log.write_lsn
### checkpoint涉及到的lsn
**log.buffer_dirty_pages_added_up_to_lsn**
到这个lsn 为止, 所有的redo log 对应的dirty page 已经添加到buffer pool 的flush list 了.
这个值其实就是recent_closed.tail()
inline lsn_t log_buffer_dirty_pages_added_up_to_lsn(const log_t &log) { return (log.recent_closed.tail()); }
这个值由log closer thread 来更新
**log.available_for_checkpoint_lsn**
表示可以进行checkpoint的lsn。到这个lsn 为止, 所有的redo log 对应的dirty page 已经flush 到btree 上了, 因此这里我们flush 的时候并不是顺序的flush, 所以有可能存在有空洞的情况, 因此这个lsn 的位置并不是最大的redo log 已经被flush 到btree 的位置. 而是可以作为checkpoint 的最大的位置.
这个值是由log checkpointer thread 来更新
**log.last_checkpoint_lsn**
到这个lsn 为止, 所有的btree dirty page 已经flushed 到disk了, 并且这个lsn 值已经被更新到了ib_logfile0 这个文件去了.
这个lsn 也是下一次recovery 的时候开始的地方, 因为last_checkpoint_lsn 之前的redo log 已经保证都flush 到btree 中去了. 所以比这个lsn 小的redo log 文件已经可以删除了, 因为数据已经都flush 到btree data page 中去了.
这个值是由log checkpointer thread 来更新
> log.last_checkpoint_lsn <= log.available_for_checkpoint_lsn <= log.buf_dirty_pages_added_up_to_lsn
### 如何循环写redo 文件
初始时current_file_real_offset和current_file_lsn对应起来。之后的每次写入都同步更新这两个值,就可以完成逻辑上无限的current_file_lsn到实际有限的current_file_real_offset的映射转换。
current_file_end_offset代表当前在写的这个文件对应的结尾位置,如果current_file_real_offset超过这个位置就需要将其加上2K Header表示切换到下一个文件
files_real_capacity表示2个文件实际大小总和,如果current_file_real_offset超过这个值,代表当前2个文件都已经写完了,需要回绕到第一个文件重新写,这里就会将current_file_real_offset重新置为2048,完成回绕。
## 后台线程
![image-20201229152120694](img/pic/redo/redothreads.png)
如上图所示,redo log的异步工作线程为4个,另2个异步辅助线程:分别是:log_writer, log_flusher, log_flush_notifier, log_write_notifier, log_checkpointer,log_close,log_flush_notifier /log_write_notifier为图中log notifier线程组,辅助线程为log_checkpointer, log_closer。
- [log_writer](#log_writer) : 负责将日志从log buffer写入磁盘的os page cache,并推进write_lsn(原子数据)
- [log_flusher](#log_flusher) : 负责fsync,并推进flushed_to_disk_lsn(原子数据)
- [log_write_notifier](#log_write_notifier) : 监听write_lsn,唤醒等待write_events的用户线程
- [log_flush_notifier](#log_flush_notifier) : 监听flushed_to_disk_lsn,唤醒等待log fsync的用户线程。
- [log_closer](#log_closer) : 1、在正常退出时清理所有redo_log相关lsn\log buffer相关数据结构;2、定期清理recent_closer的过老数据
- [log_checkpointer](#log_checkpointer) : 定期做checkpoint检查,根据flush list刷dirty page情况推进check point,释放log buffer等
在8.0 的实现中, 把一个write redo log 的操作分成了几个阶段
1. 获得写入位置, 实现: 用户线程
2. 写入数据到log buffer 实现: 用户线程
3. 将log buffer 中的数据写入到 redo log 文件 实现: log writer
4. 将redo log 中的page cache flush 到磁盘 实现: log flusher
5. 将redo log 中的log buffer 对应的page 添加到flush list
6. 更新可以打checkpoint 位点信息 recent_closed 实现: log closer
7. 根据recent_closed 打checkpoint 信息 实现: log checkpointer
线程间同步的条件变量:
- writer_event
- write_events[]
- write_notifier_event
- flusher_event
- flush_events[]
- flush_notifier_event
下面将依次介绍这些后台线程:
### **log_writer**
<span id="log_writer"></span>
在writer_event上等待用户线程唤醒或者timeout,唤醒后扫描recent_written,检测从write_lsn后,log buffer中是否有新的连续log,有的话就将他们一并写入page cache,然后唤醒此时可能等待在write_events[]上的用户线程或者等待在write_notifier_event上的log_write_notifier线程,接着唤醒等待在flusher_event上的log_flusher线程
```c++
|--> log_writer
| |--> /*endless loop*/
| |--> //Innodb将redo log buffer内容写入日志文件时需要保证不能存在空洞,
| |--> //即在写入前需要获得当前最大的无空洞lsn。这依赖LinkBuf。
| |--> //在后台写日志线程log_writer的log_advance_ready_for_write_lsn函数中完成。
| |--> log_advance_ready_for_write_lsn
| | |--> //获取之前的tail值,这里仅为验证作用
| | |--> log_buffer_ready_for_write_lsn
| | |--> //推进Link_buf::m_tail,同时回收之前空间
| | |--> log.recent_written.advance_tail_until(stop_condition)
| |--> log_buffer_ready_for_write_lsn
| |--> //Do the actual work
| |--> log_writer_write_buffer
| | |--> /* Do the write to the log files */
| | |--> log_files_write_buffer
| | | |--> notify_about_advanced_write_lsn
| | | | |--> //去唤醒write_notifier_event
| | | | |--> os_event_set(log.write_notifier_event);
| | | |--> log_update_buf_limit
```
### **log_write_notifier**
<span id="log_write_notifier"></span>
监控write_lsn,如果有增加,就去唤醒等待log落盘的用户线程(根据flush_log_at_trx_commit设置,用户commit操作会等待write_lsn推进)
```c++
log_write_notifier
{
/*endless loop*/
while(1)
{
sleep();
if (log.write_lsn.load() >= lsn)
{
while (lsn <= notified_up_to_lsn) {
const auto slot = log_compute_write_event_slot(log, lsn);
lsn += OS_FILE_LOG_BLOCK_SIZE;
//唤醒在等待的log_writer
os_event_set(log.write_events[slot]);
}
lsn = write_lsn + 1;
}
}
}
```
### **log_flusher**
<span id="log_flusher"></span>
log_flusher,在flusher_event上等待log_writer线程或者其他用户线程(调用log_write_up_to true)唤醒,比较上次刷盘的flushed_to_disk_lsn和当前写入page cache的write_lsn,如果小于后者,就将增量刷盘,然后唤醒可能等待在flush_events[]上的用户线程(调用log_write_up_to true)或者等待在flush_notifier_event上的log_flush_notifier
```c++
|--> log_flusher
| |--> /*endless loop*/
| |--> if (srv_flush_log_at_trx_commit != 1)
| |--> os_event_wait_time_low(log.flusher_event)
| |--> endif
| |--> if (last_flush_lsn < log.write_lsn.load())
| |--> log_flush_low
| |--> endif
```
### **log_flush_notifier**
<span id="log_flush_notifier"></span>
监控flushed_to_disk_lsn ,如果有增加就唤醒等待在 flush_events[slot] 上面的用户线程, 跟上面一样, 也是用户线程最后会等待在flush_events 上
```c++
log_flush_notifier
{
/*endless loop*/
while(1)
{
if (log.flushed_to_disk_lsn.load() >= lsn)
{
while (lsn <= notified_up_to_lsn) {
const auto slot = log_compute_flush_event_slot(log, lsn);
lsn += OS_FILE_LOG_BLOCK_SIZE;
//唤醒在等待的log_flusher线程
os_event_set(log.flush_events[slot]);
}
lsn = flush_lsn + 1;
}
}
}
```
### **log_closer**
<span id="log_closer"></span>
log_closer 这个线程是在后台不断的去清理recent_closed 的线程, 在mtr/mtr0mtr.cc:execute() 也就是mtr commit 的时候, 会把这个mtr 修改的内容对应start_lsn, end_lsn 的内容添加到recent_closed buffer 里面, 并且在添加到recent_closed buffer 之前, 也会把相应的page 都挂到buffer pool 的flush list 里面.
和其他线程不一样的地方在于, Log_closer 并没有wait 在一个条件变量上, 只是每隔1s 的轮询而已.
```c++
|--> log_closer
| |--> /*endless loop*/
| |--> log_advance_dirty_pages_added_up_to_lsn(log)
| | |--> //从recent_closed.m_tail 一直往下找, 只要有连续的就串到一起, 直到找到有空洞的为止
| | |--> //只要找到数据, 就更新m_tail 到最新的位置, 然后返回true。如果一条数据都没有,则返回false
| | |--> log.recent_closed.advance_tail_until(stop_condition)
| | | |--> //推进Link_buf::m_tail,同时回收之前空间
| | | |--> Link_buf<Position>::advance_tail_until
| | |--> //进行检查是否释放正确
| | |--> log.recent_closed.validate_no_links
| |--> sleep 1s
```
### **log_checkpointer**
<span id="log_checkpointer"></span>
MySQL 8.0中为了能够让mtr之间更大程度的并发,允许并发地给Buffer Pool注册脏页。类似与*log.recent_written*和*log_writer*,这里引入一个叫做*recent_closed*的*link_buf*来处理并发带来的空洞,由单独的线程*log_closer*来提升*recent_closed*的*tail*,也就是当前连续加入Buffer Pool脏页的最大LSN,这个值也就是下面提到的dpa_lsn。需要注意的是,由于这种乱序的存在,lwm_lsn的值并不能简单的获取当前Buffer Pool中的最老的脏页的LSN,保守起见,还需要减掉一个*recent_closed*的容量大小,也就是最大的乱序范围。
```c++
|--> log_checkpointer
| |--> /*endless loop*/
| |--> log_update_available_for_checkpoint_lsn(log)
| | |--> log_compute_available_for_checkpoint_lsn(log)
| | | |--> //当前连续加入Buffer Pool脏页的最大LSN: dpa_lsn
| | | |--> dpa_lsn = log_buffer_dirty_pages_added_up_to_lsn(log)
| | | |--> //获得当前所有脏页对应的最小REDO LSN:lwm_lsn
| | | |--> lwm_lsn = buf_pool_get_oldest_modification_lwm();
| | | |--> flushed_lsn = log.flushed_to_disk_lsn.load();
| | | |--> lsn = std::min(dpa_lsn, lwm_lsn, flushed_lsn)
| |--> /* Consider flushing some dirty pages. */
| |--> sync_flushed = log_consider_sync_flush(log);
| | |--> log_request_sync_flush
| | | |--> buf_flush_request_force
| | | | |--> //buf_flush_sync_lsn作为page cleaner线程同步刷脏的依据
| | | | |--> buf_flush_sync_lsn = lsn_target;
| | | | |--> os_event_set(buf_flush_event);
| | |--> log_update_available_for_checkpoint_lsn
| |--> /* Consider writing checkpoint. */
| |--> checkpointed = log_consider_checkpoint(log)
| | |--> log_checkpoint
| | | |--> log_files_write_checkpoint(log, checkpoint_lsn);
| | | | |--> fil_redo_io
```
# 参考资料
https://zhuanlan.zhihu.com/p/56583250
https://zhuanlan.zhihu.com/p/56188735
https://zhuanlan.zhihu.com/p/109417488
https://www.pagefault.info/2019/04/17/design-and-implementation-of-linkbuf-in-innodb.html
https://www.pagefault.info/2019/06/19/redo-log-design-and-implementation-in-innodb-1.html
http://mysql.taobao.org/monthly/2019/08/03/
http://mysql.taobao.org/monthly/2015/05/01/
http://liuyangming.tech/05-2019/InnoDB-Mtr.html
http://liuyangming.tech/08-2019/MySQL-8-flush-opt.html
https://mysqlserverteam.com/mysql-8-0-new-lock-free-scalable-wal-design/
http://mysql.taobao.org/monthly/2019/05/08/
https://leviathan.vip/2018/12/15/InnoDB%E7%9A%84redo-log%E5%88%86%E6%9E%90/
https://zhuanlan.zhihu.com/p/101360871
https://www.pagefault.info/2019/04/18/mtr-minitransaction-design-and-implementation.html
http://kernelmaker.github.io/InnoDB_redo_log
https://leviathan.vip/2018/12/15/InnoDB%E7%9A%84Redo-Log%E5%88%86%E6%9E%90/
http://mysql.taobao.org/monthly/2019/03/03/
http://mysql.taobao.org/monthly/2018/06/01/
以上是关于浅析MySQL 8.0 redo log的主要内容,如果未能解决你的问题,请参考以下文章
MySQL 8.0 BACKUP LOCK浅析及其在xtrabackup的应用(深度好文)