天天看点

MySQL事务(二)-事务的实现概述redo-重做日志undo-回滚日志purgegroup commit(待完善)

事务的实现

  • 概述
  • redo-重做日志
    • 基本概念
    • log block-重做日志块
    • log group-重做日志组
    • 重做日志格式
    • LSN-日志序列号
    • 恢复
  • undo-回滚日志
    • 基本概念
    • undo存储管理
    • undo log格式
      • insert undo log
      • update undo log
  • purge
  • group commit(待完善)

概述

事务隔离性 由 锁 来实现

原子性、一致性、持久性通过数据库的 redo log 和 undo log 来完成

redo log 称为 重做日志,用来保证 事务的原子性和持久性

undo log 用来保证 事务的一致性

redo和undo的作用 都可以 视为是一种 恢复操作

redo 恢复(动词) 提交事务 修改的 页操作(还是 提交 事务修改的页 操作?)

undo 回滚 行记录 到 某个特定版本

两者记录的内容不同

redo通常是物理日志,记录的是 页 的 物理修改操作

undo是逻辑日志,根据 每行记录 进行记录

redo-重做日志

基本概念

重做日志 用来实现 事务的持久性,即事务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存储引擎 都需要 调用一次f(ource)sync操作

由于 重做日志文件 打开 并没有使用

O_DIRECT

选项,因此 重做日志缓冲 先写入 文件系统缓存

为了确保 重做日志 写入磁盘,必须进行一次fsync操作

由于 fsync的效率 取决于 磁盘的性能,因此 磁盘的性能 决定了 事务提交的性能,也就是数据库的性能

InnoDB存储引擎 允许 手工设置 非持久性的情况发生,以此提高数据库的性能

即当事务提交时,日志 不写入 重做日志文件,而是 等待一个时间周期后 再执行 fsync操作

由于 并非 强制 在事务提交时 进行一次fsync操作,显然这可以显著提高数据库的性能

但是当数据库发生宕机时,由于 部分日志 未刷新到 磁盘,因此 会丢失 最后一段时间的事务

参数

innodb_flush_log_at_trx_commit

用来控制重做日志刷新到磁盘的策略

MySQL事务(二)-事务的实现概述redo-重做日志undo-回滚日志purgegroup commit(待完善)
  • 设置为1,默认值,表示 事务提交时 必须调用一次fsync操作
  • 设置为2,表示 事务提交时 将 重做日志 写入 重做日志文件,但 仅写入 文件系统的缓存中,不进行fsync操作。在这个设置下,当 MySQL数据库发生宕机 而 操作系统不发生宕机时,并不会导致事务的丢失。而当操作系统宕机时,重启数据库后 会丢失 未从文件系统缓存 刷新到 重做日志文件 那部分事务
  • 设置为0,表示 事务提交时 不进行 写入 重做日志 操作,这个操作仅在master thread中完成,而在master thread中每1秒会进行一次重做日志文件的fsync操作

虽然可以通过设置参数

innodb_flush_log_at_trx_commit

为0或2来提高事务提交的性能

但是需要牢记的是,这种设置方法丧失了事务的ACID特性

在MySQL数据库中还有一种二进制日志(binlog)

其用来进行 POINT-IN-TIME(PIT,时间点)的恢复 及 主从复制(Replication)环境的建立

从表面上看 其和重做日志 非常相似,都是记录了 对于数据库操作 的 日志

然而,从本质上来看,两者有着非常大的不同

  • 重做日志 是在 InnoDB存储引擎层 产生
  • 二进制日志 是在 MySQL数据库的上层 产生的,并且二进制日志不仅仅针对于InnoDB存储引擎,MySQL数据库中的任何存储引擎对于数据库的更改都会产生二进制日志
  • 两种日志记录的内容形式不同,MySQL数据库上层的二进制日志 是一种 逻辑日志,其记录的是 对应的SQL语句;InnoDB存储引擎层面的重做日志 是 物理格式日志,其记录的是 对于 每个页的修改
  • 两种日志记录写入磁盘的时间点不同,二进制日志 只在 事务提交完成后 进行一次写入;InnoDB存储引擎的重做日志 在事务进行中 不断地 被写入,这表现为 日志 并不是 随事务提交的顺序 进行写入的

log block-重做日志块

在InnoDB存储引擎中,重做日志 都是以 512字节进行存储的

