【MySQL技术内幕】7.2.1-事务的实现之redo log

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
日志服务 SLS,月写入数据量 50GB 1个月
简介: 【MySQL技术内幕】7.2.1-事务的实现之redo log

事务隔离性由锁来实现。原子性、一致性、持久性通过数据库的redo log和undo log来完成。redo log称为重做日志,用来保证事务的原子性和持久性。undo log用来保证事务的一致性。

有的DBA或许会认为undo是redo的逆过程,其实不然。redo和undo的作用都可以视为是一种恢复操作,redo恢复提交事务修改的页操作,而undo回滚行记录到某个特定版本。因此两者记录的内容不同,redo通常是物理日志,记录的是页的物理修改操作。undo是逻辑日志,根据每行记录进行记录。

1、基本概念

重做日志用来实现事务的持久性,即事务ACID中的D。其由两部分组成:一是内存中的重做日志缓冲(redo log buffer),其是易失的:二是重做日志文件( redo log file),其是持久的。

InnoDB是事务的存储引擎,其通过Force Log at Commit机制实现事务的持久性,即当事务提交(COMMIT)时,必须先将该事务的所有日志写入到重做日志文件进行持久化,待事务的 COMMIT操作完成才算完成。这里的日志是指重做日志,在InnoDB存储引擎中,由两部分组成,即redo log和undo log。redo log用来保证事务的持久性,undo log用来帮助事务回滚及MvCC的功能。redo log基本上都是顺序写的,在数据库运行时不需要对redo log的文件进行读取操作。而undo log是需要进行随机读写的。

为了确保每次日志都写入重做日志文件,在每次将重做日志缓冲写入重做日志文件后, InnoDB存储引擎都需要调用一次 fsync操作。由于重做日志文件打开并没有使用O_DIRECT选项,因此重做日志缓冲先写入文件系统缓存。为了确保重做日志写入磁盘,必须进行一次 fsync操作。由于fsync的效率取决于磁盘的性能,因此磁盘的性能决定了事务提交的性能,也就是数据库的性能。

InnoDB存储引擎允许用户手工设置非持久性的情况发生,以此提高数据库的性能即当事务提交时,日志不写入重做日志文件,而是等待一个时间周期后再执行 fsync操作。由于并非强制在事务提交时进行一次 fsync操作,显然这可以显著提高数据库的性能。但是当数据库发生宕机时,由于部分日志未刷新到磁盘,因此会丢失最后一段时间的事务。

参数innodb_flush_log_at_trx_commit用来控制重做日志刷新到磁盘的策略。该参数的默认值为1,表示事务提交时必须调用一次 fsync操作。还可以设置该参数的值为0和2。0表示事务提交时不进行写入重做日志操作,这个操作仅在 master thread中完成,而在 master thread中每1秒会进行一次重做日志文件的fync操作。2表示事务提交时将重做日志写入重做日志文件,但仅写入文件系统的缓存中,不进行fync操作。在这个设置下,当 MySQL数据库发生宕机而操作系统不发生宕机时,并不会导致事务的丢失而当操作系统宕机时,重启数据库后会丢失未从文件系统缓存刷新到重做日志文件那部分事务。

下面看一个例子,比较innodb_flush_log_at_trx_commit对事务的影响。首先根据如下代码创建表t和存储过程 p_load:

CREATE TABLE test_load(
a INT,
b CHAR(80)
)ENGINE=INNODB;
DELIMITER∥
CREATE PROCEDURE P load(count INT UNSIGNED)
BEGIN
DECLARE s INT UNSIGNED DEFAULT 1
DECLARE c CHAR(80) DEFAULT REPEAT('a',80);
WHILE s < count DO
INSERT INTO test_load SELECT NULL,c;
COMMIT;
SET s=s+1:
END WHILE;
END;
DELIMITER ;复制代码

存储过程p_load的作用是将数据不断地插入表 test_load中,并且每插入条就进行次显式的 COMMIT操作。在默认的设置下,即参数 innodb_flush_log_at_trx_commit为1的情况下, InnodB存储引擎会将重做日志缓冲中的日志写入文件,并调用一次fsync操作。如果执行命令 CALL p_load(500000),则会向表中插入50万行的记录,并执行50万次的 fsync操作。先看在默认情况插入50万条记录所需的时间下:

mysql> CALL p_load(500000);
Query OK, 0 rows affected (1 min 53.11 sec)复制代码

