MySQL事务(1):事务实现

本文深入探讨数据库事务的ACID特性,详细解析扁平事务、带有保存点的扁平事务、链事务、嵌套事务及分布式事务的概念与运作机制。同时,阐述redolog与binlog的作用与差异,以及undolog在事务回滚与MVCC实现中的关键作用。


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

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

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

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

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

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

事务分类

1)扁平事务

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

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

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

3)链事务

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

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,如下,
在这里插入图片描述

log group

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

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

redo log file中存储的就是之前在log buffer中存储的log block。
在这里插入图片描述
上图是有两个 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的记录的修改都写入磁盘。整个过程如下,
在这里插入图片描述
图片摘自简书博客

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

  1. redo log的大小是固定的,日志上的记录修改落盘后,日志会被覆盖掉,无法用于数据回滚/数据恢复等操作。redo log的写入如下图,
    在这里插入图片描述
    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格式会记录行的内容,记两条,更新前和更新后都有。

在这里插入图片描述
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日志恢复的过程实际上就是将修改以逻辑的形式恢复,

修改方式逻辑回滚方式
insertdelete
deleteinsert
updateupdate

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 也是有效的。
在这里插入图片描述

  • 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 的效果越明显。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值