深入理解分布式事务:原理与实战
上QQ阅读APP看书,第一时间看更新

2.3 BinLog

Redo Log是InnoDB存储引擎特有的日志,MySQL也有其自身的日志,这个日志就是BinLog,即二进制日志。

2.3.1 BinLog基本概念

BinLog是一种记录所有MySQL数据库表结构变更以及表数据变更的二进制日志。BinLog中不会记录诸如select和show这类查询操作的日志,同时,BinLog是以事件形式记录相关变更操作的,并且包含语句执行所消耗的时间。BinLog有以下两个最重要的使用场景。

1)主从复制:在主数据库上开启BinLog,主数据库把BinLog发送至从数据库,从数据库获取BinLog后通过I/O线程将日志写到中继日志,也就是Relay Log中。然后,通过SQL线程将Relay Log中的数据同步至从数据库,从而达到主从数据库数据的一致性。

2)数据恢复:当MySQL数据库发生故障或者崩溃时,可以通过BinLog进行数据恢复。例如,可以使用mysqlbinlog等工具进行数据恢复。

2.3.2 BinLog记录模式

BinLog文件中主要有3种记录模式,分别为Row、Statement和Mixed。

1.Row模式

Row模式下的BinLog文件会记录每一行数据被修改的情况,然后在MySQL从数据库中对相同的数据进行修改。

Row模式的优点是能够非常清楚地记录每一行数据的修改情况,完全实现主从数据库的同步和数据的恢复。

Row模式的缺点是如果主数据库中发生批量操作,尤其是大批量的操作,会产生大量的二进制日志。比如,使用alter table操作修改拥有大量数据的数据表结构时,会使二进制日志的内容暴涨,产生大量的二进制日志,从而大大影响主从数据库的同步性能。

2.Statement模式

Statement模式下的BinLog文件会记录每一条修改数据的SQL语句,MySQL从数据库在复制SQL语句的时候,会通过SQL进程将BinLog中的SQL语句解析成和MySQL主数据库上执行过的SQL语句相同的SQL语句,然后在从数据库上执行SQL进程解析出来的SQL语句。

Statement模式的优点是由于不记录数据的修改细节,只是记录数据表结构和数据变更的SQL语句,因此产生的二进制日志数据量比较小,这样能够减少磁盘的I/O操作,提升数据存储和恢复的效率。

Statement模式的缺点是在某些情况下,可能会导致主从数据库中的数据不一致。例如,在MySQL主数据库中使用了last_insert_id()和now()等函数,会导致MySQL主从数据库中的数据不一致。

3.Mixed模式

Mixed模式下的BinLog是Row模式和Statement模式的混用。在这种模式下,一般会使用Statement模式保存BinLog,如果存在Statement模式无法复制的操作,例如在MySQL主数据库中使用了last_insert_id()和now()等函数,MySQL会使用Row模式保存BinLog。也就是说,如果将BinLog的记录模式设置为Mixed,MySQL会根据执行的SQL语句选择写入的记录模式。

2.3.3 BinLog文件结构

MySQL的BinLog文件中保存的是对数据库、数据表和数据表中的数据的各种更新操作。用来表示修改操作的数据结构叫作日志事件(Log Event),不同的修改操作对应着不同的日志事件。在MySQL中,比较常用的日志事件包括Query Event、Row Event、Xid Event等。从某种程度上说,BinLog文件的内容就是各种日志事件的集合。

目前在MySQL的官方文档中,对于MySQL的BinLog文件结构有3种版本,如图2-8~图2-10所示。

图2-8 第一版本的BinLog文件结构

图2-9 第三版本的BinLog文件结构

图2-10 第四版本的BinLog文件结构

关于BinLog文件结构的更多细节,读者可以参考MySQL官方文档自行了解,链接为https://dev.mysql.com/doc/internals/en/event-header-fields.html,这里不再赘述。

2.3.4 BinLog写入机制

MySQL事务在提交的时候,会记录事务日志和二进制日志,也就是Redo Log和BinLog。这里就存在一个问题:对于事务日志和二进制日志,MySQL会先记录哪种呢?

我们已经知道,Redo Log是InnoDB存储引擎特有的日志,BinLog是MySQL本身就有的上层日志,并且会先于InnoDB的事务日志被写入,因此在MySQL中,二进制日志会先于事务日志被写入。

简单点理解就是MySQL在写BinLog文件时,会按照如下规则进行写操作。

