什么是redo日志

InnoDB采用页为单位管理存储空间,我们对一张或多张表进行增删改查都必须将数据所在的页面从磁盘中加载到内存中,

在数据库的持久性特性里,如果一个事务已经提交了,数据是不允许被丢失的,称为持久性,当我们在内存中修改完数据后,并且提交事务了,此时机器突然断电或其他原因,导致内存中的数据丢失了,

这就不符合一致性的要求了,因此为了保证持久性,我们可以将数据写入磁盘后,再让事务返回提交成功,但是这么做会有一些问题:

  1. 刷新完整的数据页太浪费。一个页面16个字节,如果一个事务只修改了一个页面的一个字节,也进行刷盘,会太浪费资源。

  2. 随机刷新页面太慢。一个事务操作可能同时操作了太多的页面,这些页面在磁盘中并不相邻,这时磁盘需要随机IO刷新,速度太过于慢。

redo日志格式

一条redo日志格式如下:

  1. type: 该日志的类型
  2. space ID: 表空间ID
  3. page number: 页号
  4. data: redo日志的具体内容

1. 简单的redo日志类型

在对页面的改动比较简单的情况下,只需要记录对某个页面的某个偏移量进行了几个字节的修改,修改后的内容是什么,简单的redo日志类型有下面几种:

  1. mlog_1byte
  2. mlog_2byte
  3. mlog_4byte
  4. mlog_8byte
  5. mlog_write_string

上面1-4的类型是一样的,仅仅是data长度不同,最后一个string,意味着不能确定具体数据占用多少字节,所以需要在结构中新增一个len字段:

Max Row ID

Innodb中,一张表既没有主键,又没有不可为空的唯一索引时,将会为表添加一个名为row_id的隐藏列作为主键。

row_id的赋值方式如下:

  1. MySQL会在内存中维护一个全局变量,每次隐式列新增一条记录,将把当前的全部变量值赋值给这个隐式列,并且全局变量自增1。

  2. 当该全局变量值为256的倍数时,将变量刷到系统表空间页号为7的页面中一个名为Max Row ID的属性中,之所以不每次都刷,是为了避免频繁刷盘带来性能影响。

  3. MySQL重启时,将磁盘中的该值加载到内存中,并且加上256(避免上次重启时没刷盘,实际内存中大于磁盘的值,导致重复)。

Max Row ID 占用8字节的空间,因此写redo log时,类型就是 mlog_8byte 。

2. 复杂的redo日志类型

绝大多数的一条SQL,执行一条语句时,会修改非常多的页面,系统数据页面、用户数据页面(聚簇索引、二级索引),以insert为例,既要更新索引,也可能更新系统数据页面中的Max Row ID,具体如下:

  1. 一个表中有多少索引,insert时就需要修改多少B+树
  2. 对于一颗B+树,需要更新叶子节点页面,也可能更新内节点页面,还可能创建新页面
  3. 更新Page Directory中的槽信息
  4. 更新Page Header中的统计信息,page_n_dir_slots、page_heap_top、page_n_heap....
  5. 维护上一条记录的next_record指向

这么多的修改,按照上面说的简单日志类型记录实现,有两种方案

  1. 在每个修改的地方加上一条redo日志。
  2. 将整个页面第一个修改的字节到最后一个修改字节之间所有的数据当成一条redo日志中的数据。

这两种方式都有一些弊端,第一种,如果一个页面中修改的地方太多,可能redo日志占用的空间比整个页面都多,第二种,如果第一个字节到最后一个字节之间有很多没有修改的地方,放到redo中又太浪费空间。

因此Innodb根据不同的操作,设计了不同的日志类型

  1. mlog_mlog_rec_insert:插入一条使用非紧凑行格式的记录
  2. mlog_comp_rec_insert:插入一条使用紧凑行格式的记录
  3. mlog_comp_page_create:创建一个使用紧凑行格式记录的页面
  4. mlog_comp_rec_delete:删除一条使用紧凑行格式的记录
  5. mlog_comp_list_start_delete:删除多条紧凑行数据的开始
  6. mlog_comp_list_end_delete:删除多条紧凑行数据的结束
  7. mlog_zip_page_compress:压缩一个数据页

