数据库 -- redo log日志

1. 介绍
若想让已提交的事务对数据库的修改永久生效,即使系统崩溃,重启后也可把这种修改恢复出来。没有必要在每次事务提交时就把该事务在内存中修改过的全部页面刷新到磁盘,只需要把修改的内容记录下来。称为redo log,好处如下:

  • redo日志占用的空间非常小
    存储表空间ID、页号、偏移量以及需要更新的值所需的存储空间是很小的。
  • redo日志是顺序写入磁盘的,使用顺序IO
    执行事务中,每执行一条语句,就可能有若干redo日志,并按产生的顺序写入磁盘。
    在这里插入图片描述
  • type:该条redo日志的类型,共53种不同的类型。
  • space ID:表空间ID。
  • page number:页号。
  • data:该条redo日志的具体内容。

为了保证原子性,即一条修改所产生的一系列redo,规定在执行这些需要保证原子性的操作时,必须以组的形式来记录的redo日志,在进行系统崩溃重启恢复时,针对某个组中的redo日志,要么把全部的日志都恢复掉,要么一条也不恢复。

在日志后边加上一条特殊类型的redo日志来标记,该类型名称为MLOG_MULTI_REC_END,该类型的redo日志结构很简单,只有一个type字段:
在这里插入图片描述
这样在系统崩溃重启进行恢复时,只有当解析到类型MLOG_MULTI_REC_END的redo日志,才认为解析到了一组完整的redo日志,才会进行恢复。否则的话直接放弃前边解析到的redo日志。

Mini-Transaction的概念
底层页面中的一次原子访问的过程称之为一个Mini-Transaction,简称mtr。向某个索引对应的B+树中插入一条记录的过程可看成是一个mtr,一个所谓的mtr可以包含一组redo日志,在进行崩溃恢复时这一组redo日志作为一个不可分割的整体。一个事务可以包含若干条语句,每一条语句其实是由若干个mtr组成,每一个mtr又可以包含若干条redo日志。

2. redo日志的写入过程
为了更好的进行系统崩溃恢复,通过mtr生成的redo日志都放在了大小为512字节的页中。为了和前边提到的表空间中的页做区别,这里把用来存储redo日志的页称为block(页和block的意思差不多)。一个redo log block的示意图如下:
在这里插入图片描述
redo日志缓冲区
为了解决磁盘速度过慢的问题而引入了Buffer Pool。同理,写入redo日志时也不能直接直接写到磁盘上,实际上在服务器启动时就向操作系统申请了一大片称之为redo log buffer的连续内存空间,即redo日志缓冲区,简称为log buffer。这片内存空间被划分成若干个连续的redo log block。
在这里插入图片描述

可以通过启动参数innodb_log_buffer_size来指定log buffer的大小,在MySQL
5.7.21这个版本中,该启动参数的默认值为16MB。

redo日志写入log buffer
向log buffer中写入redo日志的过程是顺序的,也就是先往前边的block中写,当该block的空闲空间用完后,再往下一个block中写。往log buffer中写入redo日志时,首先要确定写在哪个block的哪个偏移量处,InnoDB提供了一个称之为buf_free的全局变量,该变量指明后续写入的redo日志应该写入到log buffer中的哪个位置,如图所示:
在这里插入图片描述
一个mtr执行过程中可能产生若干条redo日志,这些redo日志是一个不可分割的组,所以并不是每生成一条redo日志,就将其插入到log buffer中,而是每个mtr运行过程中产生的日志先暂时存到一个地方,当该mtr结束的时候,将过程中产生的一组redo日志再全部复制到log buffer中。现在假设有两个名为T1、T2的事务,每个事务都包含2个mtr

  • 事务T1的两个mtr分别称为mtr_T1_1和mtr_T1_2。
  • 事务T2的两个mtr分别称为mtr_T2_1和mtr_T2_2。

每个mtr都会产生一组redo日志,用示意图来描述一下这些mtr产生的日志情况:
在这里插入图片描述
不同的事务可能是并发执行的,所以T1、T2之间的mtr可能是交替执行的。每当一个mtr执行完成时,伴随该mtr生成的一组redo日志就需要被复制到log buffer中,即不同事务的mtr可能是交替写入log buffer的。
在这里插入图片描述
从示意图中可以看出来,不同的mtr产生的一组redo日志占用的存储空间可能不一样,有的mtr产生的redo日志量很少,有的mtr产生的redo日志量非常大,比如mtr_t1_2产生的redo日志甚至占用了3个block来存储。

3. redo日志刷盘时机

  • log buffer空间不足时
    当前写入log buffer的redo日志量已经占满了log buffer总容量的大约一半左右,就需要把这些日志刷新到磁盘上。
  • 事务提交时
    在事务提交时可以不把修改过的Buffer Pool页面刷新到磁盘,但为了保证持久性,必 须要把修改这些页面对应的redo日志刷新到磁盘。
  • 后台线程不停的刷
    后台有一个线程,大约每秒都会刷新一次log buffer中的redo日志到磁盘。
  • 正常关闭服务器时
  • 做所谓的checkpoint 时
  • 将某个脏页刷新到磁盘
    脏页刷新到磁盘前,会保证先将该脏页对应的 redo 日志刷新到磁盘中(redo日志是顺序刷新的,所以在将某个脏页对应的 redo 日志从 redo log buffer 刷新到磁盘时,也会保证将在其之前产生的 redo 日志也刷新到磁盘)。

