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

2.1 Redo Log

MySQL中事务的原子性和持久性是由Redo Log实现的。从这句话就可以看出,Redo Log在MySQL事务的实现中起着至关重要的作用,它确保MySQL事务提交后,事务所涉及的所有操作要么全部执行成功,要么全部执行失败。

2.1.1 Redo Log基本概念

Redo Log也被称作重做日志,它是在InnoDB存储引擎中产生的,用来保证事务的原子性和持久性。Redo Log主要记录的是物理日志,也就是对磁盘上的数据进行的修改操作。Redo Log往往用来恢复提交后的物理数据页,不过只能恢复到最后一次提交的位置。

Redo Log通常包含两部分:一部分是内存中的日志缓冲,称作Redo Log Buffer,这部分日志比较容易丢失;另一分是存放在磁盘上的重做日志文件,称作Redo Log File,这部分日志是持久化到磁盘上的,不容易丢失。

2.1.2 Redo Log基本原理

Redo Log能够保证事务的原子性和持久性,在MySQL发生故障时,尽力避免内存中的脏页数据写入数据表的IBD文件。在重启MySQL服务时,可以根据Redo Log恢复事务已经提交但是还未写入IBD文件中的数据,从而对事务提交的数据进行持久化操作。

例如,在商城系统的下单业务中,用户提交订单时,系统会创建一条新的订单记录并保存到订单数据表中。在MySQL内部,Redo Log的基本原理可以使用图2-1表示。

图2-1 商城业务用户下单时MySQL内部Redo Log的基本原理

从图2-1中可以看出,用户下单后系统创建订单记录,MySQL在提交事务时,会将数据写入Redo Log Buffer,而Redo Log Buffer中的数据会根据一定的规则写入Redo Log文件,具体规则将在2.1.3节中介绍。当MySQL发生故障重启时,会通过Redo Log中的数据对订单表中的数据进行恢复,也就是将Redo Log文件中的数据恢复到order.ibd文件中。系统可以根据需要,查询并加载订单表中的数据(也就是加载order.ibd文件中的数据),也可以向订单表写入数据(也就是持久化数据到order.ibd文件中)。

2.1.3 Redo Log刷盘规则

在MySQL的InnoDB存储引擎中,通过提交事务时强制执行写日志操作机制实现事务的持久化。InnoDB存储引擎为了保证在事务提交时,将日志提交到事务日志文件中,默认每次将Redo Log Buffer中的日志写入日志文件时,都调用一次操作系统的fsync()操作。因为MySQL进程和其占用的内存空间都工作在操作系统的用户空间中,所以MySQL的Log Buffer也工作在操作系统的用户空间中。默认情况下,如果想要将Log Buffer中的数据持久化到磁盘的日志文件中,还需要经过操作系统的内核空间缓冲区,也就是OS Buffer。从Redo Log Buffer中将数据持久化到磁盘的日志文件中的大致流程如图2-2所示。

图2-2 Redo Log Buffer写日志到Redo Log文件示意图

从图2-2中可以看出,Redo Log从用户空间的Log Buffer写入磁盘的Redo Log文件时需要经过内核空间的OS Buffer。这是因为在打开日志文件时,没有使用O_DIRECT标志位,而O_DIRECT标志位可以不经过操作系统内核空间的OS Buffer,直接向磁盘写数据。

在InnoDB存储引擎中,Redo Log具有以下几种刷盘规则。

1)开启事务,发出提交事务指令后是否刷新日志由变量innodb_flush_log_at_trx_commit决定。

2)每秒刷新一次,刷新日志的频率由变量innodb_flush_log_at_timeout的值决定,默认是1s。需要注意的是,刷新日志的频率和是否执行了commit操作无关。

3)当Log Buffer中已经使用的内存超过一半时,也会触发刷盘操作。

4)当事务中存在checkpoint(检查点)时,在一定程度上代表了刷写到磁盘时日志所处的LSN的位置。其中,LSN(Log Sequence Number)表示日志的逻辑序列号。

接下来,对第1)条规则进行简单介绍。