例如mlog_comp_list_start_delete和mlog_comp_list_end_delete,如果是区间删除,每条记录都使用redo记录,那么将会占用很多的空间,用start和end可以节约空间。

redo日志/逻辑层面含义/物理层面含义

从物理层面来看,redo日志是记录了哪些表的哪些页面改成了什么值,但是很多时候并不能直接将这种日志直接作为恢复页面的手段,

例如一条insert记录,其中并不会记录将页面中的 page_n_dir_slots、page_heap_top、page_n_heap 改为什么,只是将该页面中插入一条记录的必备要素记录下来,

恢复时,将调用向某个页面插入一条记录的相关函数,redo日志中的记录参数,将被作为函数入参,page_n_dir_slots、page_heap_top、page_n_heap通过调用函数来恢复到崩溃前的样子,这就是逻辑层面的意思。

Mini-Transaction(MTR) 以组的形式写入redo日志

一条SQL执行的过程中,可能会修改多个页面(系统表空间、聚簇索引、二级索引),在执行的过程中产生的redo日志,被划分为若干个不可分割的组:

  1. 更新Max Row ID属性为一组,不可分割。
  2. 向聚簇索引插入一条记录为一组,不可分割。
  3. 向二级索引插入一条记录为一组,不可分割。
  4. 其他。

不可分割的含义

向一个B+树中新增一条记录时,可能会发生下面两种情况:

  1. 乐观插入:该页面剩余空间足够充足,直接将该记录插入该页面,然后记录一条 mlog_comp_rec_insert 类型的redo日志。

  2. 悲观插入:该页面剩余空间不足,需要分裂新的页面,将原先数据页的一部分复制到新的数据页,然后再将记录插入进去,再将该叶子节点加入到叶子节点链表中,最后在内节点中添加一条目录项指向新创建的页面,这个过程会产生很多的redo日志。

redo日志为了实现MySQL崩溃重启时,恢复到崩溃前的状态,如果悲观插入的过程中只执行了部分,那么恢复时B+树将被恢复为一种不正确的状态,

因此Innodb要求执行这些需要原子性的操作时,必须以组的形式记录redo日志,恢复时要么全部恢复,要么一条也不恢复。

redo原子性的实现

Innodb使用下面两种方式判断redo是否满足原子性的要求。

  1. 需要原子性操作的redo日志,在结束时追加一条类型为 mlog_multi_rec_end 的redo作为结尾。

  2. 只有一条redo日志,判断redo的type字段的第一个bit是否为1,为1代表只有一条日志,没有上面的结尾。

小结

一个事务会产生多个语句,每个语句会产生多个MTR,每个MTR又包含若干个redo日志。

redo日志的写入过程

redo日志被写入到一个称为 redo log block 的页中,该页大小为 512 字节,同样,redo log不可能被同步写入到磁盘中,

而是写入到了一个叫做 redo 日志缓冲区 (log buffer) 的内存区域中,该区域在MySQL启动时向操作系统申请,这片区域被划分为若干个连续的 block,可以通过启动选项 innodb_log_buffer_size 指定 log buffer 的大小。

向log buffer写入日志的过程是顺序写入,因此需要一个变量 buf_free 记录此刻应该写在哪个偏移量位置上。

redo的写入是以一个MTR作为一个组进行写入block中,在该MTR没结束时,会暂存到一个地方,同时事务是会并发执行的,因此最终写入到block中的MTR也可能是多个事务的交叉,一个MTR也可能占用多个页面。

redo log block 结构

一个 block 页分为三个部分,页头,页尾,还有body,页头占12字节,页尾占4字节,body占496字节,日志实际上都是写在body中的,页头页尾用于存储其他信息。

页头的结构属性:

  1. log_block_hdr_no:block的编号值(> 0)

  2. log_block_hdr_data_len:block使用了多少字节,初始值为12(body 从第12个字节开始),随着内容的增加值也会增长,如果满了,就是512

  3. log_block_first_rec_group:block中第一个MTR生成的redo日志记录组偏移量(因为很多MTR会跨block,因此这个值也意味着上一个MTR的结束)

  4. log_block_checkpoint_no:checkpoint的序号

