关于java:InnoDB学习四之RedoLog和UndoLog

74次阅读

共计 15910 个字符,预计需要花费 40 分钟才能阅读完成。

BinLog 是 MySQL Server 层的日志,所有的 MySQL 存储引擎都反对 BinLog。BinLog 能够反对主从复制和数据恢复,然而对事务的 ACID 个性反对比拟差。InnoDB 存储引擎引入 RedoLog 和 UndoLog 事务日志,用于晋升事务场景下的数据库性能。本文会对 RedoLog 和 UndoLog 进行介绍。

RedoLog 和 UndoLog

ChangeBuffer 和 WAL

咱们以一条 SQL 更新语句来介绍 RedoLog 的作用,首先在数据库中创立 user_info 表,该表蕴含主键列 id 和姓名列,并向数据库中插入一列测试数据:

create table user_info
(
    id int primary key,
    name  varchar(255)
);

insert into user_info(id,name) value (1,'ls');

查问语句的执行流程

如果咱们须要查问 id=1 的用户的信息,咱们能够通过以下 SQL 语句进行查问:

select  * from user_info where id = 1;

在这一条简略的查问语句之后,MySQL 做了哪些工作呢?如下所示,MySQL 执行 SQL 查问语句的流程蕴含以下步骤:

  1. 连接器:客户端和 MySQL 服务端建设连贯,用户名明码等信息校验;
  2. 查问缓存:如果 SQL 语句是查问语句,则查看查问语句是否命中缓存;
  3. 分析器:对 SQL 语句的词法和语法进行剖析,判断 SQL 语句的类型和对应的表等信息;
  4. 优化器:对 SQL 语句进行优化,抉择适合的索引;
  5. 执行器:在对应的 MySQL 引擎上执行 SQL 查问语句,并返回查问后果;

更新语句的执行流程

如果咱们不须要查问用户信息,而是要更新 id=1 的记录中的用户名为zs,则能够通过以下 SQL 语句进行更新:

update user_info set name="zs" where id=1;

和上文中的查问语句相似,MySQL 一样会先通过连接器建设数据库连贯,而后通过分析器、优化器和执行器查找到须要更新的数据所在的行,而后更新数据。

和查问流程不一样的是,更新流程还波及 ChangeBuffer 和两个重要的日志模块:BinLog 和 RedoLog。其中 BinLog 和 ChangeBuffer 的作用曾经在前文中介绍过,BinLog 用于主从复制和数据恢复,ChangeBuffer 用于缓存对数据库中数据的操作,RedoLog 则是本文介绍的配角了。

ChangeBuffer 技术

对于上文中的更新语句,如果没有 RedoLog,那么 InnoDB 引擎会依照索引查找到 id=1 的用户记录,把记录加载到内存中,而后批改内存中的数据事务提交后再写回磁盘。如果数据库数据更新的频率非常低,那么这样更新形式数据库也能够承受,然而在更新十分频繁的状况下,大量的离散 IO 会成为数据库的瓶颈,影响数据库的性能。

在更新频繁的场景下,如何升高磁盘的 IO 并保障事务呢?这就波及到咱们前边文章中介绍过的 ChangeBuffer 技术了,在满足 ChangeBuffer 缓存操作的条件下,InnoDB 并不会立刻把数据的变更操作写入磁盘,而是将这些对数据页的操作缓存到 ChangeBuffer 中,数据库找适合的机会再将操作 Merge 到数据库中。

通过 ChangeBuffer 技术,咱们能够把对数据库的屡次离散拜访合并为一次数据库拜访,并且用户的更新线程中不须要理论拜访磁盘,大大晋升了数据库性能。

WAL 技术

不过不晓得大家有没有留神到,ChangeBuffer 有一个很大的问题:如果 InnoDB 实例在运行期间掉电,ChangeBuffer 中的缓存会失落,从而造成数据库数据的不统一,影响数据库事务的原子性和一致性。

数据库中保障事务原子性和一致性通用的计划是采纳 WAL(Write-ahead logging,预写式日志)技术,在应用 WAL 的零碎中,所有的批改都先被写入到日志中,而后再被利用到零碎状态中,日志通常蕴含 redo 和 undo 两局部信息。

  • RedoLog 称为重做日志,每当有操作时,在数据变更之前将操作写入 RedoLog,这样当产生掉电之类的状况时零碎能够在重启后持续操作;
  • UndoLog 称为撤销日志,当一些变更执行到一半无奈实现时,能够依据撤销日志复原到变更之间的状态;

MySQL 的 InnoDB 引擎中就应用了 WAL 技术,所以 InnoDB 存储引擎蕴含了 RedoLog 和 UndoLog 两局部日志。