当事务提交时,需要先将事务日志写入Log Buffer,这些写入Log Buffer的日志并不是随着事务的提交立刻写入磁盘的,而是根据一定的规则将Log Buffer中的数据刷写到磁盘,从而保证了Redo Log文件中数据的持久性。这种刷盘规则可以通过innodb_flush_log_at_trx_commit变量控制,innodb_flush_log_at_trx_commit变量可取的值有0、1和2,默认为1。每个取值代表的刷盘规则如图2-3所示。

图2-3 innodb_flush_log_at_trx_commit变量每个取值代表的刷盘规则

·如果该变量设置为0,则每次提交事务时,不会将Log Buffer中的日志写入OS Buffer,而是通过一个单独的线程,每秒写入OS Buffer并调用fsync()函数写入磁盘的Redo Log文件。这种方式不是实时写磁盘的,而是每隔1s写一次日志,如果系统崩溃,可能会丢失1s的数据。

·如果该变量设置为1,则每次提交事务都会将Log Buffer中的日志写入OS Buffer,并且会调用fsync()函数将日志数据写入磁盘的Redo Log文件中。这种方式虽然在系统崩溃时不会丢失数据,但是性能比较差。如果没有设置innodb_flush_log_at_trx_commit变量的值,则默认为1。

·如果该变量设置为2,则每次提交事务时,都只是将数据写入OS Buffer,之后每隔1s,通过fsync()函数将OS Buffer中的日志数据同步写入磁盘的Redo Log文件中。

需要注意的是,在MySQL中,有一个变量innodb_flush_log_at_timeout的值为1,这个变量表示刷新日志的频率。另外,在InnoDB存储引擎中,刷新数据页到磁盘和刷新Undo Log页到磁盘就只有一种检查点规则。

2.1.4 Redo Log刷盘最佳实践

不同的Redo Log刷盘规则,对MySQL数据库性能的影响也不同。本节以一个示例来具体说明innodb_flush_log_at_trx_commit变量的不同取值,对MySQL数据库的性能影响。

创建一个数据库test,在数据库中创建一个名为flush_disk_test的数据表,如下所示。


create database if not exists test;
create table flush_disk_test(
id int not null auto_increment, 
name varchar(20),
primary key(id)
)engine=InnoDB;

为了测试方便,这里创建一个名为insert_data的存储过程,接收一个int类型的参数。这个参数表示向flush_disk_test数据表中插入的记录行数,如下所示。


drop procedure if exists insert_data;
delimiter $$
create procedure insert_data(i int)
begin
    declare s int default 1;
    declare c varchar(50) default 'binghe';
    while s<=i do
        start transaction;
        insert into flush_disk_test (name) values(c);
        commit;
        set s=s+1;
    end while;
end$$
delimiter ;

1)将innodb_flush_log_at_trx_commit变量的值设置为0,调用insert_data向flush_disk_test数据表中插入10万条数据,如下所示。


mysql> call insert_data (100000);
Query OK, 0 rows affected (2.18 sec)

可以看到,当innodb_flush_log_at_trx_commit变量的值设置为0时,向表中插入10万条数据耗时2.18s。

2)将innodb_flush_log_at_trx_commit变量的值设置为1,调用insert_data向flush_disk_test数据表中插入10万条数据,如下所示。


mysql> call insert_data (100000);
Query OK, 0 rows affected (16.18 sec)

可以看到,当innodb_flush_log_at_trx_commit变量的值设置为1时,向表中插入10万条数据耗时16.18s。

3)将innodb_flush_log_at_trx_commit变量的值设置为2,调用insert_data向flush_disk_test数据表中插入10万条数据,如下所示。


mysql> call insert_data (100000);
Query OK, 0 rows affected (3.05 sec)

可以看到,当innodb_flush_log_at_trx_commit变量的值设置为2时,向表中插入10万条数据耗时3.05s。

当innodb_flush_log_at_trx_commit变量的值设置为0或者2时,插入10万条数据耗费的时间差别不是很大,但是与innodb_flush_log_at_trx_commit变量的值设置为1对比来看,耗时差别较大。