页尾的结构属性:

  1. log_block_checksum:block的校验值,用于正确性校验

redo日志文件

1. redo日志刷盘时机

redo存在log buffer内存中,总是需要刷到磁盘中的,刷到磁盘中有以下几个时机:

  1. log buffer空间不足时
  2. 事务提交时
  3. 后台有一个线程,1s左右进行刷盘
  4. 正常关闭服务器
  5. 做checkpoint时

2. redo日志文件组

redo日志文件名可以通过 show variables like 'datadir' 查看,默认有 ib_logfile0ib_logfile1 两个文件,

默认情况下刷新到这两个磁盘文件中,可以通过下面几个启动选项调节redo日志文件:

  1. innodb_log_group_home_dir: redo日志文件所在的目录,默认是当前数据目录
  2. innodb_log_file_size: 指定日志文件大小,在5.7.22中默认为48MB
  3. innodb_log_file_in_group: 指定日志文件个数,以ib_logfile[数字]形式命名

redo日志文件实际大小 = innodb_log_file_size * innodb_log_file_in_group,如果写到最后一个文件发现写满了,将从头开始写起,

从头写起会引起覆写,覆盖掉之前的redo日志,因此后续有一个checkpoint的概念是用于解决这个问题的。

3. redo日志文件格式

log buffer是一片连续的内存空间,被划分为若干个512字节大小的block,写到文件中,就是将内存中的镜像刷到磁盘中,所以日志文件也是由若干个512字节大小的block组成。

日文文件前2048个字节(4个block)用于存储管理信息,从2048往后的字节用于存储log buffer中的block镜像。

4个block(log block header)的格式如下:

  1. log file header:描述该redo日志文件的一些整体属性
  2. checkpoint1:记录关于checkpoint的一些属性
  3. 没用
  4. checkpoint2:与checkpoint1一样

log file header属性:

  1. log_header_format: redo日志的版本, 在5.7.22中永远为1 (4字节)
  2. log_header_pad1: 用于字节填充 (4字节)
  3. log_header_start_lsn: 标记 redo 日志文件偏移量2048字节处对应的lsn值 (8字节)
  4. log_header_creator: 标记日志文件的创建者是谁,正常运行时该值为MySQL的版本号,使用mysqlbackup命令创建redo日志文件时,该值为 ibbackup 和 创建时间 (32字节)
  5. log_block_checksum:block的校验值 (4字节)

checkpoint1属性:

  1. log_checkpoint_no:服务器执行checkpoint的编号,每执行一次checkpoint,该值+1 (8字节)
  2. log_checkpoint_lsn:服务器再结束checkpoint时对应的lsn值,系统崩溃恢复时从该值开始 (8字节)
  3. log_checkpoint_offset:上个属性中的lsn值在redo日志文件组中的偏移量 (8字节)
  4. log_checkpoint_log_buf_size:服务器在执行checkpoint操作时对应的log buffer大小 (8字节)
  5. log_checkpoint_checksum:block的校验值 (4字节)

log sequence number (lsn)

lsn用于记录当前已经写入的redo日志量,该值是一个不断增长的值,初始值为8704,当写入一个MTR到log buffer中时,lsn的值也会增长MTR占用的字节数(跨block则需要加上block页头和页尾的字节大小),每一组MTR都有对应的lsn值,该值越小,则说明redo日志产生的越早。

1. flushed_to_disk_lsn

lsn代表写入到log buffer中的日志量,之后会在一些时间点写入到日志文件中,flushed_to_disk_lsn变量用于表示有多少log被写入到日志文件中了,

该值初始化时与lsn一样,都是8704,随着系统运行,redo不停的写入,逐渐会和lsn拉开距离,如果该值和lsn一致,说明所有的日志都被写入到了文件中。

2. lsn值与redo日志文件组中偏移量的对应关系

初始化时,lsn的8704对应日志文件组中的2048,随后二者随着lsn的累加,偏移量也跟着累加(在多个日志文件中需要考虑前4个block对偏移量的影响)。

3. flush链表中的lsn

一次MTR代表对页面的一次原子操作,MTR在要写入redo buffer后,还需要将这个过程中修改的页面加入到 Buffer Pool 中的 flush 链表中,