可以看到插入50万条记录差不多需要2分钟的时间。对于生产环境的用户来说,这个时间显然是不能接受的。而造成时间比较长的原因就在于 fsync操作所需的时间。

接着来看将参数innodb_flush_log_at_trx_commit设置为0的情况

mysql> SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit'\G
Variable_name: innodb_flush_log_at_trx_commit
value:0
1 row in set (0.00 sec)
mysql> CALL p_load(500000);
Query OK, 0 rows affected (13.90 sec)复制代码

可以看到将参数innodb_flush_log_at_trx_commit设置为0后,插入50万行记录的时间缩短为了13.90秒,差不多是之前的12%。而形成这个现象的主要原因是:后者大大减少了fsync的次数,从而提高了数据库执行的性能。下表显示了在参数innodb_flush_log_at_trx_commit的不同设置下,调用存储过程p_load插入50万行记录所需的时间。

不同innodb_flush_log_at_trx_commit设置对于插入的速度影响

innodb_flush_log_at_trx_commit 执行所用时间
0 13.90秒
1 1分53.11秒
2 23.37秒

虽然用户可以通过设置参数innodb_flush_log_at_trx_commit为0或2来提高事务提交的性能,但是需要牢记的是,这种设置方法丧失了事务的ACID特性。而针对上述存储过程,为了提高事务的提交性能,应该在将50万行记录插入表后进行一次的COMMIT操作,而不是在每插入一条记录后进行一次 COMMIT操作。这样做的好处是还可以使事务方法在回滚时回滚到事务最开始的确定状态。

在 MySQL数据库中还有一种二进制日志(binlog),其用来进行 POINT-IN-TIME(PIT)的恢复及主从复制(Replication)环境的建立。从表面上看其和重做日志非常相似,都是记录了对于数据库操作的日志。然而,从本质上来看,两者有着非常大的不同。

首先,重做日志是在 InnoDB存储引擎层产生,而二进制日志是在 MySQL数据库的上层产生的,并且二进制日志不仅仅针对于InnoDB存储引擎, MySQL数据库中的任何存储引擎对于数据库的更改都会产生二进制日志。

其次,两种日志记录的内容形式不同。 MySQL数据库上层的二进制日志是一种逻辑日志,其记录的是对应的SQL语句。而InnoDB存储引擎层面的重做日志是物理格式日志,其记录的是对于每个页的修改。

此外,两种日志记录写人磁盘的时间点不同,如图所示。二进制日志只在事务提交完成后进行一次写人。而 InnoDB存储引擎的重做日志在事务进行中不断地被写入,这表现为日志并不是随事务提交的顺序进行写入的。

从图中可以看到,二进制日志仅在事务提交时记录,并且对于每一个事务,仅包含对应事务的一个日志。而对于 InnoDB存储引擎的重做日志,由于其记录的是物理操作日志,因此每个事务对应多个日志条目,并且事务的重做日志写入是并发的,并非在事务提交时写入,故其在文件中记录的顺序并非是事务开始的顺序。*T1、*T2、*T3表示的是事务提交时的日志。

2、log block

在InnoDB存储引擎中,重做日志都是以512字节进行存储的。这意味着重做日志缓存、重做日志文件都是以块( block)的方式进行保存的,称之为重做日志块(redo log block),每块的大小为512字节。若一个页中产生的重做日志数量大于512字节,那么需要分割为多个重做日志块进行存储。此外,由于重做日志块的大小和磁盘扇区大小一样,都是512字节,因此重做日志的写入可以保证原子性,不需要doublewrite技术。

重做日志块除了日志本身之外,还由日志块头(log block header)及日志块尾(log block tailer)两部分组成。重做日志头一共占用12字节,重做日志尾占用8字节。故每个重做日志块实际可以存储的大小为492字节(512-12-8)。下图显示了重做日志块缓存的结构。

图7-7显示了重做日志缓存的结构,可以发现重做日志缓存由每个为512字节大小的日志块所组成。日志块由三部分组成,依次为日志块头(log block header)、日志内容(log body)、日志块尾(log block tailer)。

log block header由4部分组成,如下表所示。

log block header

名称 占用字节
LOG_BLOCK_HDR_NO 4
LOG_BLOCK_HDR_DATA_LEN 2
LOG_BLOCK_FIRST_REC_GROUP 2
LOG_BLOCK_CHECKPOINT_NO 4