如何确保曾经提交的事务不会失落?解决这个问题比较简单,InnoDB 有一个 Log-Force-at-Commit 机制,在事务提交的时候,和这个事务相干的 RedoLog 数据,包含 Commit 记录,都必须从 LogBuffer 中写入 RedoLog 文件,此时事务提交胜利的信号能力发送给用户过程。通过这个机制,能够确保哪怕这个曾经提交的事务中的局部 ChangeBuffer 还没有被写入数据文件,就产生了实例故障,在做实例复原的时候,也能够通过 RedoLog 的信息,将不统一的数据前滚。

RedoLog 和 BinLog 比拟

RedoLog 和 BinLog 不同。尽管 BinLog 中也记录了 InnoDB 表的很多操作,也能实现重做的性能,然而它们之间有很大区别。

  1. BinLog 是在存储引擎的下层产生的,不论是什么存储引擎,对数据库进行了批改都会产生二进制日志。而 RedoLog 是 Innodb 引擎层产生的,只记录该存储引擎中表的批改;
  2. BinLog 记录数据变更的逻辑性的语句,如某一行数据的的变更状况或此次变更的 SQL 语句。而 RedoLog 是在物理格局上的日志,它记录的是数据库中每个页的批改;
  3. BinLog 只在每次事务提交的时候一次性写入缓存中的日志 ” 文件 ”(对于非事务表的操作,则是每次执行语句胜利后就间接写入)。而 RedoLog 在数据筹备批改前写入缓存中的 RedoLog 中,而后才对缓存中的数据执行批改操作;而且保障在收回事务提交指令时,先向缓存中的 RedoLog 写入磁盘日志,写入实现后才执行提交动作;
  4. BinLog 只在提交的时候一次性写入,所以 BinLog 记录形式和提交程序无关,且一次提交对应一次记录。而 RedoLog 中是记录的物理页的批改,RedoLog 文件中同一个事务可能屡次记录,最初一个提交的事务记录会笼罩所有未提交的事务记录。例如事务 T1,可能在 RedoLog 中记录了 T1-1,T1-2,T1-3,T1共 4 个操作,其中 T1示意最初提交时的日志记录,所以对应的数据页最终状态是 T1对应的操作后果。而且 RedoLog 是并发写入的,不同事务之间的不同版本的记录会交叉写入到 RedoLog 文件中,例如可能 RedoLog 的记录形式如下:T1-1,T1-2,T2-1,T2-2,T2,T1-3,T1*。

事务日志记录的是物理页的状况,它具备幂等性,因而记录日志的形式极其简练。幂等性的意思是屡次操作前后状态是一样的,例如新插入一行后又删除该行,前后状态没有变动。而二进制日志记录的是所有影响数据的操作,记录的内容较多。例如插入一行记录一次,删除该行又记录一次。

RedoLog

RedoLog 包含两局部:一是内存中的日志缓冲(RedoLog Buffer),该局部日志是易失性的;二是磁盘上的重做日志文件(RedoLog File),该局部日志是长久的。

在概念上,Innodb 通过 force-log-at-commit 机制实现事务的持久性,即在事务提交的时候,必须先将该事务的所有事务日志写入到磁盘上的 RedoLog File 和 UndoLog File 中进行长久化。

为了确保每次日志都能写入到事务日志文件中,在每次将 RedoLog Buffer 中的日志写入日志文件的过程中都会调用一次操作系统的 fsync 操作 (即fsync() 零碎调用)。因为 MariaDB/MySQL 是工作在用户空间的,MariaDB/MySQL 的 RedoLog Buffer 处于用户空间的内存中。要写入到磁盘上的 RedoLog Buffer 中,两头还要通过操作系统内核空间的操作系统缓存区,调用 fsync() 的作用就是将操作系统缓存区中的日志刷到磁盘上的 RedoLog 文件中。

RedoLog 事务日志文件名为 ib_logfileN,如:ib_logfile0,ib_logfile1……

RedoLog 把日志从缓存写入磁盘的过程如下图所示:

MySQL 反对用户自定义在事务提交时如何将日志缓存中的日志刷磁盘文件中。能够管制通过变量 innodb_flush_log_at_trx_commit 的值来决定。该变量有 3 种值:0、1、2,默认为 1。但留神,这个变量只是管制事务提交时是否刷新日志缓存到磁盘。

  • 当设置为 1 的时候,事务提交时会将日志缓存中的日志写入操作系统缓存,并调用 fsync()长久化到磁盘文件中。这种形式即便零碎解体也不会失落任何数据,然而因为每次提交都写入磁盘,IO 的性能较差;
  • 当设置为 0 的时候,事务提交时不会将日志缓存中的日志写入操作系统缓存,而是每秒写入操作系统缓存并调用 fsync()长久化到磁盘文件中。也就是说设置为 0 时是 (大概) 每秒刷新写入到磁盘中的,当零碎解体,会失落 1 秒钟的数据;
  • 当设置为 2 的时候,事务提交时仅写入到操作系统缓存,而后是每秒调用 fsync()将操作系统缓存中的日志长久化到磁盘文件中;