这意味着 重做日志缓存、重做日志文件 都是 以块(block)的方式 进行保存的,称之为重做日志块(redo log block),每块的大小为512字节

若一个页中产生的重做日志数量大于512字节,那么需要分割为多个重做日志块进行存储

由于 重做日志块的大小 和 磁盘扇区 大小一样,都是512字节,因此 重做日志的写入 可以保证 原子性,不需要doublewrite技术

重做日志块 除了 日志本身之外,还由 日志块头(log block header) 及 **日志块尾(log block tailer)**两部分组成

重做日志头一共占用12字节

重做日志尾占用8字节

故 每个重做日志块 实际可以存储的大小为492字节(512-12-8)

MySQL事务(二)-事务的实现概述redo-重做日志undo-回滚日志purgegroup commit(待完善)

上图显示了**重做日志缓存(redo log buffer)**的结构

可以发现重做日志缓存由每个为512字节大小的日志块所组成

日志块由三部分组成,依次为日志块头(log block header)、日志内容(log body)、日志块尾(log block tailer)

log block header由4部分组成,见下图

MySQL事务(二)-事务的实现概述redo-重做日志undo-回滚日志purgegroup commit(待完善)
  • log buffer是由log block组成,在内部log buffer就好似一个数组,因此

    LOG_BLOCK_HDR_NO

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

    占用2字节,表示log block所占用的大小,当log block被写满时,该值为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中的情况如下所示:

MySQL事务(二)-事务的实现概述redo-重做日志undo-回滚日志purgegroup commit(待完善)

从上图可以看到,由于事务T1的重做日志占用792字节,因此需要占用两个log block,左边整个492 + 右边的一部分(270)

左侧的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中被初始化
MySQL事务(二)-事务的实现概述redo-重做日志undo-回滚日志purgegroup commit(待完善)

log group-重做日志组

log group为重做日志组,其中 有多个 重做日志文件

虽然源码中已支持log group的镜像功能,但是在ha_innobase.cc文件中禁止了该功能

因此InnoDB存储引擎实际只有一个log group

log group是一个逻辑上的概念,并没有一个 实际存储的物理文件 来表示 log group信息

log group 由 多个 重做日志文件 组成,每个log group中的日志文件大小是相同的

在InnoDB 1.2版本之前,重做日志文件的总大小要小于4GB(不能等于4GB)

从InnoDB 1.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 log file被写满时,会接着写入下一个redo log file,其使用方式为round-robin

虽然log block总是在redo log file的最后部分进行写入,可能会以为对redo log file的写入都是顺序的

其实不然,因为redo log file除了保存log buffer刷新到磁盘的log block,还保存了一些其他的信息,这些信息一共占用2KB大小,即每个redo log file的前2KB的部分不保存log block的信息

对于log group中的第一个redo log file,其前2KB的部分保存4个512字节大小的块,如下图:

MySQL事务(二)-事务的实现概述redo-重做日志undo-回滚日志purgegroup commit(待完善)

需要特别注意的是,上述信息 仅在 每个log group 的 第一个redo log file中 进行存储,log group中的 其余redo log file 仅保留 这些空间,但不保存上述信息

正因为保存了这些信息,就意味着对redo log file的写入并不是完全顺序的

因为其除了log block的写入操作,还需要更新前2KB部分的信息,这些信息对于InnoDB存储引擎的恢复操作来说非常关键和重要

故log group与redo log file之间的关系如下所示:

MySQL事务(二)-事务的实现概述redo-重做日志undo-回滚日志purgegroup commit(待完善)

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

重做日志格式

不同的数据库操作会有对应的重做日志格式

由于InnoDB存储引擎的存储管理是基于页的,故其重做日志格式也是基于页的

虽然有着不同的重做日志格式,但是它们有着通用的头部格式

MySQL事务(二)-事务的实现概述redo-重做日志undo-回滚日志purgegroup commit(待完善)

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

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

之后redo log body的部分,根据重做日志类型的不同,会有不同的存储内容

MySQL事务(二)-事务的实现概述redo-重做日志undo-回滚日志purgegroup commit(待完善)

到InnoDB1.2版本时,一共有51种重做日志类型

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,并且该事务已经提交,那么数据库需要进行恢复操作,将重做日志应用到P1页中

同样的,对于重做日志中LSN 小于 P1页的LSN,不需要进行重做,因为P1页中的LSN表示页已经被刷新到该位置

可以通过命令

SHOW ENGINE INNODB STATUS

查看LSN的情况

