redo log file 为什么可以在保证性能的同时还能保证数据的持久性

40 阅读12分钟

1. 事务的持久性

我们知道InnoDB存储引擎是以页为单位来管理存储空间的,我们进行的增删改查操作其实本质上都是在访问页面(包括读页面、写页面、创建新页面等操作)。在真正访问页面之前,需要把在磁盘上的页缓存到内存中的Buffer Pool之后才可以访问。

然而事务中的持久性的定义是,如果一个事务已经提交,不论什么原因,它产生的结果都是永久存在的,这保证了事务的结果不会丢失。

但是如果我们只在内存的Buffer Pool中修改了页面,假设在事务提交后突然发生了某个故障,导致内存中的数据都失效了,那么这个已经提交了的事务对数据库中所做的更改也就跟着丢失了,这就违背了事务中的持久性定义

为了保证数据的持久性,一个显而易见的解决方法就是,将事务的结果立即写入到存储设备中,然而将数据写入到硬盘可能会产生随机IO,开销太大。

2. 将数据写入到存储设备为什么开销很大

关于这个问题,我们需要从磁盘设备的特性开始说起。对于 SATA 硬盘来说,可以将它简单理解为一个有很多同心圆的圆盘,在写入数据的时候,会经历以下几个步骤:

  • 寻道,找到数据所在的同心圆,这个时间是毫秒级别的;
  • 寻址,找到数据所在的同心圆的位置,这个时间也是毫秒级别的;
  • 开始读写数据,每秒可以读写的数据量为 100M 级别的数据,这个是非常快的。

我们可以从上面看出,如果没有寻道和寻址这两个步骤, SATA 硬盘的性能其实是非常不错的。那么如何避免寻道和寻址呢?如果第一次寻道和寻址后,持续对数据进行大量的读写,即顺序读写,是可以忽略寻道和寻址的时间消耗的。而对应顺序读写的是随机读写,它每一次读写的数据量很小,并且数据位置不相邻,都需要先寻道、寻址,然后才能进行数据读写,所以随机读写的性能是非常差的。

从硬盘自身的特点来说,顺序读写的性能都要远远高于随机读写。另外从系统的角度来看,顺序读写在预读和缓存命中率等方面也要大大优于随机读写。

3. 解决方法

通过上文我们知道,从硬盘自身的特点来说,顺序读写的性能都要远远高于随机读写。那我们将事务所修改的数据顺序的刷会磁盘不就行了。别写入的地方叫做redo log(重做日志)。那么有两种思路

  1. 将数据页中所有的数据顺序的写到redo log中

    1. 太浪费空间,而且一个事务可能会造成多个数据页中的数据的修改
  2. 将数据页中修改的部分顺序的写到redo log中

    1. 只需要把修改了哪些东西记录一下就好,比方说某个事务将系统表空间中的第100号页面中偏移量为1000处的那个字节的值1改成2。

    2. 在事务提交时,把上述内容刷新到磁盘中

3.1 redo log

3.1.2 redo log 日志格式

redo 日志本质上只是记录了一下事务对数据库做了哪些修改

  • type :该条 redo 日志的类型, InnoDB 一共为 redo 日志设计了53种不同的类型
  • space ID:表空间ID。
  • page number:页号。
  • data:该条 redo 日志的具体内容。

redo 日志中只需要记录一下在某个页面的某个偏移量处修改了几个字节的值,具体被修改的内容是啥 就好了,把这种极其简单的 redo 日志称之为 物理日志 ,

比如

将第0号表空间的100号页面的偏移量为1000处的值更新为 2 。

对于redo log的日志格式,现在不作为重点,只需要知道redo log 记录的是数据页的变化,且顺序写入。

3.1.2 使用redo log 刷新到磁盘的好处

与在事务提交时将所有修改过的内存中的页面刷新到磁盘中相比,只将该事务执行过程中产生的redo日志刷新到磁盘的好处如下:

  • redo log占用的空间非常小

  • redo log是顺序写入磁盘的在执行事务的过程中,每执行一条语句,就可能产生若干条redo日志,这些日志是按照产生的顺序写入磁盘的,也就是使用顺序IO。

  • redo日志会把事务在执行过程中对数据库所做的所有修改都记录下来,在之后系统奔溃重启后可以把事务所做的任何修改都恢复出来。

3.2 redo log 写入磁盘过程

3.2.1 redo log block

设计InnoDB的大佬为了更好的进行系统奔溃恢复,他们把通过mtr生成的redo日志都放在了大小为512字节的页中。一个redo log block的示意图如下:

redo log block:为了和表空间中的页(16K)做区别,我们这里把用来存储redo日志的页(512B)称为block

真正的redo日志都是存储到占用496字节大小的log block body中,图中的log block header和log block trailer存储的是一些管理信息

3.2.2 redo log buffer

为了解决磁盘速度过慢的问题,写入redo日志时不能直接直接写到磁盘上,在服务器启动时就向操作系统申请了一大片称之为redo log buffer的连续内存空间,这片内存空间被划分成若干个连续的redo log block,就像这样:

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

3.2.3 redo log 写入log buffer

向rod 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中。

  • 一次原子访问的过程称之为一个Mini-Transaction,简称mtr。
  • 一个mtr可以包含对多条数据的操作。
  • 一个mtr可以包含一组redo日志,在进行奔溃恢复时这一组redo日志作为一个不可分割的整体

