详细分析MySQL中的三大日志

1,176 阅读12分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 25 天,点击查看活动详情

前言

已知,在关系型数据库中的事务的ACID模型由原子性一致性隔离性持久性组成。

对于MySQLInnoDB引擎:

  • 隔离性由基于悲观锁的加锁机制和基于无锁的多版本并发控制来支持;
  • 原子性由在引擎层生成的undo log来保证;
  • 持久性由在引擎层生成的redo log和在Service层生成的binlog来共同支撑。

除此之外,在Service层生成的binlog还会用于MySQL的主从同步,所以MySQL中的redo logbinlogundo log是十分重要的,本篇文章将对这三大日志进行学习。

正文

一. MySQL中的页

在学习三大日志之前,先了解一下MySQL中的页的概念。由于InnoDB引擎中将数据存储在磁盘上,所以为了避免读取数据时与磁盘频繁产生IOInnoDB引擎采用页(Page)作为磁盘与内存交互的基本单位。每次会将一页数据加载到内存中,以提升后续操作数据的效率。

InnoDB引擎操作系统的文件管理系统磁盘扇区三者之间的关系可以用下图进行示意。

MySQL中的页

注:后续称MySQL中的页为数据页

二. Buffer Pool

尽管InnoDB每次读取数据都会将数据页加载到内存中,以减少IO开销,但是如果每次都从磁盘读取数据页,效率也不会高,所以在InnoDB中加入了一个内存缓冲区,叫做Buffer Pool

读取数据时,先判断数据所在数据页是否存在于Buffer Pool中,如果存在于Buffer Pool中,则读取Buffer Pool中的数据,如果不存在于Buffer Pool中,则从磁盘上读取数据页,并将读取到的数据页写入Buffer Pool

修改数据时,也会先将修改写入到Buffer Pool,而不会直接写入磁盘,此时就会出现Buffer Pool中的数据页内容和磁盘上的数据页的内容不一致的情况,此时称这样的数据页为脏页,因此InnoDB引擎提供了一个后台线程每隔一定时间将Buffer Pool中的内容写入磁盘,这样的过程叫做刷脏,通过Buffer Pool + 刷脏,可以实现一次性将多个修改同步到磁盘,从而减少IO次数,提高InnoDB引擎的读写效率。

修改数据时InnoDB引擎中基于Buffer Pool的交互流程图如下所示。

InnoDB引擎中基于Buffer Pool的交互流程图

三. Redo Log

InnoDB存储引擎为了提高读写效率,引入了Buffer Pool,当要操作的数据所在的数据页在Buffer Pool中不存在时,会先将数据页从磁盘加载到Buffer Pool中,然后对Buffer Pool中的数据进行操作,此时会出现脏页,所以InnoDB引擎提供了一个后台线程以一定的周期执行刷脏操作。但是刷脏并不是实时的,当Buffer Pool中存在脏页,此时MySQL服务端发生故障而宕机或重启,此时Buffer Pool中脏页所对应的那一部分数据修改就丢失了。

为了解决数据丢失的问题,InnoDB引入了redo log(重做日志)来进行保护。当对Buffer Pool中的数据进行修改后,还会将对数据所做的修改写入redo log中,如果出现了因为MySQL服务端宕机或者重启而导致数据丢失的情况,此时可以在MySQL服务端启动的时候,从redo log进行数据的恢复。实际上redo log也有一个缓冲区,叫做redo log buffer,当要向redo log记录信息时,会先记录到redo log buffer中,然后在适当的时机(可配)把redo log buffer的内容写入到磁盘上的redo log中。那么修改数据时InnoDB引擎中基于Buffer Poolredo log buffer的交互流程图如下所示。

Buffer Pool和redo log buffer的交互流程图

那么现在对于redo log有如下几点需要进行说明。

1. 刷盘时机

注:下面提及的page cache可以理解为OS文件管理系统的缓冲区,在向磁盘写入数据时,会先写入到page cache中,然后再通过fsync函数将page cache中的内容刷到磁盘上。

InnoDB引擎中使用innodb_flush_log_at_trx_commit来设置redo log的刷盘时机策略,其可以设置支持三种策略。

  • innodb_flush_log_at_trx_commit为0,表示每次提交事务时不会刷盘;
  • innodb_flush_log_at_trx_commit为1,表示每次提交事务时会刷盘,即先将redo log buffer内容写入page cache,然后调用fsync函数将page cache中的内容刷到磁盘上;
  • innodb_flush_log_at_trx_commit为2,表示每次提交事务时,仅将redo log buffer内容写入page cache