---
LOG
---
Log sequence number          359165668
Log buffer assigned up to    359165668
Log buffer completed up to   359165668
Log written up to            359165668
Log flushed up to            359165200
Added dirty pages up to      359165668
Pages flushed up to          359164533
Last checkpoint at           359164533
1257893 log i/o's done, 0.88 log i/o's/second
           

Log sequence number 表示 当前的LSN

Log flushed up to 表示 刷新到 重做日志文件 的 LSN

Last checkpoint at 表示 刷新到 磁盘 的 LSN

恢复

InnoDB存储引擎在启动时不管上次数据库运行时是否正常关闭,都会尝试进行恢复操作

因为重做日志记录的是物理日志,因此恢复的速度比逻辑日志,如二进制日志,要快很多

与此同时,InnoDB存储引擎自身也对恢复进行了一定程度的优化,如顺序读取及并行应用重做日志,这样可以进一步地提高数据库恢复的速度

由于checkpoint表示已经刷新到磁盘页上的LSN,因此在恢复过程中仅需恢复checkpoint开始的日志部分

MySQL事务(二)-事务的实现概述redo-重做日志undo-回滚日志purgegroup commit(待完善)

当数据库在checkpoint的LSN为10 000时发生宕机,恢复操作仅恢复LSN 10 000~13 000范围内的日志

InnoDB存储引擎的重做日志是物理日志,因此其恢复速度较之二进制日志恢复快得多

undo-回滚日志

基本概念

重做日志记录了事务的行为,可以很好地通过其对页进行“重做”操作

但是事务有时还需要进行回滚操作,这时就需要undo(回滚)

因此在对数据库进行修改时,InnoDB存储引擎不但会产生redo,还会产生一定量的undo

这样如果执行的事务或语句由于某种原因失败了,又或者用一条ROLLBACK语句请求回滚,就可以利用这些undo信息将数据回滚到修改之前的样子

redo存放在重做日志文件中,与redo不同,undo 存放在 数据库内部的一个特殊段(segment)中,这个段称为undo段(undo segment)

undo段 位于 共享表空间内

undo是逻辑日志,因此 只是将 数据库 逻辑地 恢复到原来的样子

所有修改都被逻辑地取消了,但是 数据结构 和 页本身 在回滚之后 可能大不相同

这是因为 在多用户并发系统中,可能会有数十、数百甚至数千个并发事务。数据库的主要任务就是协调对数据记录的并发访问

比如,一个事务在修改当前一个页中某几条记录,同时还有别的事务在对同一个页中另几条记录进行修改。因此,不能将一个页 回滚到 事务开始的样子,因为这样会影响其他事务正在进行的工作

当InnoDB存储引擎回滚时,它实际上做的是与先前相反的工作

对于每个INSERT,InnoDB存储引擎会完成一个DELETE

对于每个DELETE,InnoDB存储引擎会执行一个INSERT

对于每个UPDATE,InnoDB存储引擎会执行一个相反的UPDATE,将修改前的行放回去

除了回滚操作,undo的另一个作用是MVCC,即 在InnoDB存储引擎中 MVCC的实现 是通过 undo来完成

当读取一行记录时,若 该记录 已经被 其他事务占用,当前事务 可以通过 undo 读取之前的行版本信息,以此实现非锁定读取

最后也是最为重要的一点是,undo log会产生redo log,也就是undo log的产生会伴随着redo log的产生,这是因为undo log也需要持久性的保护

undo存储管理

InnoDB存储引擎对undo的管理同样采用段的方式

但是这个段和之前介绍的段有所不同

首先InnoDB存储引擎有rollback segment,每个回滚段中记录了1024个undo log segment,而在每个undo log segment段中进行undo页的申请

共享表空间偏移量为5的页(0,5)记录了 所有rollback segment header所在的页,这个页的类型为

FIL_PAGE_TYPE_SYS

在InnoDB1.1版本之前(不包括1.1版本),只有一个rollback segment,因此支持同时在线的事务限制为1024。虽然对绝大多数的应用来说都已经够用,但不管怎么说这是一个瓶颈

从1.1版本开始InnoDB支持最大128个rollback segment,故其支持同时在线的事务限制提高到了128*1024

虽然InnoDB1.1版本支持了128个rollback segment,但是这些rollback segment都存储于共享表空间中