log buffer是由 log block组成,在内部 log buffer就好似一个数组,因此LOG_BLOCK_HDR_NO用来标记这个数组中的位置。其是递增并且循环使用的,占用4个字节,但是由于第一位用来判断是否是 flush bit,所以最大的值为2G。

注意:在innodb1.2.x版本之前,重做日志文件总的大小不得大于等于4G,而1.2.x版本将该限制扩大到了521G。

LOG_BLOCK_HDR_DATA_LEN占用2字节,表示 log block所占用的大小,当log blok被写满时,该值为0x200,表示使用全部log block空间,即占用512字节。

LOG_BLOCK_FIRST_REC_GROUP占用2个字节,表示 log block中第一个日志所在的偏移量。如果该值的大小和LOG_BLOCK_HDR_DATA_LEN相同,则表示当前log block不包含新的日志。如事务T1的重做日志1占用762字节,事务T2的重做日志占用100字节。由于每个log block实际只能保存492个字节,因此其在 log buffer中的情况应如图所示。

从图中可以观察到,由于事务T1的重做日志占用792字节,因此需要占用两个log block。左侧的 log block中LOG_BLOCK_FIRST_REC_GROUP为12,即log block中第一个日志的开始位置。在第二个 log block中,由于包含了之前事务T1的重做日志,事务T2的日志才是 log block中第一个日志,因此该log block的 LOG_BLOCK_FIRST_REC_GROUP为282(270+12)。

LOG_BLOCK_CHECKPOINT_NO占用4字节,表示该 log block最后被写入时的检查点第4字节的值。

log block tailer只由1个部分组成(如下表所示),且其值和LOG_BLOCK_HDR_NO相同,并在函数log block init中被初始化。

log block tailer部分

名称 大小(字节)
LOG_BLOCK_TRL_NO 4

3、log group

log group为重做日志组,其中有多个重做日志文件。虽然源码中已支持log group的镜像功能,但是在ha_ innobase.cc文件中禁止了该功能。因此InnoDB存储引擎实际只有一个 log group。

log group是一个逻辑上的概念,并没有一个实际存储的物理文件来表示 log group信息。 log group由多个重做日志文件组成,每个 log group中的日志文件大小是相同的,且在 InnoDB1.2版本之前,重做日志文件的总大小要小于4GB(不能等于4GB)。

从 InnodB1.2版本开始重做日志文件总大小的限制提高为了512GB。 InnoSQL版本的InnoDB存储引擎在1.1版本就支持大于4GB的重做日志。

重做日志文件中存储的就是之前在 log buffer中保存的 log block,因此其也是根据块的方式进行物理存储的管理,每个块的大小与 log block一样,同样为512字节。在InnoDB存储引擎运行过程中, log buffer根据一定的规则将内存中的 log block刷新到磁盘。这个规则具体是:

  • 事务提交时
  • 当log buffer中有一半的内存空间已经被使用时
  • log checkpoint时

对于 log block的写入追加( append)在 redo log file的最后部分,当一个 redo logfle被写满时,会接着写入下一个 redo log file,其使用方式为 round-robin。

虽然 log block总是在 redo log file的最后部分进行写人,有的读者可能以为对redo log file的写入都是顺序的。其实不然,因为 redo log file除了保存 log buffer刷新到磁盘的log block,还保存了一些其他的信息,这些信息一共占用2KB大小,即每个redo log fle的前2KB的部分不保存log block的信息。对于 log group中的第一个 redo log file,其前2KB的部分保存4个512字节大小的块,其中存放的内容如下表所示。

redo log file前2KB部分的内容

名称 大小(字节)
log file header 512
checkpoint1 512
512
checkpoint2 512

需要特别注意的是,上述信息仅在每个log group的第一个 redo log file中进行存储。

log group中的其余 redo log file仅保留这些空间,但不保存上述信息。正因为保存了这些信息,就意味着对 redo log file的写入并不是完全顺序的。因为其除了 log block的写入操作,还需要更新前2KB部分的信息,这些信息对于 InnoDB存储引擎的恢复操作来说非常关键和重要。故log group与 redo log file之间的关系如图所示。

在 log filer header后面的部分为 InnoDB存储引擎保存的 checkpoint(检查点)值,其设计是交替写入,这样的设计避免了因介质失败而导致无法找到可用的checkpoint的情况。

4、重做日志格式

不同的数据库操作会有对应的重做日志格式。此外,由于 InnoDB存储引擎的存储管理是基于页的,故其重做日志格式也是基于页的。虽然有着不同的重做日志格式,但是它们有着通用的头部格式,如图所示。

