MySQL事务(1):事务实现

事务实现

  • 事务分类
    • 1)扁平事务
    • 2)带有保存点的扁平事务
    • 3)链事务
    • 4)嵌套事务
    • 5)分布式事务
  • redo log
    • 1) redo log构成
      • redo log buffer和redo log file
      • log block
      • log group
      • redo log格式
      • LSN
    • 2) redo log和bin log
    • 3)redo log与bin log差异
  • undo log
    • 1)undo log概念
    • 2)undo log格式
    • 3)purge
  • group commit

事务是数据库区别于文件系统的重要特性,由一条或一组SQL语句组成。事务将数据库从一种一致状态转换为另一种一致状态。事务应当完全符合ACID特性,

  • atomicity,原子性
    事务是不可分割的工作单位,事务中的操作语句要么全部完成,要么全不完成。
    如果事务中的操作都是只读的,保持原子性是十分简单的,因为只读操作不会对数据做改变。
    如果事务中存在改变数据的操作,如插入、更新和删除,如果操作过程中出现失败,则要对已更改的部分进行恢复。

  • consistency,一致性
    事务开始和解书,数据库的完整性约束没有被破坏,如对唯一索引的操作事务成功提交或失败回滚后,不会对唯一索引的唯一性约束进行破坏。

  • isolation,隔离性
    通过锁实现,目的是使不同事务间操作数据时互不影响。

  • durability,持久性
    事务提交后对数据库的影响是永久的,即使宕机也能够被恢复。

事务的原子性和持久性由redo log保证,一致性由undo log保证。这两个log不是互逆过程,redo log恢复的是提交完成的事务对数据页进行的修改,而undo log则是回滚到之前的某个版本或实现MVCC。

事务分类

1)扁平事务

最简单的一种事务,实际生产中使用最为频繁,所有的操作都处于同一层次。由begin开始,至commitrollback结束。扁平事务的执行有三种不同的结果,
MySQL事务(1):事务实现_第1张图片
扁平事务的主要限制是不能提交或回滚事务的某一部分,或分几个步骤进行提交。当运行出错时,每次都要回滚到最开始的位置。

2)带有保存点的扁平事务

支持扁平事务的同时,允许事务执行过程中回滚到同一个事务中较早的一个状态。避免了回滚到事务开头造成过大的开销,而是通过保存点通知系统记住事务当前的状态。扁平事务在事务开始时会隐式创建一个保存点,所以当事务失败时直接回滚至开头部分。
使用savepoint函数创建保存点,如下图,
MySQL事务(1):事务实现_第2张图片
上图是在事务中使用保存点,灰色部分是由rollback导致部分回滚。
保存点在事务内部是递增的,rollback回到保存点2的状态后,理论上下一个保存点的编号应该是3。但是实际情况下,新的保存点的编号为5,意味着rollback不会影响保存点的计数

3)链事务

保存点模式的变种,带有保存点的扁平事务执行过程中如果遇到系统崩溃,所有的保存点都将消失,因为保存点是易失的。链事务的思想是,在提交一个事务时,释放不需要的数据对象。提交事务操作和下一个实务操作合并为一个原子操作,
MySQL事务(1):事务实现_第3张图片
链事务与带保存点的扁平事务不同的是,带有保存点的扁平事务可以回滚到任意正确的保存点。而链事务中回滚仅限于当前事务,即只能恢复到最近一个保存点。

4)嵌套事务

是一个层次结构,由顶层事务控制着各个层次的事务,顶层事务之下嵌套的事务被称为子事务,
MySQL事务(1):事务实现_第4张图片

  1. 嵌套事务是由若干事务组成的一棵树,子树既可以是嵌套事务,也可以是扁平事务
  2. 叶节点的事务是扁平事务,每个叶节点到根节点的层数可以不同
  3. 子事务的提交不能立刻生效,必须等待顶层事务提交后才能提交
  4. 一个事务回滚必定引起其所有子事务回滚

5)分布式事务

分为外部分布式事务和内部分布式事务,后面笔记中会讲解。

redo log

1) redo log构成

redo log buffer和redo log file

重做日志实现实物的持久性。由两部分组成

  • redo log buffer,重做日志缓冲,是易失的
  • redo log file,重做日志文件,是持久的

InnoDB引擎通过Force log at commit机制实现事务的持久性,当事务提交时,必须先将事务的所有redo log写入到redo log file中进行持久化。redo log 基本上都是顺序写的。

每次将redo log buffer中的内容写入 redo log file的后,InnoDB引擎都会执行一次fsync 操作。fsync的效率取决于磁盘性能,InnoDB支持用户设置innodb_flush_log_at_trx_commit来控制 redo log buffer中的内容刷新到磁盘的策略,该参数可取0、1和2,这三个值,