从InnoDB1.2版本开始,可通过参数对rollback segment做进一步的设置。这些参数包括:

  • innodb_undo_directory,用于设置rollback segment文件所在的路径,这意味着rollback segment可以存放在共享表空间以外的位置,即可以设置为独立表空间。该参数的默认值为“.”,表示当前InnoDB存储引擎的目录
  • innodb_undo_logs,用来设置rollback segment的个数,默认值为128。在InnoDB1.2版本中,该参数用来替换之前版本的参数innodb_rollback_segments
  • innodb_undo_tablespaces,用来设置构成rollback segment文件的数量,这样rollback segment可以较为平均地分布在多个文件中。设置该参数后,会在路径

    innodb_undo_directory

    看到undo为前缀的文件,该文件就代表rollback segment文件
    MySQL事务(二)-事务的实现概述redo-重做日志undo-回滚日志purgegroup commit(待完善)

需要特别注意的是,事务在undo log segment 分配页 并 写入undo log 的 这个过程 同样需要 写入重做日志

当事务提交时,InnoDB存储引擎会做以下两件事情:

  • 将undo log放入列表中,以供之后的purge操作
  • 判断undo log所在的页是否可以重用,若可以分配给下个事务使用

事务提交后 并不能 马上删除 undo log及undo log所在的页

这是因为 可能 还有其他事务 需要通过 undo log 来得到 行记录之前的版本

故 事务提交时 将undo log放入一个链表中,是否可以最终删除undo log及undo log所在页 由purge线程 来判断

此外,若为每一个事务 分配一个 单独的undo页 会非常浪费存储空间,特别是对于OLTP的应用类型

因为在事务提交时,可能并不能马上释放页

假设某应用的删除和更新操作的TPS(transaction per second)为1000,为每个事务分配一个undo页,那么一分钟就需要1000*60个页,大约需要的存储空间为1GB

若每秒的purge页的数量为20,这样的设计对磁盘空间有着相当高的要求

因此,在InnoDB存储引擎的设计中对undo页可以进行重用

具体来说,当事务提交时,首先将undo log放入链表中,然后判断undo页的使用空间是否小于3/4,若是则表示该undo页可以被重用,之后新的undo log记录在当前undo log的后面

由于存放undo log的列表是以记录进行组织的,而undo页可能存放着不同事务的undo log,因此purge操作需要涉及磁盘的离散读取操作,是一个比较缓慢的过程

可以通过命令

SHOW ENGINE INNODB STATUS

来查看链表中undo log的数量

------------
TRANSACTIONS
------------
Trx id counter 706000
Purge done for trx's n:o < 705999 undo n:o < 0 state: running but idle
History list length 14
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 422019934984224, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 422019934988584, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 422019934987712, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 422019934986840, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 422019934982480, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 422019934985968, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 422019934983352, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 422019934981608, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 422019934980736, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 422019934979864, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 422019934998176, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 422019934978992, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
           

History list length

就代表了undo log的数量,purge操作会减少该值

然而由于undo log所在的页可以被重用,因此即使操作发生,History list length的值也可以不为

undo log格式

在InnoDB存储引擎中,undo log分为:

  • insert undo log
  • update undo log

insert undo log

指在insert操作中产生的undo log

因为insert操作的记录,只对事务本身可见,对其他事务不可见(这是事务隔离性的要求),故该undo log可以在事务提交后直接删除。不需要进行purge操作

insert undo log结构如下:

MySQL事务(二)-事务的实现概述redo-重做日志undo-回滚日志purgegroup commit(待完善)
  • 上图中*表示对存储的字段进行了压缩
  • insert undo log开始的前两个字节next 记录的是 下一个undo log的位置,通过该next的字节 可以知道 一个undo log所占的空间字节数
  • 类似地,尾部的两个字节记录的是undo log的开始位置
  • type_cmpl占用一个字节,记录的是undo的类型,对于insert undo log,该值总是为11
  • undo_no记录事务的ID,table_id记录undo log所对应的表对象。这两个值都是在压缩后保存的
  • 接着的部分记录了所有主键的列和值。在进行rollback操作时,根据这些值可以定位到具体的记录,然后进行删除即可

update undo log

记录的是对delete和update操作产生的undo log

该undo log可能需要提供MVCC机制,因此不能在事务提交时就进行删除

提交时放入undo log链表,等待purge线程进行最后的删除

update undo log结构如下:

MySQL事务(二)-事务的实现概述redo-重做日志undo-回滚日志purgegroup commit(待完善)