有一个变量 innodb_flush_log_at_timeout 的值为 1 秒,该变量示意的是刷日志的频率,很多人误以为是管制 innodb_flush_log_at_trx_commit值为 0 和 2 时的 1 秒频率,实际上并非如此。测试时将频率设置为 5 和设置为 1,当 innodb_flush_log_at_trx_commit 设置为 0 和 2 的时候性能根本都是不变的。对于这个频率是管制什么的,在前面的 ” 刷日志到磁盘的规定 ” 中会说。

一致性的保障

在主从复制构造中,要保障事务的持久性和一致性,须要对日志相干变量设置为如下:

  • 如果启用了 BinLog,则设置sync_binlog=1,即每提交一次事务同步写到磁盘中。
  • 总是设置innodb_flush_log_at_trx_commit=1,即每提交一次事务都写到磁盘中。

上述两项变量的设置保障了:每次提交事务都写入二进制日志和事务日志,并在提交时将它们刷新到磁盘中。

抉择形式 1 时,因为每次事务提交都会写磁盘,在大量小事务提交的场景下会影响数据库的性能。

RedoLog 日志块

Innodb 存储引擎中,RedoLog 以块为单位进行存的,每个块占 512 字节,这称为 RedoLog 日志块。不论是日志缓存中还是零碎缓存以及磁盘上的 RedoLog 文件,RedoLog 都是这样以 512 字节的块存储的。

RedoLog 记录的是数据页的变动,当一个数据页产生的变动须要应用超过 492 字节的 RedoLog 来记录,那么就会应用多个 RedoLog 日志块来记录该数据页的变动。

对于 RedoLog 日志块头的第三局部 log_block_first_rec_group,因为有时候一个数据页产生的日志量超出了一个日志块,这是须要用多个日志块来记录该页的相干日志。例如,某一数据页产生了 552 字节的日志量,那么须要占用两个日志块,第一个日志块占用 492 字节,第二个日志块须要占用 60 个字节,那么对于第二个日志块来说,它的第一个日志的开始地位就是 73 字节(60+12)。如果log_block_first_rec_group 的值和 log_block_hdr_data_len 相等,则阐明该日志块中没有新开始的日志块,即示意该日志块用来连续前一个日志块。
日志尾只有一个局部:log_block_trl_no,该值和块头的 log_block_hdr_no 相等。

内存中的 RedoLog 缓存和磁盘中的 RedoLog 文件由多个日志块组成,示意图如下所示:

RedoLog 日志组

RedoLog 日志组由多个大小完全相同的 RedoLog 文件组成。组内 RedoLog 文件的数量由变量 innodb_log_files_group 决定,默认值为 2,即两个 RedoLog 文件组成 RedoLog 日志组。这个组是一个逻辑的概念,并没有真正的文件来示意这是一个组,然而能够通过变量 innodb_log_group_home_dir来定义组的目录,RedoLog 文件会放在这个目录下(默认是在 datadir 下)。

mysql>  show global variables like "innodb_log%";
+-----------------------------+----------+
| Variable_name               | Value    |
+-----------------------------+----------+
| innodb_log_buffer_size      | 16777216 |
| innodb_log_checksums        | ON       |
| innodb_log_compressed_pages | ON       |
| innodb_log_file_size        | 50331648 |
| innodb_log_files_in_group   | 2        |
| innodb_log_group_home_dir   | ./       |
| innodb_log_write_ahead_size | 8192     |
+-----------------------------+----------+
7 rows in set (0.06 sec)
root@b48ce1e480fd:/var/lib/mysql# ls -l ib*
-rw-r----- 1 mysql root       407 Oct 21 09:36 ib_buffer_pool
-rw-r----- 1 mysql mysql 50331648 Oct 26 09:00 ib_logfile0
-rw-r----- 1 mysql mysql 50331648 Oct 20 07:24 ib_logfile1
-rw-r----- 1 mysql mysql 79691776 Oct 26 09:00 ibdata1
-rw-r----- 1 mysql mysql 12582912 Oct 26 09:00 ibtmp1

能够看到在 MySQL 默认的数据目录下,有两个 ib_logfile 结尾的文件,它们就是 RedoLog 日志组中的 RedoLog 文件,而且它们的大小完全一致且等于变量 innodb_log_file_size 定义的值。ibdata1 文件是在没有开启 innodb_file_per_table 时的共享表空间文件,对应于开启 innodb_file_per_table时的.ibd 文件。

在 Innodb 将日志缓存中的 RedoLog 日志块刷到 RedoLog 文件中时,会以追加写入的形式循环轮训写入。即先在第一个 RedoLog 文件(即 ib_logfile0)的尾部追加写,直到满了之后向第二个 RedoLog 文件(即 ib_logfile1)写。当第二个 RedoLog 文件满了会清空一部分第一个 RedoLog 文件持续写入。