说明
0 事务不进行写入文件操作,这个操作只在主线程中进行,主线程中每秒会调用一次fsync操作,即 log buffer 的刷写操作和事务提交操作没有关系。在这种情况下,MySQL性能最好,但如果 mysqld 进程崩溃,通常会导致最后 1s 的日志丢失
1 事务提交时必须调用一次fsync操作。这是最安全的配置,但由于每次事务都需要进行磁盘I/O,所以也最慢。
2 事务提交时仅将 redo log写入redo log buffer,不进行fsync操作。日志文件会每秒刷写一次到磁盘。这时如果 mysqld 进程崩溃,由于日志已经写入到系统缓存,所以并不会丢失数据;在操作系统崩溃的情况下,通常会导致最后 1s 的日志丢失。

2是对0和1两种方式的折中,将参数设置为0或2可以提高提交的性能,但是使事务丧失了ACID的特性(持久性可能不会被满足)。

log block

InnoDB中,redo log以512字节的块进行存储。

每个redo log block分为log block headlog block bodylog block tail,如下,
MySQL事务(1):事务实现_第5张图片

log group

log group是一个逻辑上的概念,并没有实际的物理文件表示 log group。

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

redo log file中存储的就是之前在log buffer中存储的log block。
MySQL事务(1):事务实现_第6张图片
上图是有两个 redo log file的 log group,每个log file开头有2KB的信息,其余为 log block存储的日志内容。

redo log格式

InnoDB的存储管理是页级别的,其 redo log的格式也是页级别的。不同的操作有不同的redo log格式,但是都有通用的头部,
在这里插入图片描述

  • redo log type:重做日志类型
  • space:表空间的ID
  • page on:页偏移量
  • redo log body:根据 redo log类型的不同有不同的存储内容

LSN

log sequence number,日志序列号,表示写入到 redo log中的字节总量。

该值用于判断页是否需要恢复操作。具体的原理是,redo log file和页中各记录一个LSN。

  • 若页LSN < redo LSN,说明需要回滚
  • 若页LSN ≥ redo LSN,不需要进行恢复

2) redo log和bin log

在同一个事务中修改数据操作时,将修改结果更新到内存后,会在redo log添加一行记录记录“需要在哪个数据页上做什么修改”,并将该记录状态置为prepare

等到commit提交事务后,会将此次事务中在redo log添加的记录的状态都置为commit状态。

之后将修改落盘时,会将redo log中状态为commit的记录的修改都写入磁盘。整个过程如下,
MySQL事务(1):事务实现_第7张图片
图片摘自简书博客

首先明确一个问题,有了redo log,为什么还需要binlog呢?

  1. redo log的大小是固定的,日志上的记录修改落盘后,日志会被覆盖掉,无法用于数据回滚/数据恢复等操作。redo log的写入如下图,
    MySQL事务(1):事务实现_第8张图片
    write pos表示日志当前记录的位置,当ib_logfile_4写满后,会从ib_logfile_1从头开始记录;
    check point表示将日志记录的修改写进磁盘,完成数据落盘,数据落盘后checkpoint会将日志上的相关记录擦除掉
    write pos->checkpoint之间的部分是redo log空着的部分,用于记录新的记录。当writepos追上checkpoint时,得先停下记录,先推动checkpoint向前移动,空出位置记录新的日志。

基于上述两个问题,引入bin log,

  • bin log是server层实现的,意味着所有引擎都可以使用bin log日志
  • bin log通过追加的方式写入的,可通过配置参数max_binlog_size设置每个bin log文件的大小,当文件大小大于给定值后,日志会发生滚动,之后的日志记录到新的文件上。
  • binlog有两种记录模式,statement格式的话是记sql语句, row格式会记录行的内容,记两条,更新前和更新后都有。

MySQL事务(1):事务实现_第9张图片
bin log和 redo log要么都成功写入,要么一起失败。如果写入bin log时,事务会回滚。如果在将redo log中的状态改为commit的过程失败,也会回滚,且bin log中也会删除该事务的记录。

3)redo log与bin log差异

  1. redo log是 InnoDB引擎产生的,而bin log是MySQL服务器产生的,二进制日志不仅仅针对于 InnoDB引擎,MySQL中任何一种存储引擎都会产生 bin log
  2. 二进制日志是一种逻辑日志,记录的是对应的SQL语句;而redo log是InnoDB层面的物理格式日志,记录对每个页的修改
  3. 两种日志写入磁盘的时间点不同,bin log只在事务提交时一次性写入;而 redo log则是在事务进行中不断被写入

undo log

1)undo log概念

