MySQL三大日志(binlog,redolog,undolog)详解

4,117 阅读11分钟

热衷学习,热衷生活!😄

沉淀、分享、成长,让自己和他人都能有所收获!😄

一、MySQL日志

MySQL日志主要包括错误日志、查询日志、慢查询日志、事务日志、二进制日志几大类。其中比较重要的就是二进制日志binlog(归档日志)、事务日志redo log(重做日志)和undo log(回滚日志)。

日志关系如下图:

二、redo log

redo log(重做日志)是InnoDB存储引擎独有的,它让MySQL有了崩溃恢复的能力。

MySQL实例挂了或者宕机了,重启的时候InnoDB存储引擎会使用rede log日志恢复数据,保证事务的持久性和完整性。如下图:

MySQL中数据是以页为单数存储,当你查询一条记录时,硬盘会把一整页的数据加载出来,加载出来的数据叫做数据页,会放到Buffer Pool中。后续的查询都是先从Buffer Pool中找,没有找到再去硬盘加载其他的数据页直到命中,这样子可以减少磁盘IO的次数,提高性能。更新数据的时候也是一样,优先去Buffer Pool中找

,如果存在需要更新的数据就直接更新。然后会把“在某个数据页做了什么修改”记录到重做日志缓存(redo log buffer)里,在刷盘的时候会写入redo log日志文件里。

如下图:

小贴士:每条redo记录由“表空间号+数据页号+偏移量+修改数据长度+具体修改的数据”组成。

刷盘时机

理想情况下,事务一提交就会进行刷盘操作,但是实际上是刷盘的时机是根据策略来决定的。

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

  • 0:设置为0的时候,每次提交事务时不刷盘。
  • 1:设置为1的时候,每次提交事务时刷盘。
  • 2:设置为2的时候,每次提交事务时都只把redo log buffer写入page cache

innodb_flush_log_at_trx_commit参数默认为1,当事务提交的时候会调用fsyncredo log进行刷盘,将redo log buffer写入redo log文件中。

另外,Innodb存储引擎有一个后台线程,每隔1秒,就会把会redo log buffer中的内容写入到文件系统缓存page cache,然后调用fsync刷盘。

如上图,所以说一个没有提交事务的redo log记录,也会被刷盘。

下面是各种刷盘策略的流程图。

innodb_flush_log_at_trx_commit = 0

如上图,如果宕机了或者MySQL挂了可能造成1秒内的数据丢失。

innodb_flush_log_at_trx_commit = 1

如上图,只要事务提交成功,redo log记录就一定在磁盘里,不会有任务数据丢失。

如果执行事务的时候MySQL挂了或者宕机了,这部分日志丢失了,但是因为事务没有提交,所以日志丢了也不会有损失。

innodb_flush_log_at_trx_commit = 2

如上图,当事务提交成功时,redo log buffer日志会被写入page cache,然后后台线程会刷盘写入redo log,由于后台线程是1秒执行一次所以宕机或者MySQL挂了可能造成1秒内的数据丢失。

日志文件组

硬盘上存储的redo log日志文件不止一个,而是一个日志文件组的形式出现的,每个的redo log文件大小都是一样的。它采用的是环形数组形式,从头开始写,写到末尾回到头循环写,如下图所示:

日志文件组中有两个重要的属性,分别是witre pos、checkpoint

  • wirte pos:是当前记录的位置,一边写一边后移。
  • checkpoint:是当前要擦除的位置,也是后台推移。

每次刷盘redo log记录到日志文件组中,wirte log位置就会后移更新。

每次MySQL加载日志文件组恢复数据时,会清空加载过的redo log,并把checkpoint后移更新。

write poscheckpoint 之间的还空着的部分可以用来写入新的 redo log 记录。

如果 witre pos追上checkpoint,表示日志文件组满了,这时候不能再写入新的redo log记录,MySQL得停下来,清空一些记录,把checkpoint推荐一下。

redo log小结

redo log的作用和它的刷盘时机、存储形式。

可以思考一个问题:只要每次把修改后的数据页直接刷盘不就好了,为什么还要用redo log刷盘?不都是刷盘吗?有什么区别?

实际上,数据页大小是16KB,刷盘比较耗时,可能就修改了数据页的几byte数据,没有必要把整页的数据刷盘。而且数据页刷盘都是随机写,因为一个数据页对应的位置可能是在硬盘文件的随机位置,所以性能很差。

如果是写redo log,一行记录就占了几十byte,只要包含了表空间号、数据页号、磁盘文件偏移量、修改值,再加上是顺序写,所以刷盘效率很高。

所以用 redo log 形式记录修改内容,性能会远远超过刷数据页的方式,这也让数据库的并发能力更强。

三、binlog

redo log是物理日志,记录的是“在某个数据页做了什么修改”,属于Innodb存储引擎。

binlog日志是逻辑日志,记录内容是语句的原始逻辑,属于MySQL Server层。所有的存储引擎只要发生了数据更新,都会产生binlog日志。

binlog日志的作用

可以说MySQL数据库的数据备份、主备、主主、住从都离不开binlog,需要依赖binlog来同步数据,保证数据一致性。

binlog会记录所有涉及更新数据的逻辑规则,并且按顺序写。

记录格式

binlog日志有三种格式,可以通过binlog_format参数设置,有以下三种:

  • statement
  • row
  • mixed

设置statement记录的内容是SQL语句原文,比如执行一条update T set update_time = now() where id = 1,记录内容如下:

同步数据时,会执行记录的SQL语句,但是有个问题update_time = now()这里会获取到当前系统问题,直接执行会导致与原库数据不一致。

为了解决这种问题,我们需要将binlog_format设置成row,记录的不再是简单的SQL语句了,还包含了操作的具体数据,记录内容如下:

row格式记录的内容看不到详细信息,通过mysqlbinlog工具解析出来。

update_time = now()变成了具体的时间,条件后面的@1、@2都是该行数据第1个~2个字段的原始值(假设这张表只有2个字段)。

设置成row带来的好处就是同步数据的一致性,通常情况都设置成row,这样可以为数据库的恢复与同步带来更好的可靠性。但是这种格式需要大量的容量来记录,比较占用空间,恢复与同步时会更消耗IO资源,影响执行速度。

所以又有了一种折中方案,设置为mixed,记录的内容是前两者的混合。

MySQL会判断这条SQL语句是否会引起数据不一致,如果是就用row格式,否则就用statement格式。

写入机制

binlog的写入时机为事务执行过程中,先把日志写到binlog cache,事务提交的时候再把binlog cache写到binlog文件中(实际先会写入page cache,然后再由fsync写入binlog文件)。

因为一个事务的binlog不能被拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一块内存作为binlog cache。可以通过binlog_cache_size参数控制单线程binlog_cache大小,如果存储内容超过了这个参数,就要暂存到磁盘。

binlog日志刷盘流程如下:

  • 上图的write,是指把日志写入到文件系统的page cache,并没有把数据持久化硬盘,所以速度比较快。
  • 上图的 fsync才是将数据库持久化到硬盘的操作。

writefsync的时机可以由参数sync_binlog控制,可以配置成0、1、N(N>1)

  • 设置成0时:表示每次提交事务都只会write,由系统自行判断什么时候执行fsync
  • 设置成1时:表示每次提交事务都会执行fsync,就和redo log日志刷盘流程一样。
  • 设置成N时:表示每次提交事务都会write,但是积累N个事务后才fsync

设置为0时如下图:

从上图可知,sync_bilog = 0设置成0,只把日志写入page cache虽然性能得到了提高,但是事务提交了fsync的时候宕机了,可能造成binlog日志的丢失。

设置为2时如下图:

在出现IO瓶颈的场景里,将sync_binlog设置成一个比较大的值,可以提升性能。

同样的,如果机器宕机,会丢失最近N个事务的binlog日志。

两阶段提交

redo log(重做日志)让InnoDB存储引擎有了崩溃恢复的能力。

binlog(归档日志)保证了MySQL集群架构数据的一致性。

虽然它们都属于持久化的保证,但是侧重点不一样。

在执行更新语句过程,会记录redo logbinlog两块日志,以基本的事务为单位,redo log在事务执行过程中可以不断写入,而binlog日志只有在提交事务的时候才会写入,所以它们写入的时机不一样。

思考一个问题,如果redo log和binlog两份日志之间的逻辑不一样,会出现什么问题呢?MySQL是怎么解决这个问题的呢?

比如有这样一个场景,假设有这么一条语句update T set c = 1 where id = 2(c原值为0),假如执行过程中写完redo log日志后,在写入binlog的时候发生了异常,会出现什么情况呢?

如下图:

由于binlog日志没写完就异常,这个时候binlog日志里面没有对应的修改记录,之后使用binlog同步的数据的时候就会少这一次的更新,这一行数据c = 0,而原库使用redo log日志恢复,这一行数据c = 1 ,最终数据不一致。如下图:

为了解决两份日志之间的逻辑不一致的问题,InnoDB存储引擎使用两阶段提交方案。

redo log日志的写入拆分成两个步骤preparecommit,如下图:

使用两阶段提交后,写入binlog时发生异常也没关系,因为MySQL根据redo log日志恢复数据时,发现redo log日志处于prepare阶段,并且没有对应binlog日志(根据事务id对应),所以就会回滚事务。

再想一个场景,redo lgo设置commit阶段发生异常,事务会不会回滚呢?

并不会回滚事务,虽然redo log是处于prepare阶段,但是存在对应的事务binlog日志,所以MySQL认为是完整的,所以不会回滚事务。

undo log

想要保证事务的原子性,就需要在发生异常时,对已经执行的操作进行回滚,在MySQL中恢复机制是通过undo log(回滚日志)实现的,所有事务进行的修改都会先被记录到这个回滚日志,然后再执行其他相关的操作。如果执行过程中遇到异常的话,我们直接利用回滚日志中的信息将数据回滚到修改之前的样子。并且,回滚日志会先于数据持久化到磁盘上。这样就保证了即使遇到数据库突然宕机等情况,当用户再次启动数据库的时候,数据库还能够通过查询回滚日志来回滚将之前未完成的事务。

另外,MVCC的实现依赖:隐藏字段、Read Viewundo log。在底层实现中,InnoDB通过数据行的DB_TRX_IDRead View来判断数据的可见性,如不可见,则通过数据行DB_ROLL_PTR找到undo log中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事物里,用户只能看到该事务创建Read View之前已经提交的修改和该事务本身做的修改。

总结

MySQL InnoDB引擎使用redo log日志保证事务的持久性,使用undo log日志保证事务的原子性。

MySQL数据库的数据备份、主备、主主、主从离不开binlog,需要依赖binlog来同步数据,保证数据的一致性。

参考:javaguide.cn/database/my…