因为是将日志缓存中的日志刷到 RedoLog 文件,所以在 RedoLog 文件中记录日志的形式也是 RedoLog 日志块的形式。RedoLog 文件的大小对 Innodb 的性能影响十分大,设置的太大,复原的时候就会工夫较长,设置的太小,就会导致在写 RedoLog 的时候循环切换 RedoLog 文件。

在每个组的第一个 RedoLog 文件中,前 2KB 记录 4 个特定的局部,从 2KB 之后才开始记录 RedoLog 日志块。除了第一个 RedoLog 文件中会记录,RedoLog 日志组中的其余 RedoLog 文件不会记录这 2KB,然而却会腾出这 2KB 的空间。

RedoLog 文件格式

Innodb 存储引擎存储数据的单元是页,所以 RedoLog 也是基于页的格局来记录的。默认状况下,Innodb 的页大小是 16KB(由 innodb_page_size 变量管制),一个页内能够寄存多个 RedoLog 日志块(每个 512 字节),而 RedoLog 日志块中记录的又是数据页的变动。

其中 RedoLog 日志块中 492 字节的局部是 RedoLog 内容,该 RedoLog 内容的格局分为 4 局部:

  • redo_log_type:占用 1 个字节,示意 RedoLog 的日志类型;
  • space:示意表空间的 ID,采纳压缩的形式后,占用的空间可能小于 4 字节;
  • page_no:示意页的偏移量,同样是压缩过的;
  • redo_log_body示意每个重做日志的数据局部,复原时会调用相应的函数进行解析。

RedoLog 记录格局

RedoLog 的实质上是记录事务对数据库做了哪些批改。InnoDB 的设计者们针对事务对数据库的不同批改场景定义了多种类型的 RedoLog 日志,然而绝大部分类型的 redo 日志都有下边这种通用的构造:

各个局部的具体释义如下:

  • type:该条 redo 日志的类型。在 MySQL 5.7.21 这个版本中,InnoDB 中的 redo 日志蕴含 53 种不同的类型,稍后会具体介绍不同类型的 redo 日志。
  • space ID:表空间 ID。
  • page number:页号。
  • data:该条 redo 日志的具体内容。

对于 RedoLog 更具体的格局本文就不具体做介绍,有趣味的能够本人查找文档理解一下。咱们到此处应该晓得,如果咱们应用 Insert 语句向数据库中插入一条记录,那么 RedoLog 会记录要在指定空间的指定数据页的指定地址处设置指定的值。

RedoLog 刷盘策略

变量 innodb_flush_log_at_trx_commit 的值为 1 时,、事务每次提交的时候都会刷 RedoLog 事务日志到磁盘中,然而 Innodb 不仅仅只会在有 ICommit 动作后才会刷日志到磁盘,这只是 innodb 存储引擎刷日志的规定之一。触发日志刷盘的场景有以下几种:

  1. 收回 Commit 动作时,Commit 收回后是否刷日志由变量 innodb_flush_log_at_trx_commit 管制。
  2. 每秒刷一次。这个刷日志的频率由变量 innodb_flush_log_at_timeout 值决定,默认是 1 秒。要留神,这个刷日志频率和 commit 动作无关。
  3. 当 log buffer 中曾经应用的内存超过一半时。
  4. 当有 checkpoint 时,checkpoint 在肯定水平上代表了刷到磁盘时日志所处的 LSN 地位。

刷脏和 CheckPoint

内存中 (BufferPool) 未刷到磁盘的数据称为脏数据(DirtyData)。因为数据和日志都以页的模式存在,所以脏页示意脏数据和脏日志。上一节介绍了日志是何时刷到磁盘的,不仅仅是日志须要刷盘,脏数据页也一样须要刷盘。

在 Innodb 中,数据刷盘的规定只有一个:Checkpoint。然而触发 Checkpoint 的状况却有几种。不管怎样,Checkpoint 触发后,会将缓存中脏数据页和脏日志页都刷到磁盘。

innodb 存储引擎中 Checkpoint 分为两种:

  • Sharp Checkpoint:在重用 RedoLog 文件 (例如切换日志文件) 的时候,将所有已记录到 RedoLog 中对应的脏数据刷到磁盘。
  • Fuzzy Checkpoint:一次只刷一小部分的日志到磁盘,而非将所有脏日志刷盘。有以下几种状况会触发该检查点:

    1. Master Thread Checkpoint:由 Master 线程管制,每秒或每 10 秒刷入肯定比例的脏页到磁盘;
    2. flush_lru_list checkpoint:从 MySQL5.6 开始可通过 innodb_page_cleaners 变量指定专门负责脏页刷盘的 PageCleaner 线程的个数,该线程的目标是为了保障 lru 列表有可用的闲暇页;
    3. Async/Sync Flush Checkpoint:同步刷盘还是异步刷盘。例如还有十分多的脏页没刷到磁盘(十分多是多少,有比例控制),这时候会抉择同步刷到磁盘,但这很少呈现;如果脏页不是很多,能够抉择异步刷到磁盘,如果脏页很少,能够临时不刷脏页到磁盘;
    4. Dirty Page Too Much Checkpoint:脏页太多时强制触发检查点,目标是为了保障缓存有足够的闲暇空间。Too Much 的比例由变量 innodb_max_dirty_pages_pct 管制,MySQL 5.6 默认的值为 75,即当脏页占缓冲池的百分之 75 后,就强制刷一部分脏页到磁盘。因为刷脏页须要肯定的工夫来实现,所以记录检查点的地位是在每次刷盘完结之后才在 RedoLog 中标记的。