update undo log相对于之前介绍的insert undo log,记录的内容更多,所需占用的空间也更大

  • next、start、undo_no、table_id与之前介绍的insert undo log部分相同
  • 这里的type_cmpl,由于update undo log本身还有分类,故其可能的值如下:
    1. 12 TRX_UNDO_UPD_EXIST_REC更新non-delete-mark的记录
    2. 13 TRX_UNDO_UPD_DEL_REC将delete的记录标记为not delete
    3. 14 TRX_UNDO_DEL_MARK_REC将记录标记为delete
  • 接着的部分记录update_vector信息,update_vector表示update操作导致发生改变的列。每个修改的列信息都要记录的undo log中。对于不同的undo log类型,可能还需要记录对索引列所做的修改

purge

delete和update操作可能并不直接删除原有的数据

例如,

DELETE FROM t WHERE a=1;

表t上列a有聚集索引,列b上有辅助索引

对于上述的delete操作,通过undo log的介绍已经知道 仅是将 主键列等于1的记录delete flag设置为1,记录并没有被删除,即记录还是存在于B+树中

其次,对辅助索引上a等于1,b等于1的记录同样没有做任何处理,甚至没有产生undo log

而真正删除这行记录的操作其实被“延时”了,最终在purge操作中完成

purge用于最终完成delete和update操作

这样设计是因为InnoDB存储引擎支持MVCC,所以记录不能在事务提交时立即进行处理

这时其他事物可能正在引用这行,故InnoDB存储引擎需要保存记录之前的版本

而是否可以删除该条记录通过purge来进行判断

若该行记录已不被任何其他事务引用,那么就可以进行真正的delete操作

可见,purge操作是清理之前的delete和update操作,将上述操作“最终”完成

而实际执行的操作为delete操作,清理之前行记录的版本

上面提到过,InnoDB存储引擎的undo log设计是这样的:一个页上 允许 多个事务的undo log 存在

虽然这不代表事务在全局过程中提交的顺序,但是后面的事务产生的undo log总在最后

此外,InnoDB存储引擎还有一个history列表,它根据事务提交的顺序,将undo log进行链接

MySQL事务(二)-事务的实现概述redo-重做日志undo-回滚日志purgegroup commit(待完善)

在上图的例子中,history list 表示 按照事务提交的顺序 将undo log进行组织

在InnoDB存储引擎的设计中,先提交的事务总在尾端

undo page(页)存放了undo log,由于可以重用,因此一个undo page中可能存放了多个不同事务的undo log

trx5的灰色阴影表示该undo log还被其他事务引用

在执行purge的过程中,InnoDB存储引擎首先从history list中找到第一个需要被清理的记录,这里为trx1,清理之后InnoDB存储引擎会在 trx1的undo log所在的页中 继续寻找 是否存在 可以被清理的记录,这里会找到事务trx3,接着找到trx5,但是发现trx5被其他事务所引用而不能清理,故去再次去history list中查找,发现这时最尾端的记录为trx2,接着找到trx2所在的页,然后依次再把事务trx6、trx4的记录进行清理。由于undo page2中所有的页都被清理了,因此该undo page可以被重用

InnoDB存储引擎这种先从history list中找undo log,然后再从undo page中找undo log的设计模式 是为了 避免大量的随机读取操作,从而提高purge的效率

全局动态参数

innodb_purge_batch_size

用来设置 每次purge操作需要清理的undo page数量

MySQL事务(二)-事务的实现概述redo-重做日志undo-回滚日志purgegroup commit(待完善)

在InnoDB1.2之前,该参数的默认值为20

从1.2版本开始,该参数的默认值为300

通常来说,该参数设置得越大,每次回收的undo page也就越多,这样可供重用的undo page就越多,减少了磁盘存储空间与分配的开销

不过,若该参数设置得太大,则每次需要purge处理更多的undo page,从而导致CPU和磁盘IO过于集中于对undo log的处理,使性能下降

因此对该参数的调整需要由有经验的DBA来操作,并且需要长期观察数据库的运行的状态

当InnoDB存储引擎的压力非常大时,并不能高效地进行purge操作

那么history list的长度会变得越来越长。全局动态参数

innodb_max_purge_lag

用来控制history list的长度,若长度大于该参数时,其会“延缓”DML的操作

MySQL事务(二)-事务的实现概述redo-重做日志undo-回滚日志purgegroup commit(待完善)

该参数默认值为0,表示不对history list做任何限制