同时,InnoDB引擎有一个后台线程,其1秒执行一次,会将redo log buffer的内容写入page cache,然后再调用fsync函数将page cache中的内容写到磁盘的redo log文件中,所以innodb_flush_log_at_trx_commit的值无论设置为0,1或者2,最终都是可以将redo log buffer的内容写到磁盘的redo log文件中的。下面给出innodb_flush_log_at_trx_commit被设置为不同值时的刷盘示意图。

  • innodb_flush_log_at_trx_commit = 0

innodb_flush_log_at_trx_commit = 0

  • innodb_flush_log_at_trx_commit = 1

innodb_flush_log_at_trx_commit = 1

  • innodb_flush_log_at_trx_commit = 2

innodb_flush_log_at_trx_commit = 2

因此可以知道,innodb_flush_log_at_trx_commit设置为0或者2时,在MySQL宕机时可能会造成1秒内的数据丢失,而innodb_flush_log_at_trx_commit设置为1时,不会造成数据丢失,innodb_flush_log_at_trx_commit=1也是InnoDB的默认设置。

最后,用一个图进行刷盘时机的总结。

刷盘时机的总结

2. redo log文件形式

可以通过SHOW VARIABLES LIKE 'innodb_log%' 语句查看InnoDB引擎中的redo log相关配置。下表是部分重要参数的说明。

Variable_name说明默认值
innodb_log_buffer_sizeredo log buffer的大小16MB
innodb_log_file_size每个redo log文件的大小48MB
innodb_log_files_in_groupredo log文件数量2
innodb_log_group_home_dirredo log文件存放路径MySQL目录/data

所以InnoDB引擎中,默认情况下磁盘上的redo log文件个数为2,每个redo log文件大小为48MB,这两个redo log文件组成了一个日志文件组,整体是一个环形结构,从头到尾进行循环写入,示意图如下。

日志文件组示意图

在日志文件组中,有两个属性用于标识当前写入位置当前清除位置,说明如下。

  • write pos。记录当前写入位置,写入后会向后推移;
  • checkpoint。记录当前清除位置,清除后会向后推移。

write poscheckpoint之间的位置,表示redo log上可以写入的部分,如下所示。

write pos与checkpoint之间的位置示意图

如果write pos追上checkpoint,此时需要清除redo log内容以使得后续内容能够写入,具体做法是会先触发Buffer Pool的刷盘,然后就可以清除checkpoint之后的部分内容(这部分内容对应Buffer Pool刷到磁盘上的脏页),最后checkpoint向后推移,也就是说checkpoint之前的内容其实已经被写入到了磁盘上,所以一旦MySQL宕机重启后需要根据redo log进行数据恢复时,只需要对checkpoint之后的内容进行恢复。

3. 写redo log和写dbfile的区别

innodb_flush_log_at_trx_commit参数设置为1时,每次提交事务后,会将redo log buffer中的内容写到redo log中,既然写到redo log也是向磁盘写数据,那么为什么不在提交事务后,直接将Buffer Pool中的内容写到dbfile呢。原因就是向dbfile写数据是随机IO,向redo log写数据是顺序IO,顺序IO的读写效率要高于随机IO

4. redo log存储的数据组成

redo log中每条记录的组成为:表空间号 + 数据页号 + 偏移量 + 修改数据长度 + 修改的数据redo log属于物理日志

四. Binlog

binlog是以事件的形式记录了所有的DDLData Definition Language,数据定义语言)和DMLData Manipulation Language,数据操纵语言)语句,即binlog记录的是操作而不是数据值,因此binlog属于逻辑日志。

不同于redo log的循环写入,binlog是追加写入,且没有固定大小限制。也不同于redo log属于InnoDB存储引擎,binlog是属于MySQLService层,无论使用什么存储引擎,都会在Service层记录binlog

binlog可以用于数据恢复主从复制,主要就是通过读取binlog并将binlog中记录的操作再执行一遍。

下面将从几个方面对binlog进行说明。

1. 写入时机

当开启事务后,在事务执行过程中,会将DDLDML的操作记录到binlog cache中,当提交事务时,就会将binlog cache中的内容先写到page cache,然后通过fsync函数将page cache的内容写到磁盘上的binlog

MySQL提供了sync_binlog参数来控制具体的写入策略,可以通过SHOW VARIABLES LIKE 'sync_binlog%' 语句进行查看。sync_binlog参数的取值和说明如下所示。

  • sync_binlog设置为0,表示每次提交事务时,会将binlog cache的内容写入page cache,然后由操作系统决定什么时候将page cache的内容写到binlog
  • sync_binlog设置为1,表示每次提交事务时,会将binlog cache的内容写入page cache,然后调用fsync函数将page cache的内容写到binlog
  • sync_binlog设置为nn > 1),表示每次提交事务时,会将binlog cache的内容写入page cache,当向page cache写入数据的事务达到n个,此时调用fsync函数将page cache的内容写到binlog