MySQL 进行时是否将脏数据和脏日志刷入磁盘,由变量 innodb_fast_shutdown={0|1|2} 管制,默认值为 1,即进行时只做一部分 purge,疏忽大多数 flush 操作(但至多会刷日志),在下次启动的时候再 flush 残余的内容,实现 FastShutdown。

LSN 学习

LSN 称为日志的逻辑序列号(Log Sequence Number),在 Innodb 存储引擎中,LSN 占用 8 个字节,LSN 的值会随着日志的写入而递增。剖析 LSN 能够失去很多要害信息:

  1. 数据页的版本信息。
  2. 写入的日志总量,通过 LSN 开始号码和完结号码能够计算出写入的日志量。
  3. CheckPoint 的地位。

LSN 不仅存在于 RedoLog 中,还存在于数据页中,在每个数据页的头部,有一个 fil_page_lsn 记录了当前页最终的 LSN 值是多少。通过数据页中的 LSN 值和 RedoLog 中的 LSN 值比拟,如果页中的 LSN 值小于 RedoLog 中 LSN 值,则示意数据失落了一部分,这时候能够通过 RedoLog 的记录来复原到 RedoLog 中记录的 LSN 值时的状态。

RedoLog 的 LSN 信息能够通过 show engine innodb status 来查看。MySQL 5.5 版本的 show 后果中只有 3 条记录,没有pages flushed up to

mysql> show engine innodb status
......
---
LOG
---
Log sequence number 12734454
Log flushed up to   12734454
Pages flushed up to 12734454
Last checkpoint at  12734445
0 pending log flushes, 0 pending chkp writes
45 log i/o's done, 0.00 log i/o's/second

其中

  • log sequence number 就是以后的 RedoLog 中的 LSN,通常和缓存中的 LSN 统一,称为缓存日志 LSN;
  • log flushed up to 是磁盘上 RedoLog 文件中的 LSN,通常会比日志缓存 LSN 小,称为磁盘日志 LSN;
  • pages flushed up to 是曾经刷到磁盘数据页上的 LSN,称为磁盘数据页 LSN;
  • last checkpoint at 是上一次检查点所在位置的 LSN,称为 CheckPoint LSN。

Innodb 执行批改数据库语句的流程如下所示:

  1. 向 RedoLog 缓存中写入 RedoLog,并在 RedoLog 中记录对应的 LSN,记为缓存日志 LSN;
  2. 如果指标数据页在缓存中,批改缓存中的数据页,并在数据页中记录 LSN,记为缓存数据页 LSN;
  3. 日志刷回磁盘时,在 RedoLog 文件中记录对应的 LSN,记为磁盘日志 LSN;
  4. CheckPoint 刷脏时缓存数据页中的 LSN,记为 CheckPoint LSN;
  5. Checkpoint 要刷入的数据页多时,刷入所有的数据页须要肯定的工夫来实现,中途刷入的每个数据页都会记下当前页所在的 LSN,暂且称之为磁盘数据页 LSN。

如下图展现了一个事务过程中各个 LSN 的变动状况:

  1. 12:00:00.000 时刻,事务开始,初始时假如各个 LSN 均为001
  2. 12:00:00.200 时刻,执行更新语句 1,更新缓存日志 LSN 和缓存数据页 LSN,别离加 1,变更为 001;
  3. 12:00:00.400 时刻,执行更新语句 2,更新缓存日志 LSN 和缓存数据页 LSN,别离加 1,变更为 002;
  4. 12:00:00.600 时刻,执行更新语句 3,更新缓存日志 LSN 和缓存数据页 LSN,别离加 1,变更为 003;
  5. 12:00:01.000 时刻,Checkpoint,将缓存中的日志和数据页刷回磁盘,磁盘数据页和磁盘日志的 LSN 更新为 003,Checkpoint LSN 更新为 003;
  6. 12:00:01.200 时刻,执行更新语句 3,更新缓存日志 LSN 和缓存数据页 LSN,别离加 1,变更为 004;
  7. 12:00:01.400 时刻,事务提交,缓存日志写入磁盘,磁盘日志 LSN 更新为 004;
  8. 12:00:02.000 时刻,Checkpoint,将缓存中的日志和数据页刷回磁盘,磁盘数据页 LSN 更新为 004,Checkpoint LSN 更新为 004;

