重做日志「redo log」

redo log是InnoDB存储引擎独有的,它让MySQL有了崩溃恢复的能力。当MySQL实例挂了或崩溃的时候,重启时候,InnoDB存储引擎会redo log日志恢复数据,保证数据的持久性和完整性。
MySQL中数据是以页为单位,在查询一条数据的时候,会从硬盘中把一页数据加载出来,加载出来的数据叫数据页,会被放入缓冲池(Buffer Pool)中。
后续的查询都是先从缓冲池中找,没有命中再去硬盘加载,如此以减少硬盘IO开销,提升性能。
更新表数据的时候,也是如此,发现缓冲池中存在有要更新的数据,就直接在缓冲池中更新,然后会把信息记录在重做日缓存(redo log buffer)中,然后刷盘到redo log文件里。

刷盘时机

InnoDB存储引擎为redo log的刷盘策略提供了innodb_flush_log_at_trx_commit参数,它支持三种策略:

  • 0:设置为0的时候,表示每次事务提交时不进行刷盘操作;
    • 这种情况下,如果MySQL挂了或宕机可能会有一秒钟数据的丢失

流程图如下:
image.png

  • 1:设置为1的时候,表示每次事务提交时都将进行刷盘操作(默认值);
    • 只要事务提交成功了,redo log记录就一定在硬盘里,不会有任何数据丢失
    • 如果事务执行期间MySQL挂了或宕机,这部分日志丢了,但是因为事务并未提交,所以并不会有实际上的损失

流程图如下:
image.png

  • 2:设置为2的时候,表示每次事务提交时都只把redo log buffer内容写进page cache;
    • MySQL挂了不会有任何数据丢失,但是宕机可能会有1秒数据丢失

流程图如下:
image.png
innodb_flush_log_at_trx_commit参数默认为1,也就是说当事务提交时会调用fsyncredo log进行刷盘。
另外,InnoDB存储引擎有一个后台线程,每隔一秒,就会把redo log buffer中的内容写到文件系统缓存(page cache),然后调用fsync刷盘;除此之外,还有一种情况,当redo log buffer占用的空间即将达到innodb_log_buffer_size一半的时候,后台线程也会主动刷盘。
意思就是,可能一个没有提交任何事务的redo log记录,也有可能刷盘。

日志文件组

硬盘上存储的redo log的日志文件不止一个,而是以一个日志文件组的形式出现的,每个的redo日志文件的大小都是一样的。
比如可以配置一组4个文件,每个文件的大小都是1GB,整个redo log日志文件组可以记录4G的内容。
它采用的是环形数组形式,从头开始写,写到末尾又回到头循环写,如下图所示:
image.png
整个日志文件组中还有两个重要的属性,分别是write pos和checkpoint:

  • write pos:指当前记录的位置,一边写一边后移(图中顺时针方向);
  • checkpoint:指当前要擦除的位置。

每次刷盘redo log记录到日志文件组中,write pos位置就会后移更新;每次MySQL加载日志文件组回复数据时,会情况加载过的redo log记录,并把checkpoint后移更新。
image.png
write pos与checkpoint之间的还空着的部分可以用来写入新的redo log记录。
如果write pos追上checkpoint,则表示日志文件满了,这时候不能再写入新的redo log记录,MySQL得停下来,清空一些记录,把checkpoint推进一下。
image.png

redo log小节

为什么不直接刷盘,而先将修改存入redo log

因为,数据页的大小为16KB,且在数据页刷盘的方式为随机写,虽然可能只修改了数据页内的几Byte的数据,但是也会比较耗时。
如果是redo log,一行记录可能就占几十Byte,只包含表空间号、数据页号、磁盘文件偏移量、更新量,再加上是顺序写,所以刷盘速度很快。
所以用redo log形式记录修改内容,性能会远远超过刷数据页的方式,这也让数据库的并发能力更强。

归档日志「binlog」

redo log是物理日志,记录内容是“在某个数据页上做了什么修改”,属于InnoDB存储引擎。
而binlog是逻辑日志,记录内容是语句的原始逻辑,类似于“给ID=2的这一行的c字段加1”,属于MySQL Server层。
不管使用的什么存储引擎,只要发生了表数据更新,都会产生binlog日志。
binlog在MySQL数据库的数据备份、主备、主主、主从都离不开binlog,需要依靠binlog来同步数据,保证数据一致性。
binlog会记录所有涉及更新数据的逻辑操作,并且是顺序写。image.png

记录格式