通用的头部格式由以下3部分组成:

  • redo_log_type:重做日志的类型
  • space:表空间的ID。
  • page_no:页的偏移量

之后redo log body的部分,根据重做日志类型的不同,会有不同的存储内容,例如,对于页上记录的插入和删除操作,分别对应如下图所示的格式:

到 InnoDE1.2版本时,一共有51种重做日志类型。随着功能不断地增加,相信会加入越来越多的重做日志类型。

5、LSN

LSN是Log Sequence Number的缩写,其代表的是日志序列号。在 InnoDB存储引擎中,LSN占用8字节,并且单调递增。LSN表示的含义有:

  • 重做日志写人的总量
  • checkpoint的位置
  • 页的版本

LSN表示事务写入重做日志的字节的总量。例如当前重做日志的LSN为1000,有个事务T1写入了100字节的重做日志,那么LSN就变为了1100,若又有事务T2写人了200字节的重做日志,那么LSN就变为了1300。可见LSN记录的是重做日志的总量,其单位为字节。

LSN不仅记录在重做日志中,还存在于每个页中。在每个页的头部,有一个值FIL_PAGE_LSN,记录了该页的LSN。在页中,LSN表示该页最后刷新时LSN的大小。因为重做日志记录的是每个页的日志,因此页中的LSN用来判断页是否需要进行恢复操作。例如,页P1的LSN为10000,而数据库启动时, InnoDB检测到写入重做日志中的LSN为13000,并且该事务已经提交,那么数据库需要进行恢复操作,将重做日志应用到Pl页中。同样的,对于重做日志中LSN小于P1页的LSN,不需要进行重做为P1页中的LSN表示页已经被刷新到该位置。

用户可以通过命令 SHOW ENGINE INNODB STATUS查看LSN的情况:

Log sequence number表示当前的LSN, Log flushed up to表示刷新到重做日志文件的LSN, Last checkpoint at表示刷新到磁盘的LSN。虽然在上面的例子中, Log sequence number和 Log flushed up to的值是相同的,但是在实际生产环境中,该值有可能是不同的。因为在一个事务中从日志缓冲刷新到重做日志文件并不只是在事务提交时发生,每秒都会有从日志缓冲刷新到重做日志文件的动作。下面是在生产环境下重做日志的信息的示例。

可以看到,在生产环境下 Log sequence number、 Log fiushed up to、 Last checkpoint at三个值可能是不同的。

6、恢复

InnoDB存储引擎在启动时不管上次数据库运行时是否正常关闭,都会尝试进行恢复操作因为重做日志记录的是物理志,因此恢复的速度比逻辑日志,如二进制日志要快很多。与此同时, InnoDB存储引擎自身也对恢复进行了一定程度的优化,如顺序读取及并行应用重做日志,这样可以进一步地提高数据库恢复的速度。

由于checkpoint表示已经刷新到磁盘页上的LSN,因此在恢复过程中仅需恢复checkpoint开始的日志部分。对于下图中的例子,当数据库在 checkpoint的LSN为10000时发生宕机,恢复操作仅恢复LSN10000~13000范围内的日志。

InnoDB存储引擎的重做日志是物理日志,因此其恢复速度较之二进制日志恢复快得多。例如对于 INSERT操作,其记录的是每个页上的变化。对于下面的表:

CREATE TABLE t(a INT, b INT, PRIMARY KEY(a), KEY(b));

若执行SQL语句:

INSERT INTO t SELECT 1,2;

由于需要对聚集索引页和辅助索引页进行操作,其记录的重做日志大致为:

page(2,3), offset 32, value 1,2  #聚集索引

page(2,4), offset 64, value 2     #辅助索引

可以看到记录的是页的物理修改操作,若插人涉及B+树的 split,可能会有更多的页需要记录日志。此外,由于重做日志是物理日志,因此其是幂等的。幂等的概念如下

f(f(x))=f(x)

有的DBA或开发人员错误地认为只要将二进制日志的格式设置为ROW,那么二进制日志也是幂等的。这显然是错误的,举个简单的例子, INSERT操作在二进制日志中就不是幂等的,重复执行可能会插人多条重复的记录。而上述 INSERT操作的重做日志是幂等的。