1)根据记录的模式(Row、Statement和Mixed)和操作(create、drop、alter、insert、update等)触发事件生成日志事件(事件触发执行机制)。

2)将事务执行过程中产生的日志事件写入相应的缓冲区。注意,这里是每个事务线程都有一个缓冲区。日志事件保存在数据结构binlog_cache_mngr中,这个数据结构中有两个缓冲区:一个是stmt_cache,用于存放不支持事务的信息;另一个是trx_cache,用于存放支持事务的信息。

3)事务在Commit阶段会将产生的日志事件写入磁盘的BinLog文件中。因为不同的事务会以串行的方式将日志事件写入BinLog文件中,所以一个事务中包含的日志事件信息在BinLog文件中是连续的,中间不会插入其他事务的日志事件。

综上,一个事务的BinLog是完整的,并且中间不会插入其他事务的BinLog。

2.3.5 BinLog组提交机制

为了提高MySQL中日志刷盘的效率,MySQL数据库提供了组提交(group commit)功能。通过组提交功能,调用一次fsync()函数能够将多个事务的日志刷新到磁盘的日志文件中,而不用将每个事务的日志单独刷新到磁盘的日志文件中,从而大大提升了日志刷盘的效率。

在InnoDB存储引擎中,提交事务时,一般会进行两个阶段的操作。

1)修改内存中事务对应的信息,并将日志写入相应的Redo Log Buffer。

2)调用fsync()函数将Redo Log Buffer中的日志信息刷新到磁盘的Redo Log文件中。

其中,步骤2)因为存在写磁盘的操作,所以比较耗时。事务提交后,先将日志信息写入内存中的Redo Log Buffer,然后调用fsync()函数将多个事务的日志信息从内存中的Redo Log Buffer刷新到磁盘的Redo Log文件中,这样能够大大提升事务日志的写入效率,尤其对于写入和更新操作比较频繁的业务,性能提升更加明显。

在MySQL 5.6之前的版本中,如果开启了BinLog,则InnoDB存储引擎的组提交功能就会失效,导致事务性能下降。这是因为在MySQL中需要保证BinLog和事务日志的一致性,为了保证二者的一致性,使用了两阶段事务。两阶段事务的步骤如下所示。

1)当事务提交时,InnoDB存储引擎需要进行prepare操作。

2)MySQL上层会将数据库、数据表和数据表中的数据的更新操作写入BinLog文件。

3)InnoDB存储引擎将事务日志写入Redo Log文件中。

为了保证BinLog和事务日志的一致性,在步骤1)的prepare阶段会启用一个prepare_commit_mutex锁,这样会导致开启二进制日志后组提交功能失效。

这个问题在MySQL 5.6中得到了解决。在MySQL 5.6中,提交事务时会在InnoDB存储引擎的上层将事务按照一定的顺序放入一个队列,队列中的第一个事务称为leader,其他事务称为follower。在执行顺序上,虽然还是先写BinLog,再写事务日志,但是写日志的机制发生了变化:移除了prepare_commit_mutex锁。开启BinLog后,组提交功能不会失效。BinLog的写入和InnoDB的事务日志写入都是通过组提交功能进行的。

MySQL 5.6中,这种实现方式称为二进制日志组提交(Binary Log Group Commit,BLGC)。BLGC的实现主要分为Flush、Sync和Commit三个阶段。

1)Flush阶段:将每个事务的BinLog写入对应的内存缓冲区。

2)Sync阶段:将内存缓冲区中的BinLog写入磁盘的BinLog文件,如果队列中存在多个事务,则此时只执行一次刷盘操作就可以将多个事务的BinLog刷新到磁盘的BinLog文件中,这就是BLGC操作。

3)Commit阶段:leader事务根据队列中事务的顺序调用存储引擎层事务的提交操作,由于InnoDB存储引擎本身就支持组提交功能,因此解决了prepare_commit_mutex锁导致的组提交功能失效的问题。

在Flush阶段,将BinLog写入内存缓冲区时,不是写完就立刻进入Sync阶段,而是等待一定时间,多积累几个事务的BinLog再一起进入Sync阶段。这个等待时间由变量binlog_max_flush_queue_time决定,binlog_max_flush_queue_time变量的默认值为0。除非有大量的事务不断地进行写入和更新操作,否则不建议修改这个变量的值,这是因为修改后可能会导致事务的响应时间变长。

