数据库 -- redo log日志

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

  • redo日志占用的空间非常小
    存储表空间ID、页号、偏移量以及需要更新的值所需的存储空间是很小的。
  • redo日志是顺序写入磁盘的,使用顺序IO
    执行事务中,每执行一条语句,就可能有若干redo日志,并按产生的顺序写入磁盘。
    数据库 -- redo log日志
    为了保证原子性,即一条修改所产生的一系列redo,规定在执行这些需要保证原子性的操作时,必须以组的形式来记录的redo日志,在进行系统崩溃重启恢复时,针对某个组中的redo日志,要么把全部的日志都恢复掉,要么一条也不恢复。

在日志后边加上一条特殊类型的redo日志来标记,该类型名称为MLOG_MULTI_REC_END,该类型的redo日志结构很简单,只有一个type字段:
数据库 -- redo log日志
2. redo日志的写入过程
把生成的redo日志都放在一个redo log block 中:
数据库 -- redo log日志
与buffer pool 一样,写入redo日志时不直接写到磁盘上,服务器启动时向操作系统申请称为redo log buffer的连续内存空间,就像这样:
数据库 -- redo log日志
向log buffer中写入redo日志的过程是顺序的,前边说过一个执行过程中可能产生若干条redo日志,这些redo日志是一个不可分割的组,所以运行过程中产生的日志先暂时存到一个地方,当该执行过程结束时,将过程中产生的一组redo日志再全部复制到log buffer中。
数据库 -- redo log日志
3. redo日志刷盘时机

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

4. 磁盘中的redo日志文件组
MySQL的数据目录下默认有两个名为ib_logfile0和ib_logfile1的文件,log buffer中的日志默认情况下就是刷新到这两个磁盘文件中,以一个日志文件组的形式出现,可手动增加。
将log buffer中的redo日志刷新到磁盘的本质就是把block的镜像写入日志文件中,

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

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

所以循环使用redo日志文件,其实是从每个日志文件的第2048个字节开始算:
数据库 -- redo log日志
每个redo日志文件前2048个字节,如图:
数据库 -- redo log日志

  • log file header:描述该redo日志文件的一些整体属性
  • checkpoint1:记录关于checkpoint的一些属性,属性具体释义如下:
    数据库 -- redo log日志

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

系统第一次启动后初始化log buffer:
数据库 -- redo log日志

checkpoint
redo日志只是为了系统崩溃后恢复脏页用的,如果对应的脏页已经刷新到了磁盘,则该redo日志占用的磁盘空间就可以被后续的redo日志所重用。即:判断某些redo日志占用的磁盘空间是否可以覆盖的依据就是它对应的脏页是否已经刷新到磁盘里。
数据库 -- redo log日志
如图,虽然mtr_1和mtr_2生成的redo日志都已经被写到了磁盘上,但是它们修改的脏页仍然留在Buffer Pool中,所以它们生成的redo日志在磁盘上的空间是不可以被覆盖的。之后随着系统的运行,如果页a被刷新到了磁盘,那么它对应的控制块就会从flush链表中移除:
数据库 -- redo log日志
全局变量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值的关系就像这样:
数据库 -- redo log日志
批量从flush链表中刷出脏页
一般情况下都是后台的线程在对LRU链表和flush链表进行刷脏操作,这主要因为刷脏操作比较慢,不想影响用户线程处理请求。但是如果当前系统修改页面的操作十分频繁,这样就导致写日志操作十分频繁,系统lsn值增长过快。如果后台的刷脏操作不能将脏页刷出,那么系统无法及时做checkpoint,可能就需要用户线程同步的从flush链表中把那些最早修改的脏页(oldest_modification最小的脏页)刷新到磁盘,这样这些脏页对应的redo日志就没用了,然后就可以去做checkpoint了。

崩溃恢复

确定恢复的起点
checkpoint_lsn之前的redo日志都可以被覆盖,这些redo日志对应的脏页都已经被刷新到磁盘中了,所以需要从checkpoint_lsn开始读取redo日志来恢复页面。

redo日志文件组的第一个文件的管理信息中有两个block都存储了checkpoint_lsn的信息,分别是checkpoint1和checkpoint2,选取最近发生的那次checkpoint的信息。利用checkpoint_no衡量发生时间早晚,值越大,说明该block存储的是最近的一次checkpoint信息。