相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
17天前
|
存储 Oracle 关系型数据库
【赵渝强老师】MySQL InnoDB的数据文件与重做日志文件
本文介绍了MySQL InnoDB存储引擎中的数据文件和重做日志文件。数据文件包括`.ibd`和`ibdata`文件,用于存放InnoDB数据和索引。重做日志文件(redo log)确保数据的可靠性和事务的持久性,其大小和路径可由相关参数配置。文章还提供了视频讲解和示例代码。
126 11
【赵渝强老师】MySQL InnoDB的数据文件与重做日志文件
|
13天前
|
关系型数据库 MySQL 数据库
【赵渝强老师】MySQL的binlog日志文件
MySQL的binlog日志记录了所有对数据库的更改操作(不包括SELECT和SHOW),主要用于主从复制和数据恢复。binlog有三种模式,可通过设置binlog_format参数选择。示例展示了如何启用binlog、设置格式、查看日志文件及记录的信息。
|
17天前
|
监控 关系型数据库 MySQL
MySQL自增ID耗尽应对策略:技术解决方案全解析
在数据库管理中,MySQL的自增ID(AUTO_INCREMENT)属性为表中的每一行提供了一个唯一的标识符。然而,当自增ID达到其最大值时,如何处理这一情况成为了数据库管理员和开发者必须面对的问题。本文将探讨MySQL自增ID耗尽的原因、影响以及有效的应对策略。
56 3
|
14天前
|
存储 Oracle 关系型数据库
【赵渝强老师】MySQL的撤销日志文件和错误日志文件
本文介绍了MySQL的物理存储结构,重点讲解了InnoDB存储引擎中的撤销日志文件(undo log)和错误日志文件。从MySQL 8.0开始,默认生成两个10MB的undo表空间文件,并支持动态扩容和收缩。错误日志文件记录了MySQL启动、运行、关闭过程中的问题,通过示例展示了如何查看和使用这些日志。
|
2月前
|
存储 关系型数据库 MySQL
MySQL中的Redo Log、Undo Log和Binlog:深入解析
【10月更文挑战第21天】在数据库管理系统中,日志是保障数据一致性和完整性的关键机制。MySQL作为一种广泛使用的关系型数据库管理系统,提供了多种日志类型来满足不同的需求。本文将详细介绍MySQL中的Redo Log、Undo Log和Binlog,从背景、业务场景、功能、底层实现原理、使用措施等方面进行详细分析,并通过Java代码示例展示如何与这些日志进行交互。
95 0
|
17天前
|
SQL 关系型数据库 MySQL
12 PHP配置数据库MySQL
路老师分享了PHP操作MySQL数据库的方法,包括安装并连接MySQL服务器、选择数据库、执行SQL语句(如插入、更新、删除和查询),以及将结果集返回到数组。通过具体示例代码,详细介绍了每一步的操作流程,帮助读者快速入门PHP与MySQL的交互。
30 1
|
19天前
|
SQL 关系型数据库 MySQL
go语言数据库中mysql驱动安装
【11月更文挑战第2天】
32 4
|
2月前
|
存储 关系型数据库 MySQL
Mysql(4)—数据库索引
数据库索引是用于提高数据检索效率的数据结构,类似于书籍中的索引。它允许用户快速找到数据,而无需扫描整个表。MySQL中的索引可以显著提升查询速度,使数据库操作更加高效。索引的发展经历了从无索引、简单索引到B-树、哈希索引、位图索引、全文索引等多个阶段。
64 3
Mysql(4)—数据库索引
|
26天前
|
监控 关系型数据库 MySQL
数据库优化:MySQL索引策略与查询性能调优实战
【10月更文挑战第27天】本文深入探讨了MySQL的索引策略和查询性能调优技巧。通过介绍B-Tree索引、哈希索引和全文索引等不同类型,以及如何创建和维护索引,结合实战案例分析查询执行计划,帮助读者掌握提升查询性能的方法。定期优化索引和调整查询语句是提高数据库性能的关键。
137 1
|
28天前
|
关系型数据库 MySQL Linux
在 CentOS 7 中通过编译源码方式安装 MySQL 数据库的详细步骤,包括准备工作、下载源码、编译安装、配置 MySQL 服务、登录设置等。
本文介绍了在 CentOS 7 中通过编译源码方式安装 MySQL 数据库的详细步骤,包括准备工作、下载源码、编译安装、配置 MySQL 服务、登录设置等。同时,文章还对比了编译源码安装与使用 RPM 包安装的优缺点,帮助读者根据需求选择最合适的方法。通过具体案例,展示了编译源码安装的灵活性和定制性。
82 2