在第一次修改某个页面后,会将该页面对应的控制块放到flush链表的头部,之后修改该页面时,不会再次插入,

在这个过程中,控制块中会记录两个关于页面何时修改的属性:

  1. oldest_modification: 第一次修改该页面时,将该页面对应的MTR开始对应的lsn值写入该属性

  2. newest_modification: 每修改一次页面,将MTR结束时对应的lsn写入该属性

flush链表的顺序按照时间倒序排列,即最近修改的在前面,最后修改的在后面,被多次更新的页面不会被重复插入到flush链表中,而是更新 newest_modification 的值。

checkpoint

redo日志文件的容量是有限的,如果写满了,将会从头写起,这样会发生追尾的情况,redo日志用于在系统崩溃时恢复脏页使用,如果脏页已经被刷到磁盘中,那么该redo日志也没必要存在了,因此,判断redo日志是否可以被覆盖,需要判断其对应的脏页是否已经被刷入到磁盘中。

为了表示当前可以被覆盖的redo日志总量是多少,Innodb中有一个checkpoint_lsn全局变量,该变量初始值为8704,当某个脏页被刷到磁盘中的时候,该值就可以增加一次。

执行一次checkpoint分为两步:

  1. 计算当前系统中可以被覆盖的redo日志对应的lsn值最大是多少

    获取flush链表中最早修改的脏页,取其属性oldset_modification,意思就是小于该值的redo日志,都可以被覆盖,将该值赋值给 checkpoint_lsn。

  2. 将 checkpoint_lsn 与对应的redo日志文件组偏移量以及此次 checkpoint 的编号写到日志文件的管理信息中

    Innodb维护了一个 checkpoint_no 的变量,用来统计系统执行了多少次checkpoint,每执行一次checkpoint,该变量值+1,

    发生checkpoint时,计算出该 checkpoint_lsn 对应在redo日志文件中的偏移量 checkpoint_offset,最后将 checkpoint_no、checkpoint_offset、checkpoint_lsn 写入到 redo日志文件组的第一个日志文件的管理信息中,

    当 checkpoint_no 的值是偶数时,将信息写到checkpoint1中,是奇数时写到checkpoint2中。

执行一次checkpoint和刷新脏页到磁盘中,其实是不同的线程执行,二者也并不相等,执行checkpoint需要修改redo日志文件的管理信息,有一定的开销。

用户线程批量从flush链表中刷出脏页

一般情况下,刷新脏页的操作由后台线程执行,但是如果当前系统修改页面十分频繁,redo日志大量写入,系统lsn值增长过快,

如果后台线程的刷脏操作不能即使将脏页刷出,系统将无法及时执行checkpoint,此时就需要用户线程将flush链表中的脏页刷盘,然后系统就可以继续执行checkpoint操作。

查看系统中的各种lsn值

使用 show engine innodb status 命令,从大量的输出中可以看到下面的几个参数:

LOG
---
Log sequence number 216677000
Log flushed up to   216677000
Pages flushed up to 216677000
Last checkpoint at  216676991
0 pending log flushes, 0 pending chkp writes
10 log i/o's done, 0.33 log i/o's/second
----------------------
复制代码
  1. Log sequence number: lsn值,已经写入的redo日志量。
  2. Log flushed up to:已经写入磁盘的redo日志量。
  3. Pages flushed up to:flush链表中最早修改的页面对应的 oldset_modification。
  4. Last checkpoint at:当前系统的checkpoint_lsh值。

innodb_flush_log_at_trx_commit

一般为了严格保证事务的持久性,在事务提交时,需要将所有的redo日志都刷到磁盘中,这样会降低数据库的性能,如果对事务持久性要求不强烈,可以对 innodb_flush_log_at_trx_commit 进行调控,该变量有3个值:

0:事务提交时,不立即刷盘,交给后台线程处理,可提高处理速度,但是如果发生没刷盘时宕机,该事务的修改将丢失。

1:默认值,每次事务提交时,立即将redo日志刷入磁盘。

2:事务提交时,redo日志提交给操作系统的缓冲区,如果数据库挂了,系统没挂,可以保证持久性,如果系统也挂了,数据也会丢失。