用下图进行概括示意。

写入时机示意图

2. 两阶段提交

现在已知,在开启事务后,由于后台线程的存在,事务执行过程中是可以不断向redo log写入内容的,而binlog只能在事务提交时被写入,所以实际上redo logbinlog的写入时机是不相同的,这就导致当发生MySQL宕机时可能会出现redo logbinlog所包含的逻辑内容不一致的问题。

比如现在执行一条更新SQL语句UPDATE student SET stu_age=22 WHERE id=1,如果这条语句的修改写到了redo log中,但是在写到binlogMySQL发生宕机,然后MySQL重启之后会根据redo log进行数据恢复,由于redo log中有更新SQL语句的修改数据,所以这条更新SQL语句的修改结果会写到磁盘中,但是binlog中是没有这条更新SQL语句的,就会导致后续基于binlog进行主从同步等操作时会出现主和从数据不一致的问题。

为了解决上述的问题,在InnoDB引擎中,使用了两阶段提交来解决。具体的实现就是将redo log的写入分为两个阶段,示意图如下所示。

两阶段提交示意图

由上图可知,一条redo log记录可以由事务ID + redo log记录数据 + 提交状态组成,提交状态可以是preparecommit,当第一次将一个数据修改写入redo log时,这条redo log记录的状态为prepare,这是第一阶段提交,后续提交事务时,会在redo log中将这个事务对应的记录的状态置为commit,这是第二阶段提交。根据上述的两阶段提交的写入方式,再结合binlog,可以在发生MySQL宕机导致redo logbinlog逻辑内容不一致时判断事务是否需要进行回滚。具体的判断策略如下所示。

  • binlog无记录,redo log无记录。表示在第一阶段提交前发生宕机,此时需要回滚事务;
  • binlog无记录,redo log记录状态为prepare。表示在binlog写完之前发生宕机,此时需要回滚事务;
  • binlog有记录,redo log记录状态为prepare。表示在binlog写完之后,事务完成提交之前发生宕机,此时需要提交事务;
  • binlog有记录,redo log记录状态为commit。表示是正常完成的事务,此时无需进行操作。

五. Undo Log

undo log叫做回滚日志,属于InnoDB引擎。undo log记录了某条数据变更前的旧数据,当事务需要回滚时,可以通过undo log将数据恢复为事务修改前的数据,所以InnoDB引擎中使用undo log来保证了事务的原子性。

通常情况下,一条更新语句执行,写入三大日志的顺序为undo log先于redo logredo log先于binlog

总结

本篇文章讨论的MySQL中的三大日志,指用于数据恢复redo log,用于事务回滚undo log,以及用于数据恢复主从同步binlog

对于redo log,其属于物理日志,由InnoDB记录。在InnoDB引擎中向redo log写入数据时会先将数据写入redo log buffer内存中,并提供innodb_flush_log_at_trx_commit参数来设置刷盘时机,其可以设置支持三种策略,小节如下所示。

  • innodb_flush_log_at_trx_commit为0,表示每次提交事务时不会刷盘;
  • innodb_flush_log_at_trx_commit为1,表示每次提交事务时会刷盘,即先将redo log buffer内容写入page cache,然后调用fsync函数将page cache中的内容刷到磁盘上;
  • innodb_flush_log_at_trx_commit为2,表示每次提交事务时,仅将redo log buffer内容写入page cache

对于binlog,其属于逻辑日志,在MySQLService层记录。当向binlog写入数据时,会先将数据写入binlog cache内存中,并提供了sync_binlog参数来控制具体的写入策略,小节如下所示。

  • sync_binlog设置为0,表示每次提交事务时,会将binlog cache的内容写入page cache,然后由操作系统决定什么时候将page cache的内容写到binlog
  • sync_binlog设置为1,表示每次提交事务时,会将binlog cache的内容写入page cache,然后调用fsync函数将page cache的内容写到binlog
  • sync_binlog设置为nn > 1),表示每次提交事务时,会将binlog cache的内容写入page cache,当向page cache写入数据的事务达到n个,此时调用fsync函数将page cache的内容写到binlog

对于undo log,其用于事务回滚,可以参见一文读懂MySQL中的MVCC机制一文来了解其特性,本文不再赘述。

参考

MySQL官方文档-Redo Log

JavaGuide-MySQL三大日志详解


开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 25 天,点击查看活动详情