目录
MySQL 事务的实现原理 之 Redo Log 事务/重做日志( 基本概念、原理、刷盘最佳实践演示、写入机制、LSN 机制 、相关参数 )详解
MySQL 作为互联网行业使用最多的关系型数据库之一,其 InnoDB 存储引擎本身就支持事务。
MySQL 的事务实现离不开 Redo Log(重做日志) 和 Undo Log (回滚日志)。
从某种程度上说,事务的隔离性是由 锁 和 MVCC 机制实现的,原子性 和 持久性 是有 Redo Log 实现的,一致性是由 Undo Log 实现的。
Redo:重做、重复、恢复
Undo:撤销、取消
MySQL 事务的实现原理,涉及的内容大概有:
Redo Log :事务、重做日志
Undo Log :回滚日志
BinLog:二进制日志文件
MySQL 事务的流程;
MySQL 中的 XA 事务。
Redo Log(事务日志,也叫重做日志)
MySQL 中事务的 原子性 和 持久性 是由 Redo Log 实现的。
从这句话可以看出,Redo Log 在 MySQL 事务的实现中起着至关重要的作用,它确保 MySQL 事务提交后,事务所涉及的所有操作要么全部成功,要么全部失败(原子性)。
重点作用理解:
问:如果 SQL 语句在执行的时候,MySQL 崩溃了怎么办?
预写日志(Write-Ahead Logging, WAL): 这是一种常用的技术,要求在任何数据被修改写入数据库之前,修改操作必须先被写入到日志记录中,并且被持久化到磁盘上。这样即使发生系统崩溃,也能通过日志来恢复未提交的修改。
WAL 通常由两个部分组成:redo log(重做日志)和 undo log(撤销日志)
Redo Log 重做日志,有一个很重要的作用,就是持久性。
作用就是在 MySQL 真正执行 SQL 语句去修改数据记录之前,一定会先把要修改的具体操作写到日志里面,并刷盘到 Redo Log 日志文件中,才能算事务提交完成,后面才会去真正执行 SQL 语句。
如果在执行 SQL 语句的时候,MySQL 崩溃了,那么不用慌,直接到 Redo Log 文件,根据之前写到里面的具体的修改操作,再重新拿出来执行一遍就可以了。
问:那如果在把数据的修改操作写到日志文件之前,数据库崩溃了怎么办?
我觉得应该就会是事务没有提交成功,然后就触发回滚。
1、Redo Log 基本概念
Redo Log 也被称作【重做日志】,它是在 【InnoDB】存储引擎中产生的,用来保证事务的 原子性 和 持久性。
Redo Log 主要记录的是 【物理日志】,也就是对磁盘上的数据进行的修改操作。
Redo Log 往往用来恢复提交后的物理数据页,不过只能恢复到最后一次提交的位置。
Redo Log 通常包含两部分:
一部分是内存中的【日志缓存】,称作 【Redo Log Buffer】,这部分日志比较容易丢失;
另一部分是存放在磁盘上的【重做日志文件】,称作【Redo Log File】,这部分日志是持久化到磁盘上的,不容易丢失。
2、Redo Log 基本原理
Redo Log 能够保证事务的原子性和持久性,在 MySQL 发生故障时,尽力避免内存中的脏页数据写入数据表的 IBD 文件。
在重启 MySQL 服务时,可以根据 Redo Log 恢复 事务已经提交但是还未写入 IBD 文件中的数据,从而对事务提交的数据进行【持久化操作】。
IBD 文件(InnoDB Data File):在 MySQL 的 InnoDB 存储引擎中,创建的表在文件夹里面显示的就是xxx.idb。IBD(InnoDB Data File)
流程图示例:
例如,在商城系统的下单业务中,用户提交订单时,系统会创建一条新的订单记录并保存到订单数据表(order.idb)中。在 MySQL 内部,Redo Log 的基本原理如图所示:
可以看出,用户下单后,系统创建了一条订单记录,MySQL 在提交事务时,会先将数据写入到 【Redo Log Buffer】缓冲区中,而【Redo Log Buffer】中的数据会根据一定的规则写入(持久化) 【Redo Log 文件】。
当 MySQL 发生故障重启时,会通过 Redo Log 中的数据对订单表中的数据进行恢复,也就是将 【Redo Log 文件】中的数据恢复到 【order.ibd文件】中。
系统可以根据需要,查询并加载订单表中的数据(也就是加载【order.ibd 文件】中的数据),也可以向订单表写入数据(也就是持久化数据到【order.ibd文件】中)
3、Redo Log 刷盘规则
刷盘就是将内存中的数据持久化到磁盘中。
上面说的,【Redo Log Buffer】中的数据会根据一定的规则写入(持久化) 【Redo Log 文件】,此时就来介绍持久化(刷盘)的规则。
在 MySQL 的 InnoDB 存储引擎中,通过提交事务时强制执行写日志操作机制实现事务的持久化。
InnoDB 存储引擎为了保证在事务提交时,将日志提交到事务日志文件(就是 Redo Log 文件)中,默认每次将 【 Redo Log Buffer 】中的日志写入到日志文件(就是 Redo Log 文件)时,都调用一次操作系统的【 fsync() 】操作。
因为 MySQL 进程和其占用的内存空间都工作在操作系统的【用户空间】中,所以 MySQL 的 【Log Buffer】也工作在操作系统的用户空间中。
默认情况下,如果想要将【Log Buffer】中的数据持久化到磁盘的日志文件(Redo Log 文件)中,还需要经过操作系统的【内核空间缓冲区(OS Buffer)】。
fsync() :是系统中的一个强制的同步操作,用于强制将文件系统缓冲区中的数据写到磁盘中,并确保数据的持久化。
OS Buffer(Operating System Buffer):通常指的是操作系统级别的缓冲区。
刷盘流程图
从 Redo Log Buffer 缓冲区中将数据持久化到磁盘的日志文件(Redo Log 文件)中的大致流程如图:
可以看出,Redo Log 从用户空间的 Log Buffer 写入磁盘的 Redo Log 文件时,需要经过内核空间的 OS Buffer 。这是因为在打开日志文件时,没有使用 O_DIRECT 标志位,而 O_DIRECT 标志位可以不经过操作系统内核空间的 OS Buffer ,直接向 磁盘写数据。
在 InnoDB 存储引擎中,Redo Log 具有一下几种刷盘规则:
1、开启事务,发出提交事务指令后,是否刷新日志由变量【 innodb_flush_log_at_trx_commit 】 决定。
innodb_flush_log_at_trx_commit 是 MySQL 中 InnoDB 存储引擎的一个配置参数,用于控制事务提交时日志写入磁盘的行为。
2、每秒刷新一次,刷新日志的频率由变量【 innodb_flush_log_at_timeout 】的值决定,默认是 1s 。需要注意的是,刷新日志的频率和是否执行了 commit 操作无关。
3、当 Log Buffer 中已经使用的内存超过一半时,也会触发刷盘操作。
4、当事务中存在 checkpoint(检查点)时,在一定程度上代表了刷写到磁盘时,日志所处的 LSN 的位置。
其中,LSN(Log Sequence Number) 表示日志的逻辑序列号。
对第一条规则进行简单介绍:
当事务提交时,需要先将事务日志写入到 【 Log Buffer 】,这些写入 Log Buffer 的日志并不是随着事务的提交立刻写入到磁盘的,而是根据一定的规则将 Log Buffer 中的数据刷写到磁盘,从而保证了 【Redo Log 文件】中数据的持久性。
这种刷盘规则可以通过【innodb_flush_log_at_trx_commit 】变量控制,innodb_flush_log_at_trx_commit 变量可取的值有【 0 、1 和 2 】,默认是 1 。
每个取值代表的刷盘规则如下图所示:
如果该变量设置为 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 也到磁盘就只有一种检查点规则。
4、Redo Log 刷盘最佳实践演示
不同的 Redo Log 刷盘规则,对 MySQL 数据库性能的影响也不同。
接下来,演示【innodb_flush_log_at_trx_commit 】变量的不同取值,对 MySQL 数据库的性能影响。
1、创建一个 test 数据库,在数据库中创建一个名为【 flush_disk_test 】的数据表,如下所示:
2、为了测试方便,这里创建一个名为 insert_data 的存储过程,接收一个 int 类型的参数。这个参数表示像 flush_disk_test 数据表中插入的记录行数,如下所示:
3、查看 会话级别 和 全局级别 的 innodb_flush_log_at_trx_commit 变量的值
innodb_flush_log_at_trx_commit = 0
4、将 innodb_flush_log_at_trx_commit 变量的值设置为 0 。调用 insert_data 向 flush_disk_test 数据表中插入 10 万条数据,如下所示:
太久了,改成插入 1万条数据来演示:
把原本的表清空和截断,再重新测试一下:
可以看到,将 innodb_flush_log_at_trx_commit 变量的值设置为 0 时,向表中插入 1 万条数据耗时 10.60 秒。
innodb_flush_log_at_trx_commit = 1
5、将 innodb_flush_log_at_trx_commit 变量的值设置为 1,调用 insert_data 向 flush_disk_test 数据表中插入 1 万条数据,如下所示:
可以看到,将 innodb_flush_log_at_trx_commit 变量的值设置为 1 时,向表中插入 1 万条数据耗时 20.30 秒。
innodb_flush_log_at_trx_commit = 2
6、将 innodb_flush_log_at_trx_commit 变量的值设置为 2,调用 insert_data 向 flush_disk_test 数据表中插入 1 万条数据,如下所示:
可以看到,将 innodb_flush_log_at_trx_commit 变量的值设置为 1 时,向表中插入 1 万条数据耗时 10.39 秒。
如图,可以看出,当 innodb_flush_log_at_trx_commit 变量的值设置为 0 或者 2 时,插入 1 万条数据耗费的时间差别不是很大,但是与 innodb_flush_log_at_trx_commit = 1 对比来看,耗时差别就比较大了。
需要注意的是,虽然将 innodb_flush_log_at_trx_commit 变量的值设置为 0 或者 2 时,插入数据的性能比较高,但是在系统发生故障时,可能会丢失 1s 的数据,而这 1s 内可能会产生大量的数据。
也就是说,可能会造成大量数据丢失。
优化存储过程–>性能提升百倍
上面的 insert_data 存储过程还有优化的空间,就是在存储过程中,把事务的开启和关闭放到循环体外面,如下所示:
此时再重新测试一遍,发现几种刷盘的性能都提升了很大
特别是 innodb_flush_log_at_trx_commit = 1 时,性能提升了 96 倍。
变量值为 0 和 2 的,性能提升接近 50 倍。
命令汇总
# 登录 mysql 服务器
e:
cd E:\install\mysql8\mysql-8.0.21-winx64\bin
mysql -u root -p
密码:123456
# 如果 test 数据库不存在,则创建
create database if not exists test;
# 使用这个test数据库
use test;
# 创建 flush_disk_test 这张表
create table flush_disk_test(
id int not null auto_increment,
name varchar(20),
primary key(id)
)engine=InnoDB;
# 创建一个名为 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 'ljh';
while s<=i do
start transaction;
insert into flush_disk_test (name) values (c);
commit;
set s = s + 1;
end while;
end$$
delimiter ;
# 查看会话级别的 innodb_flush_log_at_trx_commit 变量的值
SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';
# 查看全局级别的 innodb_flush_log_at_trx_commit 变量的值
SHOW GLOBAL VARIABLES LIKE 'innodb_flush_log_at_trx_commit';
# 通过设置该变量的值,来设置日志记录刷盘的规则-->0、1、2
SET GLOBAL innodb_flush_log_at_trx_commit = 0;
# 优化 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 'ljh';
start transaction;
while s<=i do
insert into flush_disk_test (name) values (c);
set s = s + 1;
end while;
commit;
end$$
delimiter ;
5、Redo Log 写入机制
Redo Log 主要记录的是 物理日志 ,其文件内容是以顺序循环的方式写入的,一个文件写满时,会写入另一个文件,最后一个文件写满时,会向第一个文件写数据,并且是覆盖写。
如图所示:
Redo Log 写入机制流程图
1、Wirte Pos(Write Position) 是数据表中当前记录(某一行数据的位置)所在的位置(可以理解为:数据写入位置),随着不断地向数据表中写数据,这个位置会向后移动,当移动到最后一个日志文件的最后一个位置时,又会回到第一个日志文件的开始位置进行写操作。
2、CheckPoint 是当前要擦除的位置(位置存的是日志记录),这个位置也是向后移动的,移动到最后一个日志文件的最后一个位置时,也会回到第一个日志文件的开始位置进行擦除。只不过在擦除记录之前,需要把记录更新到数据文件(指的就是磁盘中的 .idb 文件)中。
擦除位置表示的是日志文件中可以被擦除的位置,即已经写入到数据文件中并且不再需要的日志记录的位置。在执行 Checkpoint 操作时,数据库系统可能会清理或擦除这些已经写入的日志记录,以释放磁盘空间并维护日志文件的大小。
3、Write Pos 和 CheckPoint 之间存在间隔时,中间的间隔表示还可以记录新的操作。
如果 Write Pos 移动的速度比较快,追上了 CheckPoint ,则表示数据已经写满,不能再向 Redo Log 文件中写数据了。
此时,需要停止写入数据,擦除一些记录。
6、Redo Log 的 LSN 机制 -->(Log Sequence Number)
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 的值。
show engine innodb status \G
LSN 值
重要的参数说明如下所示:
Log sequence number:表示当前内存缓冲区中的 Redo Log 的 LSN 值
Log flushed up to:表示刷新到磁盘上的 Redo Log 文件中的 LSN 值
Pages flushed up to: 表示已经刷新到磁盘数据页上的 LSN 值
Last checkpoint at:表示上一次检查点所在位置的 LSN 值
7、Redo Log 相关参数
在 MySQL 中,输入如下命令可以查看于 Redo Log 相关的参数:
show variables like '%innodb_log%';
可以查询到于 Redo Log 有关的几个重要参数如下所示:
innodb_log_buffer_size = 16777216 :表示 log buffer 的大小,默认是 8 MB。
innodb_log_file_size = 50331648 :表示事务日志的大小,默认是5 MB 。
innodb_log_files_in_group = 2 :表示事务日志组中的事务日志文件个数,默认是 2 个。
innodb_log_group_home_dir = . \ :表示事务日志组所在的目录,当前目录表示 MySQL 数据所在的目录。