我们现在假设有两个名为T1、T2的事务,每个事务都包含2个mtr,我们给这几个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_1、mtr_t2_1就被放到同一个block中存储,有的mtr产生的redo日志量非常大,比如mtr_t1_2产生的redo日志甚至占用了3个block来存储。

3.2.4 redo log 刷盘时机

我们前面说mtr运行过程中产生的一组redo日志在mtr结束时会被复制到log buffer中,可是这些日志总在内存里呆着也不是个办法,在一些情况下它们会被刷新到磁盘里,比如:

  • log buffer空间不足时:log buffer的大小是有限的(通过系统变量innodb_log_buffer_size指定),如果不停的往这个有限大小的log buffer里塞入日志,很快它就会被填满。设计InnoDB的大佬认为如果当前写入log buffer的redo日志量已经占满了log buffer总容量的大约一半左右,就需要把这些日志刷新到磁盘上。

  • 事务提交时:我们前面说过之所以使用redo日志主要是因为它占用的空间少,还是顺序写,在事务提交时可以不把修改过的Buffer Pool页面刷新到磁盘,但是为了保证持久性,必须要把修改这些页面对应的redo日志刷新到磁盘。

  • 后台线程:后台有一个线程,大约每秒都会刷新一次log buffer中的redo日志到磁盘。

  • 正常关闭服务器时

  • 做所谓的checkpoint时

  • 其他的一些情况...

1. 刷盘策略

  • Innodb在写Redo日志的时候,是先写入redo log buffer 中,然后再按照一定频率刷新到redo log file中。
  • 这里的 一定频率 有多个选择,这些选择对应的就是不同的刷盘策略.

这里的刷盘不是指将内存中的数据刷新到磁盘,而是指从Redo日志位于内存中的缓冲区(redo log buffer)刷新到位于磁盘中的文件区(redo log file).

  • redo log buffer 刷盘到redo log file的过程也不是真正刷到磁盘中去,只是刷入到 文件缓存系统(page cache)中去。真正的刷入磁盘会交给操作系统来决定

2. redo log 刷盘

MySQL的数据目录(使用SHOW VARIABLES LIKE 'datadir'查看)下默认有两个名为ib_logfile0和ib_logfile1的文件,redo log buffer中的日志默认情况下就是刷新到这两个磁盘文件中。

从上面的描述中可以看到,磁盘上的redo日志文件不只一个,而是以一个日志文件组的形式出现的。这些文件以ib_logfile[数字](数字可以是0、1、2...)的形式进行命名。在将redo日志写入日志文件组时,是从ib_logfile0开始写,如果ib_logfile0写满了,就接着ib_logfile1写,同理,ib_logfile1写满了就去写ib_logfile2,依此类推。如果写到最后一个文件该咋办?那就重新转到ib_logfile0继续写,所以整个过程如下图所示:

redo log buffer本质上是一片连续的内存空间,被划分成了若干个512字节大小的redo log block。将redo log buffer中的redo log 刷新到磁盘的本质就是把redo log block的镜像写入日志文件(写入磁盘中)中。

3. 关键参数

数据库中innodb_flush_log_at_trx_commit参数就控制了在事务提交时,如何将buffer中的日志数据刷新到file中.

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

  • 参数值为0:表示在事务提交时不立即向磁盘中同步redo日志,这个任务是交给后台线程做的。这样很明显会加快请求处理速度,但是如果事务提交后服务器挂了,后台线程没有及时将redo日志刷新到磁盘,那么该事务对页面的修改会丢失。
  • 参数值为1:提交事务一次就刷盘一次( 默认刷盘策略)
  • 参数值为2:表示在事务提交时需要将redo日志写到操作系统的缓冲区中,但并不需要保证将日志真正的刷新到磁盘。这种情况下如果数据库挂了,操作系统没挂的话,事务的持久性还是可以保证的,但是操作系统也挂了的话,那就不能保证持久性了。
mysql> show variables like 'innodb_flush_log_at_trx_commit';
+--------------------------------+-------+
| Variable_name                  | Value |
+--------------------------------+-------+
| innodb_flush_log_at_trx_commit | 1     |
+--------------------------------+-------+
1 row in set (0.04 sec)

4. 总结

由于事务的持久性是必须的,如果一个事务已经提交,不论什么原因,它产生的结果都是永久存在的,所以对于单节点来说, 我们可以先在内存中将事务的操作完成然后将处理的结果顺序写入日志文件中,这就避免了事务操作结果随机写入存储的性能问题了。

然后我们再提交事务,这样一来,哪怕事务提交后,机器立即崩溃了,在机器故障恢复后,系统依然能通过日志文件,恢复已经提交的事务。

所以,通过顺序写入日志的形式,避免了非易失性存储设备随机写入性能差的问题,达到了事务提交时,所有事务操作结果都写入存储设备的目的。在这个时候,即使系统崩溃,事务的持久性也是有保障的。我们把这种通过顺序写入日志的形式,称之为重做日志(RedoLog)或预写日志(Write Ahead Log)。

目前我们主要是通过重做日志(RedoLog)或预写日志(Write Ahead Log),将随机读写转化为顺序读写来提高事务的性能。