Innodb Crash-Safe

在启动 innodb 的时候,不论上次是失常敞开还是异样敞开,总是会进行复原操作。因为 RedoLog 记录的是数据页的物理变化,因而复原的时候速度比逻辑日志 (如 BinLog) 要快很多。而且,Innodb 本身也做了肯定水平的优化,让复原速度变得更快。

重启 Innodb 时,Checkpoint 示意曾经残缺刷到磁盘上数据页的 LSN,因而复原时仅须要复原从 Checkpoint 开始的日志局部。例如,当数据库在上一次 Checkpoint 的 LSN 为 10000 时宕机,且事务是曾经提交过的状态。启动数据库时会查看磁盘中数据页的 LSN,如果数据页的 LSN 小于日志中的 LSN,则会从 Checkpoint 开始复原。

还有一种状况,在宕机前正处于 Checkpoint 的刷盘过程,且数据页的刷盘进度超过了日志页的刷盘进度。这时候一宕机,数据页中记录的 LSN 就会大于日志页中的 LSN,在重启的复原过程中会查看到这一状况,这时超出日志进度的局部将不会重做,因为这自身就示意曾经做过的事件,无需再重做。

另外,事务日志具备幂等性,所以屡次操作失去同一后果的行为在日志中只记录一次。而二进制日志不具备幂等性,屡次操作会全副记录下来,在复原的时候会屡次执行二进制日志中的记录,速度就慢得多。例如,某记录中 id 初始值为 2,通过 update 将值设置为了 3,起初又设置成了 2,在事务日志中记录的将是无变动的页,基本无需复原;而二进制会记录下两次 update 操作,复原时也将执行这两次 update 操作,速度比事务日志复原更慢。

RedoLog 相干变量

  • innodb_flush_log_at_trx_commit={0|1|2}:指定何时将事务日志刷到磁盘,默认为 1;

    1. 0 示意每秒将 ”log buffer” 同步到 ”os buffer” 且从 ”os buffer” 刷到磁盘日志文件中;
    2. 1 示意每事务提交都将 ”log buffer” 同步到 ”os buffer” 且从 ”os buffer” 刷到磁盘日志文件中;
    3. 2 示意每事务提交都将 ”log buffer” 同步到 ”os buffer” 但每秒才从 ”os buffer” 刷到磁盘日志文件中;
  • innodb_log_buffer_size:log buffer 的大小,默认 8M
  • innodb_log_file_size:事务日志的大小,默认 5M
  • innodb_log_files_group =2:事务日志组中的事务日志文件个数,默认 2 个
  • innodb_log_group_home_dir =./:事务日志组门路,当前目录示意数据目录
  • innodb_mirrored_log_groups =1:指定事务日志组的镜像组个数,但镜像性能如同是强制敞开的,所以只有一个 RedoLog 日志组。在 MySQL5.7 中该变量曾经移除。

UndoLog

基本概念

UndoLog 有两个作用:提供回滚和多个行版本控制(MVCC)。

WAL 技术在数据批改的时,不仅记录了 RedoLog,还记录了绝对应的 UndoLog,如果因为某些起因导致事务失败或回滚了,能够借助该 UndoLog 进行回滚。

UndoLog 和 RedoLog 记录物理日志不一样,它是逻辑日志。能够认为当 Delete 一条记录时,UndoLog 中会记录一条对应的 Insert 记录,反之亦然;当 update 一条记录时,它记录一条对应相同的 update 记录。

当执行 Rollback 时,就能够从 UndoLog 中的逻辑记录读取到相应的内容并进行回滚。有时候利用到行版本控制的时候,也是通过 UndoLog 来实现的:当读取的某一行被其余事务锁定时,它能够从 UndoLog 中剖析出该行记录以前的数据是什么,从而提供该行版本信息,让用户实现非锁定一致性读取。

UndoLog 是采纳段 (segment) 的形式来记录的,每个 undo 操作在记录的时候占用一个 UndoLog Segment。

另外,UndoLog 也会产生 RedoLog,因为 UndoLog 也要实现持久性爱护。

UndoLog 存储形式

Innodb 存储引擎对 Undo 的治理采纳段的形式。Rollback Segment 称为回滚段,每个回滚段中有 1024 个 UndoLog Segment。

在以前老版本,只反对 1 个 Rollback Segment,这样就只能记录 1024 个 UndoLog Segment。起初 MySQL5.5 能够反对 128 个 Rollback Segment,即反对 128*1024 个 Undo 操作,还能够通过变量 innodb_undo_logs(5.6 版本以前该变量是 innodb_rollback_segments) 自定义多少个 Rollback Segment,默认值为 128。UndoLog 默认寄存在共享表空间中。