redo log记录了事务的行为,可以通过redo log对页进行重做操作。事务有时需要进行回滚,此时就需要undo log。undo log的功能有两个,

  1. 执行的事务或语句由于某种原因失败了,或者用户显示地调用了rollback命令请求回滚。此时可以使用undo信息将数据库修改回之前的样子
  2. 实现MVCC(多版本并发控制),当读取(一致性非锁定读)一条记录时,若该记录已经被其他事务占用,当前事务可以通过undo读取之前的行版本信息

undo log是采用段(segment)的方式来记录的,每个undo操作在记录的时候占用一个undo log segment,该段位于共享表空间内。innodb 存储引擎对 undo 的管理采用段的方式。rollback segment称为回滚段,每个回滚段中有1024个 undo log segment

undo是逻辑日志,不是物理日志。使用undo日志恢复的过程实际上就是将修改以逻辑的形式恢复,

修改方式 逻辑回滚方式
insert delete
delete insert
update update

undo回滚后,数据结构和页在变化后可能差异很大。因为在多并发的环境中,可能会有成百上千个并发事务。一个事务对数据进行修改时,其他事务也会对页中的记录进行修改,所以不能将页直接回滚到事务开始时的样子,因为会影响其他事务的工作。所以undo不会对页进行过多修改,主要是对数据结构上的修改。

2)undo log格式

InnoDB中,undo log有两种,

  • insert undo log
    insert undo log是在insert操作中产生的,只对事务本身可见,对其他事务不可见。该undo log在提交后直接删除。
  • update undo log
    update undo log是在delete和update操作时产生的,因为对MVCC机制的实现起到帮助,所以不能在提交时被删除。
    delete和update操作不会立刻删除该记录,而是将delete对象打上delete flag,等待purge线程最后删除。

3)purge

delete和update操作并不直接删除原有数据,如下面的SQL语句,

delete from t where a=1;

假设在表t中,a字段是主键,b字段上存在普通索引。

  1. undo log将主键列a=1的记录delete flag设置为1,但此时记录并没有被删除,依旧存在于B+索引树中
  2. 对于辅助索引上a=1对应的记录b,同样没有被删除,甚至不产生undo log
  3. 当记录不再被其他事物引用时,才进行真正的删除操作

group commit

如果事务不是只读事务,即涉及到了数据的修改,默认情况下会在 commit 的时候调用 fsync() 将日志刷到磁盘,保证事务的持久性。

但是一次刷一个事务的日志性能较低,特别是事务集中在某一时刻时事务量非常大的时候。innodb提供了 group commit 功能,可以将多个事务的事务日志通过一次fsync()刷到磁盘中。

因为事务在提交的时候不仅会记录事务日志,还会记录二进制日志。二进制日志是MySQL的上层日志,先于存储引擎的事务日志被写入。

  1. 当事务提交(即发出commit指令)后,MySQL接收到该信号进入commit prepare阶段;
  2. 进入prepare阶段后,立即写内存中的二进制日志,写完内存中的二进制日志后就相当于确定了commit操作;
  3. 然后开始写内存中的事务日志;最后将二进制日志和事务日志刷盘,它们如何刷盘,分别由变量 sync_binloginnodb_flush_log_at_trx_commit控制。

为保证二进制日志和事务日志的一致性,在提交后的prepare阶段会启用一个prepare_commit_mutex锁保证顺序性和一致性。但这样会导致开启二进制日志后group commmit失效,特别是在主从复制结构中,几乎都会开启二进制日志。MySQL5.6 中进行了改进。提交事务时,在存储引擎层的上一层结构中会将事务按序放入一个队列,队列中的第一个事务称为 leader,其他事务称为 follower,leader 控制着 follower 的行为。虽然顺序还是一样先刷二进制,再刷事务日志,但是机制完全改变了:删除了原来的prepare_commit_mutex 行为,也能保证即使开启了二进制日志,group commit 也是有效的。
MySQL事务(1):事务实现_第10张图片

  • flush阶段:向内存中写入每个事务的二进制日志。
  • sync阶段:将内存中的二进制日志刷盘。若队列中有多个事务,那么仅一次 fsync 操作就完成了二进制日志的刷盘操作。这在 MySQL5.6 中称为BLGC(binary log group commit)。
  • commit阶段:leader根据顺序调用存储引擎层事务的提交,由于innodb本就支持group commit,所以解决了因为锁 prepare_commit_mutex 而导致的group commit失效问题。

在flush阶段写入二进制日志到内存中,不是写完就进入sync阶段的,而是要等待一定的时间,多积累几个事务的 binlog 一起进入 sync 阶段。等待时间由变量binlog_max_flush_queue_time 决定,默认值为 0,表示不等待直接进入 sync。设置该变量为一个大于0的值的好处是group中的事务多了,性能会好一些,但是这样会导致事务的响应时间变慢,所以建议不要修改该变量的值,除非事务量非常多并且不断的在写入和更新。

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

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

你可能感兴趣的:(MySQL)