需要注意的是,虽然将innodb_flush_log_at_trx_commit变量的值设置为0或者2时,插入数据的性能比较高,但是在系统发生故障时,可能会丢失1s的数据,而这1s内可能会产生大量的数据。也就是说,可能会造成大量数据丢失。

细心的读者可以发现,其实insert_data还有优化的空间,那就是在存储过程中把事务的开启和关闭放到循环体外面,如下所示。


drop procedure if exists insert_data;
delimiter $$
create procedure insert_data(i int)
begin
    declare s int default 1;
    declare c varchar(50) default 'binghe';
    start transaction;
    while s<=i do
        insert into flush_disk_test (name) values(c);
        set s=s+1;
    end while;
    commit;
end$$
delimiter ;

此时,再次测试将innodb_flush_log_at_trx_commit变量的值设置为1的情况,如下所示。


mysql> call insert_data (100000);
Query OK, 0 rows affected (9.32 sec)

可以看到,向数据表中插入数据的性能提升了不少。

2.1.5 Redo Log写入机制

Redo Log主要记录的是物理日志,其文件内容是以顺序循环的方式写入的,一个文件写满时会写入另一个文件,最后一个文件写满时,会向第一个文件写数据,并且是覆盖写,如图2-4所示。

图2-4 Redo Log的写入机制

由图2-4可以看出:

1)Wirte Pos是数据表中当前记录所在的位置,随着不断地向数据表中写数据,这个位置会向后移动,当移动到最后一个文件的最后一个位置时,又会回到第一个文件的开始位置进行写操作;

2)CheckPoint是当前要擦除的位置,这个位置也是向后移动的,移动到最后一个文件的最后一个位置时,也会回到第一个文件的开始位置进行擦除。只不过在擦除记录之前,需要把记录更新到数据文件中;

3)Write Pos和CheckPoint之间存在间隔时,中间的间隔表示还可以记录新的操作。如果Write Pos移动的速度较快,追上了CheckPoint,则表示数据已经写满,不能再向Redo Log文件中写数据了。此时,需要停止写入数据,擦除一些记录。

2.1.6 Redo Log的LSN机制

LSN(Log Sequence Number)表示日志的逻辑序列号。在InnoDB存储引擎中,LSN占用8字节的存储空间,并且LSN的值是单调递增的。一般可以从LSN中获取如下信息。

1)Redo Log写入数据的总量。

2)检查点位置。

3)数据页版本相关的信息。

LSN除了存在于Redo Log中外,还存在于数据页中。在每个数据页的头部,有一个fil_page_lsn参数记录着当前页最终的LSN值。将数据页中的LSN值和Redo Log中的LSN值进行比较,如果数据页中的LSN值小于Redo Log中的LSN值,则表示丢失了一部分数据,此时,可以通过Redo Log的记录来恢复数据,否则不需要恢复数据。

在MySQL的命令行通过如下命令可以查看LSN值。


mysql> show engine innodb status \G
#########省略部分日志#############
Log sequence number          3072213599
Log buffer assigned up to    3072213599
Log buffer completed up to   3072213599
Log written up to            3072213599
Log flushed up to            3072213599
Added dirty pages up to      3072213599
Pages flushed up to          3072213599
Last checkpoint at           3072213599
1620 log i/o's done, 0.00 log i/o's/second
#########省略部分日志#############

重要的参数说明如下所示。

1)Log sequence number:表示当前内存缓冲区中的Redo Log的LSN。

2)Log flushed up to:表示刷新到磁盘上的Redo Log文件中的LSN。

3)Pages flushed up to:表示已经刷新到磁盘数据页上的LSN。

4)Last checkpoint at:表示上一次检查点所在位置的LSN。

2.1.7 Redo Log相关参数

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


show variables like '%innodb_log%';

可以查询到与Redo Log有关的几个重要参数如下所示。

1)innodb_log_buffer_size:表示log buffer的大小,默认为8MB。

2)innodb_log_file_size:表示事务日志的大小,默认为5MB。

3)innodb_log_files_group=2:表示事务日志组中的事务日志文件个数,默认为2个。

4)innodb_log_group_home_dir=./:表示事务日志组所在的目录,当前目录表示MySQL数据所在的目录。