崩溃恢复

当数据库挂掉的时候,redo日志有助于让数据库重新启动并恢复到挂掉之前的状态中,恢复状态的步骤大概如下:

1. 确定恢复的起点

  1. 对于lsh值小于checkpoint_lsn的redo日志,说明这些日志对应的页面已经被刷到磁盘中,这些日志则没必要进行恢复,对于不小于checkpoint_lsn的日志,因为刷盘是异步操作,可能也被刷到磁盘中了,这里不能确定,因此要选择lsn值为checkpoint_lsn的redo日志开始进行恢复。

  2. redo日志文件组中的第一个文件管理信息中,有checkpoint1和2两个block都存储了checkpoint_lsn,我们选择其中最大的那个,也就是最近一次的checkpoint,然后确定其在文件中的偏移量checkpoint_offset,这就是恢复的起点了。

2. 确定恢复的终点

redo日志写入是顺序写入的,必须写满一个block后再写下一个,因此定位到最后一个没写满的block,就可以知道恢复的终点了,

每个block的页头部分有一个 log_block_hdr_data_len 属性,每个block大小是512字节,该值记录block使用了多少字节,如果该值不是512,那么就说明这个是最后一个block。

3. 如何恢复

确定了起点和终点后,从起点开始顺序扫描恢复页面是没问题的,不过MySQL为了加快恢复速度,还使用了下面的两种优化手段:

1. 生成哈希表

哈希表主要针对相同的spaceId和page number计算出hash值,然后将hash值相同的放到一个槽中,在同一个槽中的数据按照生成的先后顺序进行链表连接。

之后遍历哈希表,对同一个页面的修改都在一个槽中,因此遍历一个槽就可以恢复好一个页面,这可以避免很多次读取页面的随机IO,加快恢复速度,

在槽中的链表必须按照生成顺序排列,否则恢复的过程中就会发生错误,例如新增和删除的顺序倒过来了。

2. 跳过已经刷新的磁盘中的页面

因为脏页刷盘是后台线程异步操作的,可能执行最近一次checkpoint后,有些页面又被刷到磁盘中了,对于这种我们没必要使用redo日志重新恢复一次,

如何知道该页面有没有被异步线程刷到磁盘中?每个页面中都有一个 fil_page_lsn 的属性,记录了最后一次修改页面的lsh值,redo恢复时,如果fil_page_lsn的值大于checkpoint_lsn的值,说明该页已经被刷盘,无需重新恢复一次,以此又可以加快恢复速度。

LOG_BLOCK_HDR_NO是如何计算的

MySQL规定redo日志文件组中的所有文件大小总和不能超过512GB,一个block大小是512字节,因此系统中存在的block块最多是1G个,因此block的编号有1G个不重复的就足够使用,所以LOG_BLOCK_HDR_NO的计算公式如下:

LOG_BLOCK_HDR_NO = ((lsn/512) & 0x3FFFFFFF) + 1 = 2^30 = 1G

另外 LOG_BLOCK_HDR_NO 值的第一个bit被称为flush bit,如果该值为1,表示该block是log buffer中的block写入到磁盘时,第一个被刷入的block。