binlog日志有三种格式,可以通过binlog_format参数指定。

  • statement
    • 指定为statement时,记录的内容是SQL原文,如果此时包含函数,可能重新执行的时候的值与记录时值不一致
    • image.png
  • row(通常选择)
    • 指定为row时,记录的内容不仅是SQL,还包含操作的具体数据,其中@1、@2、@3指代的该行数据的第1~3列的原始数据
    • image.png
  • mixed

    • 因row模式下需要更大的容量来记录,较为占用空间,且在回复与同步时会更消耗IO资源,影响执行速度
    • 因此有了这种方案,即mixed,是row与statement的混合
    • MySQL会判断这条SQL语句是否可能引起数据不一致,若是则用row,反之用statement

      写入机制

      binlog的写入时间也非常简单,事务执行过程中,先把日志写到binlog cache,事务提交的时候,再把binlog cache写到binlog文件中。
      因为一个事务的binlog不能被拆分,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一个块内存作为binlog cache。
      我们可以通过binlog_cache_size参数控制单个线程binlog cache大小,如果存储内容超过了这个参数,就要暂存到磁盘(Swap)。
      binlog日志刷盘流程:
      image.png
  • 图中write,是指把日志写入到文件系统的page cache,并没有把数据持久化到磁盘,所以速度比较快

  • 图中的fsync,才是将数据持久化到磁盘的操作

write和fsync的时机,可以由参数sync_binlog控制,默认是0,即每次提交事务都只进行write,由系统自行判断什么时候执行fsync。
此时虽然性能会得到提升,但是机器宕机,page cache里面的binlog会丢失,详情如下图:
image.png
为了安全起见,可以设置为1,表示每次提交事务都会执行fsync,就如同binlog日志刷盘流程一样。
最后还有一种折中方式,可以设置为N(N>1)「数字」,表示每次提交事务都write,但累计到N个事务后才fsync,具体如下图:
image.png
当出现IO瓶颈的场景里,将sync_binlog设置成一个比较大的值,可以提升性能。
同样的,如果机器宕机,会丢失最近N个事务的binlog日志。

两阶段提交

redo log让InnoDB存储引擎拥有了崩溃恢复能力。
binlog保证了MySQL集群架构的数据一致性。
虽然他们都属于持久化的保证,但是侧重点不同。
在执行更新语句过程中,会记录redo log与binlog两块日志,以基本的事务为单位,redo log在事务执行过程中可以不断写入,而binlog只有在提交事务时才写入,所以redo log与binlog的写入时机不一样。

重做日志和归档日志出现差异

如果,redo log与binlog两份日志之间的逻辑不一致的时候,会出现什么问题?
现在有一条记录,id=2,c=0。SQL=update T set c = 1 where id = 2

归档日志出错的情况

image.png
由于binlog没写完就出现异常,所以binlog中是没有对应的修改记录的。即通过binlog进行恢复时,会缺少这个更新,则c=0「从库」。而原库使用redo log进行恢复,c=1。继而出现主从不一致的情况。
image.png

两阶段提交

两阶段提交就是为了处理以上这种问题而提出的解决方案。原理很简单,就是把redo log的写入拆成了两个步骤-prepare 和 commit,即两阶段提交,具体如图所示:
image.png
使用两阶段提交后,写入binlog时发生异常也不会有影响,因为MySQL根据redo log日志回复数据时,发现redo log还处于prepare阶段,并且没有对应的binlog日志,就会回滚该事务,如下图所示:
image.png
但当redo log在设置commit阶段发生异常,就不会回滚事务,因为虽然redo log处于prepare阶段,但是可以通过事务id找到对应的binlog日志,即MySQL认为它是完整的,所以会提交事务恢复数据,如下图所示:
image.png

回滚日志「undo log」

我们知道如果想要保证事务的原子性,就需要在异常发生时,对已经执行的操作进行回滚,在 MySQL 中,恢复机制是通过 回滚日志(undo log) 实现的,所有事务进行的修改都会先记录到这个回滚日志中,然后再执行相关的操作。如果执行过程中遇到异常的话,我们直接利用 回滚日志 中的信息将数据回滚到修改之前的样子即可!并且,回滚日志会先于数据持久化到磁盘上。这样就保证了即使遇到数据库突然宕机等情况,当用户再次启动数据库的时候,数据库还能够通过查询回滚日志来回滚将之前未完成的事务。
另外,MVCC(Multi-Version Concurrency Control)的实现依赖于:隐藏字段、Read View、undo log。在内部实现中,InnoDB 通过数据行的 DB_TRX_ID 和 Read View 来判断数据的可见性,如不可见,则通过数据行的 DB_ROLL_PTR 找到 undo log 中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建 Read View 之前已经提交的修改和该事务本身做的修改。

总结

MySQL InnoDB 引擎使用 redo log(重做日志) 保证事务的持久性,使用 undo log(回滚日志) 来保证事务的原子性
MySQL数据库的数据备份、主备、主主、主从都离不开binlog,需要依靠binlog来同步数据,保证数据一致性。