浅析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事务中的redo与undo

MySQL 8.0 BACKUP LOCK浅析及其在xtrabackup的应用(深度好文)

若依RuoYi框架浅析 部署篇②——CentOS 8 配置MySQL 8.0

浅析ReDoS

浅析如何将undo log从tablespace分离

浅析python日志重复输出问题