当大于0时,就会延缓DML的操作,其延缓的算法为:

delay的单位是毫秒

此外,需要特别注意的是,delay的对象 是 行,而不是一个DML操作

例如当一个update操作需要更新5行数据时,每行数据的操作都会被delay,故总的延时时间为5*delay

而delay的统计会在每一次purge操作完成后,重新进行计算

InnoDB1.2版本引入了新的全局动态参数

innodb_max_purge_lag_delay

,其用来控制delay的最大毫秒数

也就是当上述计算得到的delay值大于该参数时,将delay设置为

innodb_max_purge_lag_delay

,避免由于purge操作缓慢导致其他SQL线程出现无限制的等待

MySQL事务(二)-事务的实现概述redo-重做日志undo-回滚日志purgegroup commit(待完善)

group commit(待完善)

若事务为非只读事务,则每次事务提交时需要进行一次fsync操作,以此保证重做日志都已经写入磁盘

当数据库发生宕机时,可以通过重做日志进行恢复

虽然固态硬盘的出现提高了磁盘的性能,然而磁盘的fsync性能是有限的

为了提高磁盘fsync的效率,当前数据库都提供了group commit的功能,即一次fsync 可以刷新 确保 多个事务日志 被写入文件

对于InnoDB存储引擎来说,事务提交时会进行两个阶段的操作:

  1. 修改 内存中 事务对应的信息,并且将 日志 写入 重做日志缓冲
  2. 调用fsync 将确保 日志 都从 重做日志缓冲 写入 磁盘

步骤2相对步骤1是一个较慢的过程,这是因为存储引擎需要与磁盘打交道。但当有事务进行这个过程时,其他事务可以进行步骤1的操作,正在提交的事物完成提交操作后,再次进行步骤2时,可以将多个事务的重做日志通过一次fsync刷新到磁盘,这样就大大地减少了磁盘的压力,从而提高了数据库的整体性能

对于写入或更新较为频繁的操作,group commit的效果尤为明显

然而在InnoDB1.2版本之前,在开启二进制日志后,InnoDB存储引擎的group commit功能会失效,从而导致性能的下降。并且在线环境多使用replication环境,因此二进制日志的选项基本都为开启状态,因此这个问题尤为显著

导致这个问题的原因是在开启二进制日志后,为了保证 存储引擎层中的事务 和 二进制日志的一致性,二者之间使用了两阶段事务,其步骤如下:

  1. 当事务提交时InnoDB存储引擎进行prepare操作
  2. MySQL数据库上层写入二进制日志
  3. InnoDB存储引擎层 将日志 写入 重做日志文件

    3.1 修改 内存中 事务 对应的信息,并且将 日志 写入 重做日志缓冲

    3.2 调用fsync 将 确保 日志 都从 重做日志缓冲 写入 磁盘

一旦步骤2中的操作完成,就确保了事务的提交,即使在执行步骤3时数据库发生了宕机

此外需要注意的是,每个步骤都需要进行一次fsync操作才能保证上下两层数据的一致性

步骤2的fsync由参数

sync_binlog

控制

步骤3的fsync由参数

innodb_flush_log_at_trx_commit

控制

MySQL事务(二)-事务的实现概述redo-重做日志undo-回滚日志purgegroup commit(待完善)

为了保证MySQL数据库上层二进制日志的写入顺序 和 InnoDB层的事务提交顺序一致,MySQL数据库内部使用了

prepare_commit_mutex

这个锁。但是在启用这个锁之后,步骤3中的步骤3.1 不可以 在其他事务 执行 步骤3.2时 进行,从而导致了group commit失效

为什么需要保证MySQL数据库上层二进制日志的写入顺序和InnoDB层的事务提交顺序一致呢?

这时因为备份及恢复的需要,例如通过工具xtrabackup或者ibbackup进行备份,并用来建立replication,如下所示:

MySQL事务(二)-事务的实现概述redo-重做日志undo-回滚日志purgegroup commit(待完善)

可以看到若通过在线备份进行数据库恢复来重新建立replication,事务T1的数据会产生丢失

因为在InnoDB存储引擎层会检测事务T3在上下两层都完成了提交,不需要再进行恢复

因此通过锁

prepare_commit_mutex

以串行的方式来保证顺序性,然而这会使group commit无法生效,如下所示:

MySQL事务(二)-事务的实现概述redo-重做日志undo-回滚日志purgegroup commit(待完善)