root@b48ce1e480fd:/var/lib/mysql# ls -l ib*
-rw-r----- 1 mysql root       407 Oct 21 09:36 ib_buffer_pool
-rw-r----- 1 mysql mysql 50331648 Oct 26 09:00 ib_logfile0
-rw-r----- 1 mysql mysql 50331648 Oct 20 07:24 ib_logfile1
-rw-r----- 1 mysql mysql 79691776 Oct 26 09:00 ibdata1
-rw-r----- 1 mysql mysql 12582912 Oct 26 09:00 ibtmp1

如果开启了 innodb_file_per_table,UndoLog 将存储在每个表的.ibd 文件中。在 MySQL5.6 中,undo 的寄存地位还能够通过变量innodb_undo_directory 来自定义寄存目录,默认值为 ”.” 示意 datadir。

默认 Rollback Segment 全副写在一个文件中,但能够通过设置变量 innodb_undo_tablespaces 平均分配到多少个文件中。该变量默认值为 0,即全副写入一个表空间文件。该变量为动态变量,只能在数据库示例进行状态下批改,如写入配置文件或启动时带上对应参数。

更新语句与 UndoLog

当事务提交的时候,Innodb 不会立刻删除 UndoLog,因为后续还可能会用到 UndoLog,如隔离级别为 Repeatable-Read 时,事务读取的都是开启事务时的最新提交行版本,只有该事务不完结,该行版本就不能删除,即 UndoLog 不能删除。

然而在事务提交的时候,会将该事务对应的 UndoLog 放入到删除列表中,将来通过 Purge 来删除。并且提交事务时,还会判断 UndoLog 调配的页是否能够重用,如果能够重用,则会调配给前面来的事务,防止为每个独立的事务调配独立的 UndoLog 页而节约存储空间和性能。

通过 UndoLog 记录 Delete 和 Update 操作的后果发现:(insert 操作无需剖析,就是插入行而已)

  • Delete 操作实际上不会间接删除,而是将 Delete 对象打上 Delete flag,标记为删除,最终的删除操作是 Purge 线程实现的。
  • Update 分为两种状况:update 的列是否是主键列。如果不是主键列,在 UndoLog 中间接反向记录是如何 Update 的,即 update 是间接进行的;如果是主键列,update 分两部执行:先删除该行,再插入一行指标行。

UndoLog 中蕴含了旧版本数据行的快照信息,存储在表空间。

BinLog 和事务日志

如下图所示,事务提交时,波及到写日志的中央有三个步骤:

  • 写入 RedoLog,处于 Prepare 状态
  • 写 binlog
  • 批改 redo log 状态为 commit

这里咱们留神到在 redo log 的提交过程中引入了两阶段提交。为什么必须有“两阶段提交”呢?这是为了让两份日志之间的逻辑统一。

因为 RedoLog 和 BinLog 是两个独立的逻辑,如果不必两阶段提交,要么就是先写完 RedoLog 再写 BinLog,或者采纳反过来的程序,咱们看看这两种形式会有什么问题,用下面的更新示例做假如:

  • 先写 RedoLog 后写 BinLog。假如在 RedoLog 写完,BinLog 还没有写完的时候,MySQL 过程异样重启。因为 RedoLog 曾经写完,零碎即便解体依然可能把数据恢复回来。然而 BinLog 外面就没有记录这个语句,因而备份日志的时候 BinLog 外面就没有这条语句;如果须要用这个 BinLog 来复原长期库的话,因为这个语句的 BinLog 失落,复原进去的值就与原库值不同。
  • 先写 BinLog 后写 RedoLog。如果在 BinLog 写完之后宕机,因为 RedoLog 还没写,解体复原当前这个事务有效,所以这一行的值还是未更新以前的值。然而 BinLog 外面曾经记录了解体前的更新记录,BinLog 来复原的时候就多了一个事务进去与原库的值不同。

能够看到,两阶段提交就是为了避免 BinLog 和 RedoLog 不统一产生。同时咱们也留神到为了这个解体复原的一致性问题引入了很多新的货色,也让零碎简单了很多,所以有得有失。二阶段提交 RedoLog 和 BinLog 的过程中,两者刷盘之后都会记录 2PC 事务的 XID(RedoLog 和 BinLog 中事务落盘的标识),若中途数据库 Crash,通过 XID 关联两者并在复原时决定 commit 和 rollback 与否,具体步骤见下一段“复原步骤”。

复原步骤

RedoLog 中的事务如果经验了二阶段提交中的 Prepare 阶段,则会打上 Prepare 标识,如果经验 Commit 阶段,则会打上 Commit 标识(此时 RedoLog 和 BinLog 均已落盘):

  1. 按程序扫描 RedoLog,如果 RedoLog 中的事务既有 Prepare 标识,又有 Commit 标识,就间接提交(复制 RedoLog Disk 中的数据页到磁盘数据页);
  2. 如果 RedoLog 事务只有 Prepare 标识,没有 Commit 标识,则阐明以后事务在 Commit 阶段 Crash 了,RedoLog 中以后事务是否残缺未可知,此时拿着 RedoLog 中以后事务的 XID(RedoLog 和 BinLog 中事务落盘的标识),去查看 binlog 中是否存在此 XID:

    • 如果 BinLog 中有以后事务的 XID,则提交事务(复制 RedoLog disk 中的数据页到磁盘数据页);
    • 如果 BinLog 中没有以后事务的 XID,则回滚事务(应用 UndoLog 来删除 redolog 中的对应事务);

