背景
经过前两篇文章可以了解到,对于InnoDB了提升性能会在内存中进行数据操作,但此时如果数据库突然崩溃内存中的数据来不及刷到磁盘中就会导致数据丢失,这对大部分应用来讲都是不可容忍的,为了解决这个问题InnoDB就引入了redo log。
Redo Log
redo log结构
innodb为了方便管理redo log也会把其进行分页存放,会把redo log存放在连续
的大小为512字节
的block中,如图:
其中每个redo log block的head占12个字节,包含:
- HDR_NO(4字节):block的唯一编号,从1开始;
- HDR_DATA_LEN(2字节):该block已经使用了多少字节,初始值为12,写满时为512;
- FIRST_REC_GROUP(2字节):第一个MTR生成的第一条redo的偏移量;(后续介绍);
- CHECKPOINT_NO(4字节):checkpoint序号;(后续介绍)。
tail占4个字节,包含:
- CHECKSUM(4字节):内容正确性校验值。
body占用496字节,它的内容由一条条redo log日志组成。而redo log日志本质就是记录了一下事务对数据进行的修改,为了方便描述innodb定义了多种类型的redo log,它的结构如下:
- type: redo log的类型;
- space id: 表空间编号;
- page no: 数据页对应的编号;
- data: 日志的具体内容。
为了让大家能更清晰的认识redo log格式,这里举个例子:
type为MLOG_1BYTE
它表示这一条在页面的某个偏移量位置写入1个字节内容的日志;此时结构图如下:
这只是最简单的一种type,实际上一个数据页除了真实数据还包含页头、页尾等内容,更改一个页中的数据通常都会把该页中的很多字段进行修改,为此innodb为redo log设计了很多复杂的类型,他们本质上就是一些定制的规则没必要记。
组提交(Mini-Transaction)
上述内容介绍了redo log的结构,但实际写数据的过程中可能既要写数据页也要写索引页此时就需要记录多个page no
的redo log日志,而这些日志都是原子不可分割,为此innodb提供了组提交redo log,对于这种一个sql语句会产生的一组redo log,把它称为MTR,每个组的最后一条日志类型都为MLOG_MULTI_REC_END
,这样就可以区分每个组都包含哪些redo log。如图:
此时会发现一个问题,如果只需要更改一个表空间难道也要增加一个类型为MLOG_MULTI_REC_END
的redolog么?这样会浪费一半的空间,对此innodb在type上做一个小动作,type目前有五十多种,使用后7位(128种)来表示类型,第1位表示是否为只有一条的redo log。
在介绍redo log结构
时每个block head中都有一个FIRST_REC_GROUP
,当一组redo log(单条redo log也算一个组)是第一个写入到该block时,就会把它的offset赋值给FIRST_REC_GROUP
。
写入过程
通过上面两个小章节我们知道了redo log结构以及每组redo log是什么样子的,那么如何往redo log里面写呢?innodb对于操作redo log也是在内存中进行操作,而上图中的redo log block就是redo log的内存格式,每次写redo log时会顺序往redo log buffer中写入,同时innodb提供了一个buf_free的全局变量用来维护后续从哪个位置写入buffer。如图:
注意:这里每次写入一个MTR,而不是产生一个redo log就写入一次。一个事务中可能会有多个MTR,每次事务产生的MTR在redo log buffer中可能是穿插写入的。
上面介绍了redo log在内存中的写入,那它在磁盘中是什么样的结构以及什么时间写入磁盘的呢?磁盘结构如图:
目前我们线上就配置了3个1G大小的redo log file,所以这里我也只画了3个,前面4个block占用2048个字节负责维护一些基本信息,后面的block用来存储真实数据。磁盘这里有2个checkpoint,我理解一个是可以优化并发性能、一个是在崩溃时会发生很多意想不到的事情例如更新checkpoint时发生异常导致这个checkpoint不能使用,此时还可以使用另一个进行兜底。
innodb给我们提供了一个innodb_flush_log_at_trx_commit
的参数,根据修改这个参数值来控制在什么时间把buffer中的数据刷到磁盘上。
- 0: 事务提交时不会立即同步日志,而是交给后台线程来处理,如果后台线程没来得及处理那么redo log就会丢失;
- 1: 事务提交时会直接同步到磁盘中,设置这个参数可以保证我们的数据在一定条件下不会丢失;(磁盘坏了怎么都保证不了)
- 2: 事务提交时写到操作系统缓冲区中,但并不保证日志真刷到磁盘了,如果mysql服务挂了,但操作系统还正常运行,操作系统是可以把缓冲区中数据持久化的。
注意:当这个属性设置为1
时,由于每组mtr都会写到redo log buffer,不同事务可能有交叉的redo log,如果A、B两个事务同时运行,A事务提交了也会把B事务的写入到redo log buffer中的redo log进行刷盘的。
lsn与checkpoint
可以看到redo log是一个环形文件它总会有写满的时候,但随着mysql的运行会不断的把内存中的脏页刷到磁盘中的,由于这些脏页已经刷盘持久了,那他们对应的redo log其实就是没用的了。innodb设计了lsn和checkpoint来保证redo log file的可重复使用以及尽力最小颗粒的崩溃恢复。
lsn可以理解为redo log的大小,如图:
初始值为8704,除了计算redo log block body本身也要计算head和tail的字节,例如MTR1为100个字节,上图就是写到各个位置时lsn的值。在最初版本时lsn是一个4字节无符号的整数,到MySQL5.6.3中变成了一个8字节的无符号整数,这意味着它几乎是无穷大仅受限于配置。
在上一篇关于buffer pool的文章中有介绍控制块,这个lsn值也会更新到控制块中,如图:
当控制块a对应的页第一次被修改,生成的MTR1这组redo log,此时就会如上图,把控制块a的o_m和n_m分别赋值lsn,其中o_m只有第一次修改该数据页时才会把MTR开始的lsn进行赋值,而n_m每次修改都会把redo log结尾对应的lsn进行赋值。数据页的第一次修改就都会加入到flush链表的头部,整个链表是按照o_m倒序排列的,这样每次从尾部拿出对应的数据页总是能把先更改的数据进行刷盘,这样redo log先产生的日志就可以被废弃。
但如何知道哪些脏数据页已经被刷盘了呢?此时innodb就提出来checkpoint概念,checkpoint主要是维护一个checkpoint_lsn,当一个数据页刷盘了,该页对应数据块的o_m就会赋值给checkpoint_lsn,此时就知道redo log file从哪个lsn开始之前的日志都可以进行废弃了。同时还需要把这个checkpoint_lsn写到上面介绍的redo log file
头中的checkpoint1
或者checkpoint2
,其中当为偶数时写入checkpoint1,奇数时写入checkpoint2。
崩溃恢复
redo仅仅是为了在mysql崩溃时进行数据恢复的;可以看到为了数据的持久性innodb做了很多设计,那当崩溃时如何恢复呢?
在这些redo log file中找到最大的checkpoint,它就是我们最近做checkpoint的lsn值,小于该值的数据页都已经进行刷盘了不需要重做,用这个lsn值去对比redo log block头中的checkpoint no,找到最后一个checkpoint no比这个lsn小的block,开始从该block开始进行重做。
由于redo log是一条一条存储的,而数据恢复是可以按页进行恢复的,为了加快恢复速度innodb会以space id
和page no
进行分组,这样恢复起来就可以一页一页恢复了,而有的页可能已经刷盘了但是checkpoint没有更新,此时需要比较每个页中的lsn值,该值记录最近一次修改页面时对应的lsn值(n_m值)。
总结
- redo log由连续的block组成,一个block分为头、体、尾;
- redo log分为日志缓冲区和日志文件。事务的修改首先被写入日志缓冲区,这个缓冲区位于内存中。当满足一定条件时,日志缓冲区中的内容会被刷新到磁盘上的日志文件中。日志文件通常有多个,以循环的方式使用,同时redo log是顺序写的,要比直接写数据文件快的多。
- 在数据库运行过程中,InnoDB会不断地将日志缓冲区中的内容写入日志文件,这个过程称为 checkpoint(检查点)。checkpoint的目的是确保数据库在发生崩溃时,能够从最近的一个检查点开始恢复,而不需要从头开始处理所有的日志记录。
- 当数据库发生崩溃时,InnoDB会在启动过程中读取redo log文件,并根据其中的记录将数据库恢复到崩溃前的状态。这一过程确保了事务的持久性,即一旦一个事务被提交,它的修改就不会因为数据库的崩溃而丢失。
- 此外,redo log的存在还可以提高数据库的并发性能。由于事务的修改可以先记录到redo log中,而不是立即写入数据文件,因此多个事务可以并发地进行修改,而不会相互阻塞。
创作不易,觉得文章写得不错帮忙点个赞吧!如转载请标明出处~