innodb_flush_log_at_trx_commit的用法
为了保证事务的持久性,用户线程在事务提交时需要将该事务执行过程中产生的所有redo日志都刷新到磁盘上。这要求会很明显的降低数据库性能。如果对事务的持久性要求不是那么强烈的话,可以选择修改一个称为innodb_flush_log_at_trx_commit的系统变量的值,该变量有3个可选的值:

  • 0:表示在事务提交时不立即向磁盘中同步redo日志,这个任务是交给后台线程做的。这样会加快请求处理速度,但是如果事务提交后服务器挂了,后台线程没有及时将redo日志刷新到磁盘,那么该事务对页面的修改会丢失。
  • 1:表示在事务提交时将redo日志同步到磁盘,可以保证事务的持久性。1也是默认值。
  • 2:表示在事务提交时将redo日志写到操作系统的缓冲区中,但并不需要保证将日志真正的刷新到磁盘。如果数据库挂了,操作系统没挂,事务的持久性还是可以保证的,但是操作系统也挂了的话,那就不能保证持久性了。

4. 磁盘中的redo日志文件组
MySQL的数据目录下默认有两个名为ib_logfile0和ib_logfile1的文件,log buffer中的日志默认刷新到这两个磁盘文件中,磁盘上的redo日志文件不只一个,而是以一个日志文件组的形式出现。这些文件以ib_logfile[数字](数字可以是0、1、2…)的形式进行命名。在将redo日志写入日志文件组时,是从ib_logfile0开始写,如果ib_logfile0写满,就接着ib_logfile1写,同理,ib_logfile1写满了就去写ib_logfile2,依此类推。如果写到最后一个文件该咋办?那就重新转到ib_logfile0继续写。
在这里插入图片描述
将log buffer中的redo日志刷新到磁盘的本质就是把block的镜像写入日志文件中。

redo日志文件组中的每个文件大小和格式都一样,由两部分组成:

  • 前2048个字节,也就是前4个block是用来存储一些管理信息的。
  • 从第2048字节往后是用来存储log buffer中的block镜像的。

所以循环使用redo日志文件,其实是从每个日志文件的第2048个字节开始算:

每个redo日志文件前2048个字节,如图:
在这里插入图片描述

  • log file header:描述该redo日志文件的一些整体属性
  • checkpoint1:记录关于checkpoint的一些属性,属性具体释义如下:
    在这里插入图片描述

Log Sequeue Number:LSN
规定初始的 lsn 值为8704(也就是一条redo日志也没写入时,lsn的值为8704)。在统计lsn的增长量时,是按照实际写入的日志量加上占用的log block header和log block trailer来计算。每一组生成的redo日志都有一个唯一的LSN值与其对应,LSN值越小,redo日志产生的越早。

系统第一次启动后初始化log buffer:
在这里插入图片描述
lsn值和redo日志文件偏移量的对应关系
因为lsn的值是代表系统写入的redo日志量的一个总和,一个mtr中产生多少日志,lsn的值就增加多少(当然有时候要加上log block header和log block trailer的大小),这样mtr产生的日志写到磁盘中时,很容易计算某一个lsn值在redo日志文件组中的偏移量,如图:
在这里插入图片描述
初始时的LSN值是8704,对应文件偏移量2048,之后每个mtr向磁盘中写入多少字节日志,lsn的值就增长多少。

checkpoint
redo日志只是为了系统崩溃后恢复脏页用的,如果对应的脏页已经刷新到了磁盘,则该redo日志占用的磁盘空间就可以被后续的redo日志所重用。即:判断某些redo日志占用的磁盘空间是否可以覆盖的依据就是它对应的脏页是否已经刷新到磁盘里。
在这里插入图片描述
如图,虽然mtr_1和mtr_2生成的redo日志都已经被写到了磁盘上,但是它们修改的脏页仍然留在Buffer Pool中,所以它们生成的redo日志在磁盘上的空间是不可以被覆盖的。之后随着系统的运行,如果页a被刷新到了磁盘,那么它对应的控制块就会从flush链表中移除:
在这里插入图片描述
全局变量checkpoint_lsn来代表当前系统中可被覆盖的redo日志总量是多少,初始值8704。比方说现在页a被刷新到了磁盘,mtr_1生成的redo日志就可以被覆盖了,我们可以进行一个增加checkpoint_lsn的操作,这个过程称为做一次checkpoint。