进入Sync阶段后,会将内存缓冲区中多个事务的BinLog刷新到磁盘的BinLog文件中,和刷新一个事务的BinLog一样,也是由sync_binlog变量进行控制的。

一组事务正在执行Commit阶段的操作时,其他新产生的事务可以执行Flush阶段的操作,Commit阶段的事务和Flush阶段的事务不会互相阻塞。这样,组提交功能就会持续生效。此时,组提交功能的性能和队列中的事务数量有关,如果队列中只存在一个事务,组提交功能和单独提交一个事务的效果差不多,有时甚至会更差。提交的事务越多,组提交功能的性能提升就越明显。

2.3.6 BinLog与Redo Log的区别

BinLog和Redo Log在一定程度上都能恢复数据,但是二者有着本质的区别,具体内容如下。

1)BinLog是MySQL本身就拥有的,不管使用何种存储引擎,BinLog都存在,而Redo Log是InnoDB存储引擎特有的,只有InnoDB存储引擎才会输出Redo Log。

2)BinLog是一种逻辑日志,记录的是对数据库的所有修改操作,而Redo Log是一种物理日志,记录的是每个数据页的修改。

3)Redo Log具有幂等性,多次操作的前后状态是一致的,而BinLog不具有幂等性,记录的是所有影响数据库的操作。例如插入一条数据后再将其删除,则Redo Log前后的状态未发生变化,而BinLog就会记录插入操作和删除操作。

4)BinLog开启事务时,会将每次提交的事务一次性写入内存缓冲区,如果未开启事务,则每次成功执行插入、更新和删除语句时,就会将对应的事务信息写入内存缓冲区,而Redo Log是在数据准备修改之前将数据写入缓冲区的Redo Log中,然后在缓冲区中修改数据。而且在提交事务时,先将Redo Log写入缓冲区,写入完成后再提交事务。

5)BinLog只会在事务提交时,一次性写入BinLog,其日志的记录方式与事务的提交顺序有关,并且一个事务的BinLog中间不会插入其他事务的BinLog。而Redo Log记录的是物理页的修改,最后一个提交的事务记录会覆盖之前所有未提交的事务记录,并且一个事务的Redo Log中间会插入其他事务的Redo Log。

6)BinLog是追加写入,写完一个日志文件再写下一个日志文件,不会覆盖使用,而Redo Log是循环写入,日志空间的大小是固定的,会覆盖使用。

7)BinLog一般用于主从复制和数据恢复,并且不具备崩溃自动恢复的能力,而Redo Log是在服务器发生故障后重启MySQL,用于恢复事务已提交但未写入数据表的数据。

2.3.7 BinLog相关参数

在MySQL中,输入如下命令可以查看与BinLog相关的参数。


show variables like '%log_bin%'; 
show variables like '%binlog%';

其中,几个重要的参数如下所示。

1)log_bin:表示开启二进制日志,未指定BinLog的目录时,会在MySQL的数据目录下生成BinLog,指定BinLog的目录时,会在指定的目录下生成BinLog。

2)log_bin_index:设置此参数可以指定二进制索引文件的路径与名称。

3)binlog_do_db:表示只记录指定数据库的二进制日志。

4)binlog_ignore_db:表示不记录指定数据库的二进制日志。

5)max_binlog_size:表示BinLog的最大值,默认值为1GB。

6)sync_binlog:这个参数会影响MySQL的性能和数据的完整性。取值为0时,事务提交后,MySQL将binlog_cache中的数据写入BinLog文件的同时,不会执行fsync()函数刷盘。当取值为大于0的数字N时,在进行N次事务提交操作后,MySQL将执行一次fsync()函数,将多个事务的BinLog刷新到磁盘中。

7)max_binlog_cache_size:表示BinLog占用的最大内存。

8)binlog_cache_size:表示BinLog使用的内存大小。

9)binlog_cache_use:表示使用BinLog缓存的事务数量。

10)binlog_cache_disk_use:表示使用BinLog缓存但超过binlog_cache_size的值,并且使用临时文件来保存SQL语句中的事务数量。

需要注意的是,MySQL中默认不会开启BinLog。如果需要开启BinLog,要修改my.cnf或my.ini配置文件,在mysqld下面增加log_bin=mysql_bin_log命令,重启MySQL服务,如下所示。


binlog-format=ROW 
log-bin=mysqlbinlog