能够将 MySQL 中的 RedoLog 和 BinLog 二阶段提交和狭义上的二阶段提交进行比照,狭义上的二阶段提交,若某个参与者超时未收到协调者的 ack 告诉,则会进行回滚,回滚逻辑须要开发者在各个参与者中进行记录。MySql 二阶段提交是通过 xid 进行复原。

组提交

为了进步性能,通常会将有关联性的多个数据批改操作放在一个事务中,这样能够防止对每个批改操作都执行残缺的长久化操作。这种形式,能够看作是人为的组提交(group commit)。除了将多个操作组合在一个事务中,记录 binlog 的操作也能够按组的思维进行优化:将多个事务波及到的 BinLog 一次性 Flush,而不是每次 Flush 一个 Binlog。

事务在提交的时候不仅会记录事务日志,还会记录二进制日志,然而它们谁先记录呢?BinLog 是 MySQL 的下层日志,先于存储引擎的事务日志被写入。

在 MySQL5.6 以前,当事务提交 (即收回 Commit 指令) 后,MySQL 接管到该信号进入 Commit Prepare 阶段;进入 Prepare 阶段后,立刻写内存中的 BinLog 日志,写完内存中的 BinLog 日志后就相当于确定了 Commit 操作;而后开始写内存中的事务日志;最初将 BinLog 日志和事务日志刷盘,它们如何刷盘,别离由变量 sync_binloginnodb_flush_log_at_trx_commit管制。

但因为要保障 BinLog 日志和事务日志的一致性,在提交后的 Prepare 阶段会启用一个 prepare_commit_mutex 锁来保障它们的程序性和一致性。但这样会导致开启 BinLog 日志后 Group Commmit 生效,特地是在主从复制构造中,简直都会开启 BinLog 日志。在 MySQL5.6 中进行了改良。提交事务时,在存储引擎层的上一层构造中会将事务按序放入一个队列,队列中的第一个事务称为 Leader,其余事务称为 Follower,Leader 管制着 Follower 的行为。尽管程序还是一样先刷 BinLog,再刷事务日志,然而机制齐全扭转了:删除了原来的 prepare_commit_mutex 行为,也能保障即便开启了 BinLog,Group Commit 也是无效的。

MySQL5.6 中分为 3 个步骤:flush 阶段、sync 阶段、commit 阶段:

  • flush 阶段:向内存中写入每个事务的 BinLog;
  • sync 阶段:将内存中的 BinLog 日志刷盘。若队列中有多个事务,那么仅一次 fsync 操作就实现了二进制日志的刷盘操作。这在 MySQL5.6 中称为 BLGC(binary log group commit);
  • commit 阶段:Leader 依据顺序调用存储引擎层事务的提交,因为 Innodb 本就反对 Group Commit,所以解决了因为锁 prepare_commit_mutex 而导致的 Group Commit 生效问题;

在 flush 阶段写入 BinLog 到内存中,然而不是写完就进入 sync 阶段的,而是要期待肯定的工夫,多积攒几个事务的 binlog 一起进入 sync 阶段,等待时间由变量 binlog_max_flush_queue_time 决定,默认值为 0 示意不期待间接进入 sync,设置该变量为一个大于 0 的值的益处是 group 中的事务多了,性能会好一些,然而这样会导致事务的响应工夫变慢,所以倡议不要批改该变量的值,除非事务量十分多并且一直的在写入和更新。

进入到 sync 阶段,会将 Binlog 从内存中刷入到磁盘,刷入的数量和独自的 Binlog 日志刷盘一样,由变量 sync_binlog 管制。

当有一组事务在进行 commit 阶段时,其余新事务能够进行 flush 阶段,它们本就不会互相阻塞,所以 Group Commit 会一直失效。当然,group commit 的性能和队列中的事务数量无关,如果每次队列中只有 1 个事务,那么 group commit 和独自的 commit 没什么区别,当队列中事务越来越多时,即提交事务越多越快时,group commit 的成果越显著。

我是御狐神,欢送大家关注我的微信公众号:wzm2zsd

参考文档

MySQL 实战 45 讲 <br\>
什么是 WAL<br\>
详细分析 MySQL 事务日志 (redo log 和 undo log)<br\>
说过的话就肯定要办到 —— redo 日志(上)<br\>

本文最先公布至微信公众号,版权所有,禁止转载!

正文完
 0