前言
已知,在关系型数据库中的事务的ACID模型由原子性,一致性,隔离性和持久性组成,对于MySQL
的InnoDB
引擎,隔离性由基于悲观锁的加锁机制和基于无锁的多版本并发控制来支持,而原子性,则由在引擎层生成的undo log来保证,以及持久性,则是由在引擎层生成的redo log和在Service层生成的binlog来共同支撑,除此之外,在Service层生成的binlog还会用于MySQL
的主从同步,所以MySQL
中的redo log,binlog和undo log是十分重要的,本篇文章将对这三大日志进行学习。
MySQL
版本:5.7
正文
一. MySQL中的页
在学习三大日志之前,先了解一下MySQL
中的页的概念。由于InnoDB
引擎中将数据存储在磁盘上,所以为了避免读取数据时与磁盘频繁产生IO,InnoDB
引擎采用页(Page)作为磁盘与内存交互的基本单位。每次会将一页数据加载到内存中,以提升后续操作数据的效率。
InnoDB
引擎,操作系统的文件管理系统,磁盘扇区三者之间的关系可以用下图进行示意。
注:后续称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的交互流程图如下所示。
三. 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 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 = 1
- 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_size | redo log buffer的大小 | 16MB |
innodb_log_file_size | 每个redo log文件的大小 | 48MB |
innodb_log_files_in_group | redo log文件数量 | 2 |
innodb_log_group_home_dir | redo log文件存放路径 | MySQL 目录/data |
所以InnoDB
引擎中,默认情况下磁盘上的redo log文件个数为2,每个redo log文件大小为48MB,这两个redo log文件组成了一个日志文件组,整体是一个环形结构,从头到尾进行循环写入,示意图如下。
在日志文件组中,有两个属性用于标识当前写入位置和当前清除位置,说明如下。
- write pos。记录当前写入位置,写入后会向后推移;
- checkpoint。记录当前清除位置,清除后会向后推移。
write pos与checkpoint之间的位置,表示redo log上可以写入的部分,如下所示。
如果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是以事件的形式记录了所有的DDL(Data Definition Language,数据定义语言)和DML(Data Manipulation Language,数据操纵语言)语句,即binlog记录的是操作而不是数据值,因此binlog属于逻辑日志。
不同于redo log的循环写入,binlog是追加写入,且没有固定大小限制。也不同于redo log属于InnoDB
存储引擎,binlog是属于MySQL
的Service层,无论使用什么存储引擎,都会在Service层记录binlog。
binlog可以用于数据恢复和主从复制,主要就是通过读取binlog并将binlog中记录的操作再执行一遍。
下面将从几个方面对binlog进行说明。
1. 写入时机
当开启事务后,在事务执行过程中,会将DDL和DML的操作记录到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设置为n(n > 1),表示每次提交事务时,会将binlog cache的内容写入page cache,当向page cache写入数据的事务达到n个,此时调用
fsync
函数将page cache的内容写到binlog。
用下图进行概括示意。
2. 两阶段提交
现在已知,在开启事务后,由于后台线程的存在,事务执行过程中是可以不断向redo log写入内容的,而binlog只能在事务提交时被写入,所以实际上redo log和binlog的写入时机是不相同的,这就导致当发生MySQL
宕机时可能会出现redo log和binlog所包含的逻辑内容不一致的问题。
比如现在执行一条更新SQL
语句UPDATE student SET stu_age=22 WHERE id=1
,如果这条语句的修改写到了redo log中,但是在写到binlog前MySQL
发生宕机,然后MySQL
重启之后会根据redo log进行数据恢复,由于redo log中有更新SQL
语句的修改数据,所以这条更新SQL
语句的修改结果会写到磁盘中,但是binlog中是没有这条更新SQL
语句的,就会导致后续基于binlog进行主从同步等操作时会出现主和从数据不一致的问题。
为了解决上述的问题,在InnoDB
引擎中,使用了两阶段提交来解决。具体的实现就是将redo log的写入分为两个阶段,示意图如下所示。
由上图可知,一条redo log记录可以由事务ID + redo log记录数据 + 提交状态组成,提交状态可以是prepare和commit,当第一次将一个数据修改写入redo log时,这条redo log记录的状态为prepare,这是第一阶段提交,后续提交事务时,会在redo log中将这个事务对应的记录的状态置为commit,这是第二阶段提交。根据上述的两阶段提交的写入方式,再结合binlog,可以在发生MySQL
宕机导致redo log和binlog逻辑内容不一致时判断事务是否需要进行回滚。具体的判断策略如下所示。
- 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 log,redo 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,其属于逻辑日志,在MySQL
的Service层记录。当向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设置为n(n > 1),表示每次提交事务时,会将binlog cache的内容写入page cache,当向page cache写入数据的事务达到n个,此时调用
fsync
函数将page cache的内容写到binlog。
对于undo log,其用于事务回滚,可以参见MySQL-事务隔离级别与MVCC一文来了解其特性,本文不再赘述。