MySQL之redo日志相关推荐

  1. mysql的redo日志_MySQL redo与undo日志解析

    前言: 前面文章讲述了 MySQL 系统中常见的几种日志,其实还有事务相关日志 redo log 和 undo log 没有介绍.相对于其他几种日志而言, redo log 和 undo log 是更 ...

  2. 【经验】GaussDB(for MySQL)性能优化 —— 日志的“快递驿站”

    摘要:GaussDB(for MySQL)数据库在写入性能上,在业界同类产品中是最好的,这主要得益于GaussDB(for MySQL)在MySQL内核方面的诸多优化.其中有一项从"送快递& ...

  3. mysql存储物流信息_【经验】GaussDB(for MySQL)性能优化 —— 日志的“快递驿站”...

    GaussDB(for MySQL)数据库在写入性能上,在业界同类产品中是最好的,这主要得益于GaussDB(for MySQL)在MySQL内核方面的诸多优化.其中有一项从"送快递&quo ...

  4. MySQL之 事务日志: redo和undo

    目录 1 概述 2 redo日志 2.1 redo log和二进制日志的区别 2.2 redo日志的基本概念 2.3 日志块(log block) 2.4 redo log的格式 2.5  日志刷盘的 ...

  5. mysql数据库undo日志恢复_MySQL的undo/redo日志和binlog日志,以及2PC

    发现自己的知识点有点散,今天就把它们连接起来,好好总结一下. 一.undo log.redo log.binlog的定义和对比 定义和作用 所在架构层级 日志形式 所在文件和默认名称,组织结构 是否缓 ...

  6. 认真学习MySQL的事务日志-Redo日志

    事务有4种特性:原子性.一致性.隔离性和持久性.那么事务的四种特性到底是基于什么机制实现呢? 事务的隔离性由锁机制执行. 事务的原子性.一致性和持久性由事务的redo日志和undo日志来保证. red ...

  7. MySQL是怎样运行的:从根儿上理解MySQL | redo日志(上)

    文章目录 前言:本博文是对MySQL是怎样运行的:从根儿上理解MySQL这本书的归纳和总结 20.redo日志(上) 1.redo日志是个啥 1.1 回忆回忆 1.2 redo正式登场 2.redo日 ...

  8. Linux系统查看mysql redo日志的位置

    redo日志文件名格式为 ib_logfile0或ib_logfile1 可使用find命令模糊查找,如下图:

  9. mysql有多少种日志_MySQL到底有多少种日志类型必须我们记住的!

    MySQL中有六种日志文件,分别是:重做日志(redo log).回滚日志(undo log).二进制日志(binlog).错误日志(errorlog).慢查询日志(slow query log).一 ...

  10. oracle 5632,Oracle系统默认临时表空间以及redo日志文件问题处理

    本人现在要把Oracle的数据同步到MySQL,运用的ETL工具,由于数据量很大,而且有子查询要用到临时表空间,导致原来的该临时表空间 问题:本人现在要把Oracle的数据同步到MySQL,运用的ET ...

最新文章

  1. Java 多线程(三)线程间的通信jdk1.5中Lock,Condition---生产者消费者为例
  2. 栈应用之 括号匹配问题(Python 版)
  3. php 如何 闭源,Linux_Debian如何安装闭源软件包有哪些方法,  在系统操作中,闭源软件 - phpStudy...
  4. 电子病历模板_年会献礼3:浮针专家平台病历撰写系统年会启动
  5. 前端面试题vue-element汇总
  6. python计算长方体体积最简单代码_python处理DICOM并计算三维模型体积
  7. C语言程序设计基础讲座之函数
  8. 顶会VLDB‘22论文解读:CAE-ENSEMBLE算法
  9. ES6 中的 Set、Map 和 WeakMap
  10. CSS样式的简单使用
  11. python中除法运算_python除法运算
  12. IP地址、子网掩码、网络地址之间相关的计算
  13. 什么是Windows驱动程序?
  14. 一个使用CC2530实现的Zigbee红绿灯
  15. 干货丨时序数据库DolphinDB脚本语言的混合范式编程
  16. Service的两种启动方式
  17. 【在SpringBoot项目中使用Validation框架检查数据格式-常用的检查注解】
  18. 【IT互联网行业内,什么岗位工作更有前景?】
  19. Unity实现按Esc键控制面板出现消失,同时游戏暂停
  20. 构造拉丁方阵和正交拉丁方阵组

热门文章

  1. 计算机电子贺卡制作圣诞节,如何制作电子圣诞贺卡?贺卡制作步骤
  2. 2020届校园招聘360笔试题
  3. Pygame小工具:模拟键盘 - 虚拟键盘(Keyboard)
  4. kappa系数简介---一致性与分类准确度指标
  5. 使用video speed controller给视频加速
  6. TP礼物钻石投票评选男神女神萌娃商家投票系统源码简介下载
  7. 读史可以明智_明智之举:获得满意的广告
  8. OpenCV每日函数 图像过滤模块 (14) medianBlur中值滤波函数
  9. linux返回根目录的命令
  10. php-fpm 端口号,PHP-FPM 配置说明