做一次checkpoint其实可以分为两个步骤:

  • 步骤一:计算一下当前系统中可以被覆盖的redo日志对应的lsn值最大是多少。
    redo日志可以被覆盖,意味着它对应的脏页被刷到了磁盘,只要我们计算出当前系统中最早修改的脏页对应的oldest_modification值,那凡是在系统lsn值小于该节点的oldest_modification值时产生的redo日志都是可以被覆盖掉的,我们就把该脏页的o_m 赋值给checkpoint_lsn。
  • 步骤二:将checkpoint_lsn和对应的redo日志文件组偏移量以及此次checkpoint的编号写到日志文件的管理信息(就是checkpoint1或者checkpoint2)中。上述关于checkpoint的信息只会被写到日志文件组的第一个日志文件的管理信息中,变量checkpoint_no,每做一次checkpoint,该变量的值就加1。当checkpoint_no的值是偶数时,就写到checkpoint1中,是奇数时,就写到checkpoint2中。

记录完checkpoint的信息之后,redo日志文件组中各个lsn值的关系就像这样:
在这里插入图片描述
批量从flush链表中刷出脏页
一般情况下都是后台的线程在对LRU链表和flush链表进行刷脏操作,这主要因为刷脏操作比较慢,不想影响用户线程处理请求。但是如果当前系统修改页面的操作十分频繁,这样就导致写日志操作十分频繁,系统lsn值增长过快。如果后台的刷脏操作不能将脏页刷出,那么系统无法及时做checkpoint,可能就需要用户线程同步的从flush链表中把那些最早修改的脏页(oldest_modification最小的脏页)刷新到磁盘,这样这些脏页对应的redo日志就没用了,然后就可以去做checkpoint了。

崩溃恢复

确定恢复的起点
checkpoint_lsn之前的redo日志都可以被覆盖,即这些redo日志对应的脏页都已经被刷新到磁盘中,既然它们已经被刷盘,就没必要恢复它们了。对于checkpoint_lsn之后的redo日志,它们对应的脏页可能没被刷盘,也可能被刷盘了,不能确定,所以需要从checkpoint_lsn开始读取redo日志来恢复页面。

redo日志文件组的第一个文件的管理信息中有两个block都存储了checkpoint_lsn的信息,选取最近发生的那次checkpoint的信息。衡量checkpoint发生时间早晚的信息就是所谓的checkpoint_no,只要把checkpoint1和checkpoint2这两个block中的checkpoint_no值读出来比一下大小,哪个的checkpoint_no值更大,说明哪个block存储的就是最近的一次checkpoint信息。这样就能拿到最近发生的checkpoint对应的checkpoint_lsn值以及它在redo日志文件组中的偏移量checkpoint_offset。

确定恢复的终点
写redo日志的时候都是顺序写的,写满了一个block之后会再往下一个block中写:
在这里插入图片描述

普通block的log block header部分有一个LOG_BLOCK_HDR_DATA_LEN的属性,该属性值记录了当前block里使用了多少字节的空间。对于被填满的block来说,该值永远为512。如果该属性的值不为512,那么它就是此次崩溃恢复中需要扫描的最后一个block。

怎么恢复
确定了需要扫描哪些redo日志进行崩溃恢复之后,假设现在的redo日志文件中有5条redo日志,如图:
在这里插入图片描述由于redo 0在checkpoint_lsn后边,恢复时可以不管它。现在按照redo日志的顺序依次扫描checkpoint_lsn之后的各条redo日志,按照日志中记载的内容将对应的页面恢复出来。不过设计InnoDB的大叔还是想了一些办法加快这个恢复的过程:

  • 使用哈希表

    根据redo日志的space ID和page number属性计算出散列值,把space ID和page number相同的redo日志放到哈希表的同一个槽里,如果有多个space ID和page number都相同的redo日志,那么它们之间使用链表连接起来,按照生成的先后顺序链接起来的,如图所示:
    在这里插入图片描述
    之后就可以遍历哈希表,因为对同一个页面进行修改的redo日志都放在了一个槽里,所以可以一次性将一个页面修复好(避免了很多读取页面的随机IO),这样可以加快恢复速度。另外需要注意一点的是,同一个页面的redo日志是按照生成时间顺序进行排序的,所以恢复的时候也是按照这个顺序进行恢复,如果不按照生成时间顺序进行排序的话,那么可能出现错误。

  • 跳过已经刷新到磁盘的页面
    在恢复时怎么知道某个redo日志对应的脏页是否在崩溃发生时已经刷新到磁盘了呢?每个页面都有一个称之为File Header的部分,在File Header里有一个称之为FIL_PAGE_LSN的属性,该属性记载了最近一次修改页面时对应的lsn值(其实就是页面控制块中的newest_modification值)。如果在做了某次checkpoint之后有脏页被刷新到磁盘中,那么该页对应的FIL_PAGE_LSN代表的lsn值肯定大于checkpoint_lsn的值,凡是符合这种情况的页面就不需要重复执行lsn值小于FIL_PAGE_LSN的redo日志了,更进一步提升了崩溃恢复的速度。

  • 18
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值