Mysql事务

简单来说,数据库事务可以保证多个对数据库的操作构成一个逻辑上的整体,这些数据库操作遵循着要么全部执行成功,要么全部不执行 。

关系型数据库(例如:MySQLSQL ServerOracle 等)事务都有ACID特性:

  • 原子性(Atomicity):事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用
  • 一致性(Consistency):执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的
  • 隔离性(Isolation):并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的
  • 持久性(Durability):一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响

👉 只有保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。也就是说 A、I、D 是手段,C 是目的!

而对于这四大特性,实际上分为两个部分。 其中的原子性、一致性、持久化,实际上是由InnoDB中的两份日志来保证的,即redo logundo log。 而隔离性是通过数据库的锁,加上MVCC来保证的。

事务的隔离级别

  • READ-UNCOMMITTED(读取未提交) :最低的隔离级别,允许读取尚未提交的数据,可能会导致脏读、幻读或不可重复读。
  • READ-COMMITTED(读取已提交) :允许读取事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
  • REPEATABLE-READ(可重复读) :对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
  • SERIALIZABLE(可串行化) :最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
隔离级别脏读不可重复读幻读
读未提交可能可能可能
读已提交不可能可能可能
可重复读不可能不可能可能
串行化不可能不可能不可能

并发事务带来了哪些问题

在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对同一数据进行操作)。并发虽然是必须的,但可能会导致以下的问题。

  • 脏读:一个事务读取数据并且对数据进行了修改,这个修改对其他事务来说是可见的,即使当前事务没有提交。这时另外一个事务读取了这个还未提交的数据,但第一个事务突然回滚,导致数据并没有被提交到数据库,那第二个事务读取到的就是脏数据,这就是脏读。
  • 丢失修改:在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。
  • 不可重复读:指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
  • 幻读:幻读与不可重复读类似。它发生在一个事务读取了几行数据,接着另一个并发事务插入了一些数据时。在随后的查询中,第一个事务就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。

不可重复读和幻读有什么区别?

  • 不可重复读的重点是内容修改或者记录减少比如多次读取一条记录发现其中某些记录的值被修改;
  • 幻读的重点在于记录新增比如多次执行同一条查询语句时,发现查到的记录增加了。

幻读其实可以看作是不可重复读的一种特殊情况,单独把幻读区分出来的原因主要是解决幻读和不可重复读的方案不一样。

举个例子:执行 deleteupdate 操作的时候,可以直接对记录加锁,保证事务安全。而执行 insert 操作的时候,由于记录锁(Record Lock)只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁(Gap Lock)。也就是说执行 insert 操作的时候需要依赖Next-Key Lock(Record Lock+Gap Lock)进行加锁来保证不出现幻读。

并发事务的控制方式有哪些

MySQL中并发事务的控制方式无非就两种:锁和MVCC。锁可以看作是悲观控制的模式,多版本并发控制MVCC可以看作是乐观控制的模式。

锁控制方式下会通过锁来显式控制共享资源而不是通过调度手段,MySQL中主要是通过读写锁来实现并发控制。

  • 共享锁(S 锁):又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。
  • 排他锁(X 锁):又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条记录加任何类型的锁(锁不兼容)。

读写锁可以做到读读并行,但是无法做到写读、写写并行。另外,根据锁粒度的不同,又被分为表级锁和行级锁。InnoDB不光支持表级锁,还支持行级锁,默认为行级锁。行级

锁的粒度更小,仅对相关的记录上锁即可(对一行或者多行记录加锁),所以对于并发写入操作来说InnoDB的性能更高。不论是表级锁还是行级锁,都存在共享锁(S 锁)和排他锁(X锁)这两类。

MVCC是多版本并发控制方法,即对一份数据会存储多个版本,通过事务的可见性来保证事务能看到自己应该看到的版本。通常会有一个全局的版本分配器来为每一行数据设置版本号,版本号是唯一的。

MVCC在MySQL中实现所依赖的手段主要是:隐藏字段、readView、undo log。

  • undolog:用于记录某行数据的多个版本的数据。
  • readView和隐藏字段:用来判断当前版本数据的可见性。

MySQL的隔离级别是基于锁实现的吗

MySQL的隔离级别基于锁和MVCC机制共同实现的。

SERIALIZABLE隔离级别是通过锁来实现的,READ-COMMITTEDREPEATABLE-READ 隔离级别是基于MVCC实现的。不过SERIALIZABLE 之外的其他隔离级别可能也需要用到锁机制,就比如REPEATABLE-READ在当前读情况下需要使用加锁读来保证不会出现幻读。

MySQL的默认隔离级别

InnoDB存储引擎的默认支持的隔离级别是REPEATABLE-READ。我们可以通过SELECT @@tx_isolation命令来查看,MySQL 8.0该命令改为SELECT @@transaction_isolation

事务日志

undolog

Innodb通过undolog的设计来实现对事务原子性的保证。undolog最直观的用途是用于支持事务的回滚操作。任何针对行记录的修改操作都会一种类似写时复制(COW)的方式

在生成新版本数据的同时,也会通过undolog保留修改前的数据副本,这样每条行记录根据修改的先后顺序,会形成一条版本链。其中版本链是通过触发修改行为的事务id进行标

识。之所以能够组成一个版本链,是因为同一数据行的undolog之间,会通过修改的先后顺序依次通过回滚指针roll_ptr指向上一个版本的undolog

对于事务的原子性我们可以从以下三个方面来理解:

  • 屏蔽中间态数据: 一个事务产生的修改会通过其事务id进行版本标识,这样在事务未提交前其作出的修改都不会被外界所认可,外界的读操作可以借助行记录对应的undolog回滚并获取到上一个已提交的正式数据版本。
  • 全部提交: 当事务提交时,这样一瞬间其产生的所有行记录对应的数据版本都会被外界所认可,体现了原子性中全部动作一起成功。
  • 全部回滚:当事务回滚时,其产生的所有行记录对应数据版本都被外界否定。与此同时,可以很方便地借助undolog将涉及修改的行记录内容回滚成上一个版本的状态,体现了原子性中全部动作一起失败。

此外,因为undolog版本链的存在,也为外界在读取行记录时提供了一个能够自由选取指定版本的能力,这样就很好地契合了MVCC一致性非锁定读的实现。

言归正传,undolog以数据行记录为粒度,其存放在数据库的特殊共享表空间undo segment内,可以将其理解为一类特殊的数据,其本身也有自己的表结构,也可以进行数据持久化,因此在Innodb中会通过redolog来保证undolog的持久性。在对一行数据作出更新(insert/update/delete)时,需要通过undolog的形式对前一个数据版本进行留痕,并且需要对版本先后顺序进行指针的串联。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

💡 需要注意的是在利用undolog回滚数据并不是在物理层面上将Innodb中的数据恢复到执行事务前的样子。因为Innodb中的数据存储单元是page的粒度,而以数据行为粒度的。undolog只能在逻辑层面上针对该行数据执行逆向操作,以使其逻辑性地恢复到上一个版本的样子。此时由于并发事务的存在,page中的其它行可能也在进行更新操作,因此整个page在物理层面上很可能已经演变成截然不同的样子。

是做什么的

作用:事务回滚,实现多版本控制。

每一个事务对数据的修改都会被记录到undolog,当执行事务过程中出现错误或者需要执行回滚操作的话,MySQL 可以利用undolog将数据恢复到事务开始之前的状态。

undolog属于逻辑日志,记录的是SQL语句,比如说事务执行一条DELETE语句,那undolog就会记录一条相对应的INSERT语句。同时undolog的信息也会被记录到redolog中,因为undolog也要实现持久性保护。并且undolog本身是会被删除清理的,例如INSERT操作,在事务提交之后就可以清除掉了;UPDATE/DELETE操作在事务提交不会立即删除,会加入history list,由后台线程purge进行清理。

undolog是采用segment的方式来记录的,每个undo操作在记录的时候占用一个undo log segmentundo log segment包含在rollback segment中。事务开始时,需要为其分配一个rollback segment。每个rollback segment有1024个undo log segment,这有助于管理多个并发事务的回滚需求。

通常情况下, rollback segment header(通常在回滚段的第一个页)负责管理rollback segmentrollback segment headerrollback segment的一部分,通常在回滚段的第一个页。history list是rollback segment header的一部分,它的主要作用是记录所有已经提交但还没有被清理(purge)的事务的undolog。这个列表使得purge线程能够找到并清理那些不再需要的undolog记录。

另外,MVCC的实现依赖于:隐藏字段、Read Viewundolog。在内部实现中,InnoDB通过数据行的DB_TRX_IDRead View来判断数据的可见性,如不可见,则通过数据行的DB_ROLL_PTR找到undolog中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建Read View之前已经提交的修改和该事务本身做的修改。

undolog对比redolog

undoLogredoLog相比二者都算是用来提供数据恢复的日志。不同的是redoLog是物理日志,而undoLog是逻辑日志。比如我们执行了一条update操作,undoLog就会将update的反向操作记录下来,而redoLog则只会记录修改后的数据值。并且undoLog本身也需要持久化支持,所以undoLog也会产生redoLog。很多人说undoLog属于redoLog的逆向操作,两者呈对立关系。这个理解是不到位的,这两种日志本身不属于一个维度的东西。下面通过几个方面对两者展开对比:

  • 内容粒度:redoLog以页page为粒度,记录一个page物理层面的数据内容;undoLog以行为粒度,记录数据行记录前一个版本的数据内容
  • 使用目的:redoLog是一种类似于预写日志的内容,用于实现对数据持久性及写操作性能的保证;undoLog采用一种类似写时复制的策略,记录一个行记录上一版本的旧数据,并通过指针串联成版本链,用以支持事务回滚操作以及MVCC的版本选取策略
  • 存储介质:redoLog通过位于磁盘上的redoLog`` file 进行持久化存储;undoLog属于一类特殊的数据,存放于Innodb共享表空间undo segment 中,本身也需要依赖于 redoLog实现数据持久化

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

存储格式

前面提到,undoLog本身可以视为一类特殊的数据,因此存储时也会有自己从属的undo page,这部分内容位于数据库内部的特殊共享表空间undo segment当中。

写操作可以分为插入(insert)和更新(update/delete)两大类。根据不同的写操作类别,会生成不同类型的undoLog

insert

针对于insert类型的undoLog,除了执行该操作的事务本身之外,对于其他事务都是不可见的,因此无需考虑与MVCC有关的内容。这种undoLog格式比较简单,可以在事务提交后直接删除,其格式如下:

  • next/start: 头部的next字段记录下一条undoLog起始偏移量;尾部的 start字段记录本条undoLog 的起始偏移量
  • type_compl: 对应undoLog的类型,insert操作枚举值固定为11
  • undo no: 本条undo记录的编号
  • table id: 对应表的编号
  • n_unique_index:该区域记录了表中各个唯一键的内容长度以及对应的具体内容

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

update

针对执行update操作而形成的undoLog,由于需要对MVCC机制进行支持,因此需要形成版本链的拓扑结构,其在insert数据格式的基础上,在以下几项内容上会有所不同

  • type_compl: undo 类型,枚举值固定为12
  • trx_id: 执行操作的事务id
  • roll_ptr: 指向上一版本undoLog记录的指针
  • update_vector: 本次操作更新的列及其旧值
  • n_bytes_below: 后续内容记录各列的完整数据

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

delete

至于delete操作对应生成的undoLog,与update类型的格式基本一致,区别在于type_compl枚举值变为14,以及少了update_vector模块部分的内容:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

产生机制

在事务执行过程中,每当在对一个数据行记录进行写操作时,不论是insert/update还是delete操作,都会生成对应的undoLog。其中insert操作相对比较简单,下面以

undate操作为例进行说明:

  1. 生成undoLog: 基于更新前的行数据版本,生成对应的undoLog,插入到该行对应undo log list的起点位置
  2. 修改行数据: 接下来再对page中对应的行记录进行修改

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

一条undoLog就这样成功生成了。但是此时生成的undoLog是在内存中处于一种dirty page的状态,需要借由这条undoLog本身对应的redoLog来实现数据持久化。

🤔 前面我们多次提到了undoLog需要进行持久化。那么为什么要持久化呢?为了防止因数据库的突然宕机而导致数据内容的丢失。

但是undoLog的内容本身都与运行时的事务强相关,如果数据库一旦发生宕机,那么所有事务自然也就不复存在了。那么为什么还需要依赖到undoLog的内容呢?

我们从undoLog两个核心用途出发,进一步对上面的问题进行拆解:

支持事务回滚:如果数据库宕机了,事务自然执行失败了,还需要依赖于undoLog进行回滚吗?

支持 MVCC:如果数据库宕机了,那么所有活跃事务自然也就不存在了,那么也就不存在MVCC的使用场景了,还需要用到老版本undoLog中的内容吗?

使用机制

针对undoLog其实包含了三大核心用途:

  • 支持事务回滚

事务执行过程中,在修改行记录前,会通过undoLog保留上一个版本的数据副本,这样一旦需要对事务进行回滚,就可以很方便地通过undoLog进行数据回溯。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 支持MVCC

在可重复读的事务隔离级别下,为了支持一致性非锁定读(MVCC)操作,老版本的undoLog不能在新事务提交后立即删除,因为它可能还会被更早的活跃事务依赖到。因此其需要在undo log list中保留一段时间,直到确保没有任何事务依赖到它时,才能通过purge线程进行回收删除。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 支持数据恢复流程

此处来正面回答一下,undoLog也需要通过redoLog进行持久化的原因。

这里试想一个场景,在一个page中,有行A和行B两行记录:

  1. 时刻1:事务1对行A进行修改,也生成对应行A上一版本数据的undoLog
  2. 时刻2:事务2对行B进行修改,也会生成对应行的undoLog
  3. 时刻3:事务2提交了,这样基于force log at commit机制,本次提交行为会把page对应redoLog持久化到磁盘的redo log file中(注意,此时事务1还没提交,因此该page 中行A还处于脏数据状态,但同样被连带着持久化到redo log file中了)
  4. 时刻4:数据库宕机了

接下来在数据库重启时,会基于该page对应的redoLog进行数据恢复操作,可想而知就可能把行A恢复成事务1未提交的脏数据状态,在这个环节中,如果行A对应的undoLog因为没来得及持久化而丢失了,那么之前正式版本的数据就无迹可寻了。基于上述案例可以明确一点,undoLog也是必须通过redoLog来保证持久性的,否则在数据恢复流程出现正式数据丢失的问题。

回收机制

Innodb中会有一个异步的purge线程,专门负责对undoLog对应的page进行内容清理,清理后的page可以在后续流程中重新进行分配复用。在清理undoLog时,需要遵循指定校验条件。额外值得注意的就是,在可重复读的事务隔离级别下,需要保证不存在更小编号的活跃事务存在时,才能回收一笔undoLog

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

relaylog

relaylog是做什么的

中继(重做)日志,在主从同步的时候使用,他是一个中介临时的日志文件,用于存储从master到节点同步过来的binlog日志内容。

master主节点的binlog传到slave节点之后,被写入relaylog,从节点里sql线程从relaylog里读取日志然后应用到slave从节点本地。slave的IO线程将master的二进制日志读取过来记录到slave本地文件,然后sql线程会读取relaylog日志的内容并应用到slave从而使slave和master的数据保持一致。

binlog

是做什么的

redoLog它是物理日志,记录的是在某个数据页上做了什么修改。而binlog是逻辑日志(归档日志),记录的是语句的原始逻辑,并且是顺序写,类似于给ID=2这一行的c字段加1,属于MySQL Server 层,是一个二进制格式的文件。主要作用是主从复制和数据恢复。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

记录格式

binlog日志有三种格式,可以通过binlog_format参数指定。

  • statement:记录的内容是SQL语句原文,存在数据一致性问题

比如执行一条update T set update_time = now() where id=1,记录的内容如下。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

同步数据时,会执行记录的SQL语句,但是有个问题,update_time = now()这里会获取当前系统时间,直接执行会导致与原库的数据不一致。

  • row:记录包含操作的具体数据,能保证同步数据的一致性

记录的内容不再是简单的SQL语句了,还包含操作的具体数据,记录内容如下。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

row格式记录的内容看不到详细信息,要通过mysqlbinlog工具解析出来。update_time = now()变成了具体的时间update_time = 1627112756247,条件后面的@1、@2、@3都是该行数据第1个~3个字段的原始值(假设这张表只有 3 个字段)。这样就能保证同步数据的一致性,通常情况下都是指定为row,这样可以为数据库的恢复与同步带来更好的可靠性。

但是这种格式,需要更大的容量来记录,比较占用空间,恢复与同步时会更消耗IO资源,影响执行速度。

  • mixed:记录的内容是前两者的混合,MySQL会判断这条SQL语句是否可能引起数据不一致:如果是,就用row格式,否则就用statement格式。
写入机制

事务执行过程中,先把日志写到binlog缓存,事务提交的时候,再把binlog缓存写到binlog文件中。因为一个事务的binlog不能被拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一个块内存作为binlog缓存。可以通过binlog_cache_size参数控制单个线程binlog缓存大小,如果存储内容超过了这个参数,就要暂存到磁盘(Swap)。binlog也提供了sync_binlog参数来控制写入page cache和磁盘的时机:

  • 0:每次提交事务都只写入到文件系统的page cache,由系统自行判断什么时候执行fsync,机器宕机,page cache里面的binlog会丢失。
  • 1:每次提交事务都会执行fsync,就如同redoLog日志刷盘流程 一样。
  • N(N>1):每次提交事务都写入到文件系统的page cache,但累积N个事务后才fsync。如果机器宕机,会丢失最近N个事务的binlog日志。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

上图的 write,是指把日志写入到文件系统的page cache,并没有把数据持久化到磁盘,所以速度比较快。 fsync才是将数据持久化到磁盘的操作。writefsync的时机,可以由参数sync_binlog控制,默认是1

0的时候,表示每次提交事务都只write,由系统自行判断什么时候执行fsync。虽然性能得到提升,但是机器宕机,page cache里面的binlog会丢失。如下图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

为了安全起见,可以设置为1,表示每次提交事务都会执行fsync,就如同redoLog日志刷盘流程 一样。

设置为N(N>1)是一种折中方式,表示每次提交事务都write,但累积N个事务后才fsync。如下图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

redolog

重做日志用来实现事务的持久性,即D特性。它由两部分组成:

  • 内存中的重做日志缓冲
  • 重做日志文件

在事务提交时,必须先将该事务的所有日志写入到redoLog文件中,待事务的commit操作完成才算整个事务操作完成。在每次将redoLog`` buffer 中的数据写入redoLog file后,都需要调用一次fsync操作,因为redoLog buffer只是把内容先写入操作系统的缓冲系统中,并没有确保直接写入到磁盘上,所以必须进行一次fsync操作。因此,磁盘的性能在一定程度上也决定了事务提交的性能。

我们知道,对于InnoDB这种以磁盘作为存储介质的数据库来说,全量数据内容是存储在磁盘上的。但是在事务执行过程中,针对涉及到的数据记录,会基于局部性原理,以数据所在的页page(默认16KB)为单位将对应内容从磁盘加载到内存中。如果此时事务执行的是更新操作并且事务被成功提交,那么内存中的数据会被更新,且直到该数据被溢写更新回到磁盘之前,该page在内存和磁盘中的结果是不一致的,我们称这样的页page为脏页dirty page

InnoDB的核心就在于在事务提交时,应该采取怎样的持久化机制来保证这部分变更的数据能够被持久稳定地存储下来。一种实现方式是,直接将内存中的page溢写回到磁盘文件中,这种方案足够简单粗暴,但是这是一次磁盘随机写操作,性能方面会显得比较不尽如人意。

InnoDB选择采用另一种方式,当遇到事务提交时,内存中对应的dirty page不直接溢写回到ibd文件对应位置,而是采取一种类似预写日志(WAL,write ahead log)的思路,以磁盘顺序写的方式生成该page对应的redoLog file。这样既通过磁盘存储形式,保证了对事务持久性的支持,又能通过大幅度规避磁盘随机写操作的发生,在很大程度上提高了事务的执行性能。redoLog本身是为了防止数据库宕机的一项保险措施, 在宕机后的数据恢复流程中,通过存量ibd文件以及redoLog file就能还原出最真实准确的数据。

在数据库正常运行场景中,其实是不需要使用到redoLog的,因为此时哪怕磁盘ibd文件内容与内存中的dirty page存在差异也不会影响数据的一致性:

  • 当对应读写操作涉及到这部分dirty page时,会直接复用内存中实时性较高的数据,因此不会有问题
  • 如果dirty page因内存淘汰策略即将被踢出内存时,也会确保持久化到ibd文件中,保证内容的一致性
为什么需要redolog

InnoDB引擎中的内存结构中,主要的内存区域就是Buffer Pool,在Buffer Pool中缓存了很多的数据页。 当我们在一个事务中,执行多个增删改的操作时,InnoDB引擎会先操作Buffer Pool中的数据,如果Buffer Pool没有对应的数据,会通过后台线程将磁盘中的数据加载出来,存放在Buffer Pool中,然后将缓冲池中的数据修改,修改后的数据页我们称为脏页(dirty page)。 而脏页则会在一定的时机,通过后台线程刷新到磁盘中,从而保证Buffer Pool与磁盘的数据一致。 假如刷新到磁盘的过程出错了,而提示给用户事务提交成功,而数据却没有持久化下来,这就出现问题了,没有保证事务的持久性。

🤔 既然redoLog的最终目的也是刷盘,那么只要每次把修改后的数据页直接刷盘不就好了,为什么还需要写redoLog呢?

数据页大小是16KB,刷盘比较耗时,可能就修改了数据页里的几 Byte 数据,有必要把完整的数据页刷盘吗?而且数据页刷盘是随机写,因为一个数据页对应的位置可能在硬盘文件的随机位置,所以性能是很差。如果是写redoLog,一行记录可能就占几十Byte,只包含表空间号、数据页号、磁盘文件偏移量、更新值,再加上是顺序写,所以刷盘速度很快。所以用redoLog形式记录修改内容,性能会远远超过刷数据页的方式,这也让数据库的并发能力更强。

产生机制

InnoDB基于局部性原理,将逻辑上的最小存储单元设定为页page,而redoLog同样以page为粒度,存储的内容是一个page在更新后物理层面上的数据状态。redoLog本质上由两部分组成:

  • 内存中的重做日志缓存区redoLog buffer,属于易失性存储,每当有事务执行并执行写操作时,会以数据所从属的page为单位,生成对应的redoLog,并将其投递到redoLog buffer
  • 磁盘中的重做日志文件redoLog file,属于持久性存储,在事务提交时,会把内存中的redoLog持久化到磁盘上的redoLog file

本质上这个持久化动作又可以被拆解为两个小步骤:

  1. 投递文件系统缓冲区: 首先将redoLog提交到文件系统缓冲区中,此时仍可能存在数据丢失的风险,当操作系统崩溃时,这部分内容会丢失
  2. fsync强制落盘: 接下来需要执行fsync操作,确保内容被落盘写入到redoLog file中,至此才真正的持久化。这种机制就称之为Force Log At Commit机制,而此处的fsync操作也就是性能瓶颈所在。

💡上述是常规的事务提交流程,只有通过fsync操作保证redoLog强制落盘,才能在严格意义上实现事务的持久化。然而由于fsync操作是整个流程的性能瓶颈所在,所以在实际应用场景中,使用方也可以在稳定性与高性能之间进行权衡取舍,选择延迟或者降低fsync的执行频次,舍弃一部分持久性的要求,进而提高整体性能表现。

具体来说可以通过修改参数innodb_flush_log_at_trx_commit来调整redoLog落盘的策略:

  • 设置为 0:选择不主动落盘,而把redoLog持久化时机交由db master thread
  • 只将redoLog投递到文件系统缓冲区而不执行 fsync,把真正落盘的执行时机托付给操作系统
redolog是做什么的

MySQL中数据是以页为单位,你查询一条记录,会从硬盘把一页的数据加载出来放入到 Buffer Pool 中。后续的查询都是先从 Buffer Pool 中找,没有命中再去硬盘加载,减少硬盘IO开销,提升性能。更新表数据的时候,发现 Buffer Pool 里存在要更新的数据,就直接在 Buffer Pool 里更新。然后会把在某个数据页上做了什么修改记录到重做日志缓存(redoLog`` buffer )里,接着刷盘到redoLog文件里。

redoLogInnoDB存储引擎所特有的一种日志,用于记录事务操作的变化物理日志文件,记录的是数据修改之后的值,不管事务是否提交都会记录下来。可以做数据恢复并且提供crash-safe能力。当有增删改相关操作时,会先记录到redoLog中,并修改缓存也中的数据,等到MySQL闲下来的时候才会真正的将redoLog中的数据写入磁盘。

在每次进行写数据的时候都会发生IO,对于InnoDB来说每次修改一次数据都发生IO在性能上肯定是不被允许的,所以加入了事务日志缓存这个概念,这就是redoLog`` buffer (日志缓冲区)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在概念上,InnoDB通过force log at commit机制实现事务的持久性,即在事务提交的时候,必须先将该事务的所有事务日志写入到磁盘上的redo log fileundo log file中进行持久化。为了确保每次日志都能写入到事务日志文件中,在每次将log buffer中的日志写入日志文件的过程中都会调用一次操作系统的fsync操作,调用fsync的作用就是将缓冲中的日志刷到磁盘上。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

刷盘时机

redoLog刷盘有几种情况:

  1. 事务提交:当事务提交时,redoLog`` buffer 里的redoLog会被刷新到磁盘(可以通过innodb_flush_log_at_trx_commit参数控制)。

  2. redoLog`` buffer 空间不足:redoLog`` buffer 中缓存的redoLog已经占满了redoLog`` buffer 总容量(通过innodb_log_buffer_size参数设置)的大约一半左右,就需要把这些日志刷新到磁盘上。

  3. 事务日志缓冲区满:InnoDB使用一个事务日志缓冲区(transaction log buffer)来暂时存储事务的重做日志条目。当缓冲区满时,会触发日志的刷新,将日志写入磁盘。

  4. Checkpoint(检查点):InnoDB定期会执行检查点操作,将内存中的脏数据(已修改但尚未写入磁盘的数据)刷新到磁盘,并且会将相应的重做日志一同刷新,以确保数据的一致性。

  5. 后台刷新线程:InnoDB启动了一个后台线程,负责周期性(每隔1秒)地将脏页(已修改但尚未写入磁盘的数据页)刷新到磁盘,并将相关的重做日志一同刷新。

    后台线程每隔1秒就会把redoLog`` buffer 中的内容写到文件系统缓存(page cache),然后调用fsync刷盘。也就是说,一个没有提交事务的redoLog记录,也可能会刷盘。这是因为在事务执行过程redoLog记录是会写入redoLog`` buffer 中,这些redoLog记录会被后台线程刷盘。

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  6. 正常关闭服务器:MySQL关闭的时候,redoLog都会刷入到磁盘里去。

总之,InnoDB在多种情况下会刷新重做日志,以保证数据的持久性和一致性。我们要注意设置正确的刷盘策略innodb_flush_log_at_trx_commit。根据MySQL配置的刷盘策略的不同,宕机之后可能会存在轻微的数据丢失问题。innodb_flush_log_at_trx_commit的值有3种,也就是共有三种刷盘策略:

  • 设置为0时,表示每次事务提交时不进行刷盘操作。这种方式性能最高,但是也最不安全,因为如果MySQL挂了或宕机了,可能会丢失最近1秒内的事务。
  • 设置为1时,表示每次事务提交时都进行刷盘操作。这种方式性能最低,但是也最安全,因为只要事务提交成功,redoLog记录就一定在磁盘里,不会有任何数据丢失。
  • 设置为2时,表示每次事务提交时都只把log buffer里的redoLog内容写入page cache(文件系统缓存)。page cache是专门用来缓存文件的,这里被缓存的文件就是redoLog文件。这种方式的性能和安全性都介于前两者中间。

刷盘策略innodb_flush_log_at_trx_commit的默认值为 1,设置为1的时候才不会丢失任何数据。为了保证事务的持久性,我们必须将其设置为1。

下面是不同刷盘策略的流程图。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

redolog对比binlog
  • redoLogInnodb独有的日志,而binlog是server层的,所有的存储引擎都有使用到
  • redoLog记录了具体的数值,对某个页做了什么修改,binlog记录的操作内容
  • binlog大小达到上限或者flush log会生成一个新的文件,而redoLog有固定大小只能循环利用
  • binlog日志没有crash-safe的能力,只能用于归档。而redoLogcrash-safe能力

我们对redoLogbinlog之间的关系作个对比:

  • 产生源头:binlogMysql数据库层面产生的,不依附于任何存储引擎,属于全局共用的二进制日志;redoLogInnodb专属的,供引擎内部使用
  • 存储内容:binlog的记录内容是逻辑层面的增量执行SQL语句,主要用途可能用于数据库之间的主从复制,通过重放增量SQL的方式复刻出完整的数据内容;redoLog以page为粒度存储物理意义上的页数据,其目的是为了兼顾事务的持久性以及写操作的高性能

🤔其实通过binlog是不是也能闭环实现数据的持久化,Innodb引入redoLog是否存在重复设计?

redoLog的采用是有必要的,这个问题的核心就在于数据恢复流程的效率问题。在Innodb中,每次启动数据库时,都会统一基于redoLog执行数据恢复流程,而不会刻意区分此前数据库是异常宕机还是正常终止。同样是通过持久化日志还原数据,基于binlog这种逻辑增量记录的方式,其效率是远远不如基于redoLog这种物理日志的。

另外在Innodb中,除了普通数据外,针对回滚日志undoLog也需要持久化,这些都需要通过redoLog的能力加以保证。

针对binlogredoLog的写入时机进行对比分析:

  • binlog:其产生是在事务提交时,以事务为粒度,一次打包产生,按照事务提交先后顺序进行排列,与每个提交事务一一对应
  • redoLog:其产生时在事务修改数据时(可能没有提交),以page为粒度持续生成并投入到redoLog`` buffer ,最终在事务提交时被强制持久化落盘。在 Innodb中是能支持多事务并行的,因此多个事务可能会穿插生成redoLog,直到某个事务提交时,则强制将此前对应的一系列redoLog进行强制落盘。
redolog是怎么记录日志的

硬盘上存储的redoLog日志文件不只一个,而是以一个日志文件组的形式出现的,每个的redoLog大小都是一样的。比如可以配置为一组4个文件,每个文件的大小是 1GB,整个redoLog日志文件组可以记录4G的内容。它采用的是环形数组形式,从头开始写,写到末尾又回到头循环写,如果数据写满了但是还没来得及将数据真正刷入磁盘中,那么就会发生内存抖动的现象从肉眼的角度来观察会发现Mysql会宕机一会,此时就是正在刷盘。如下图所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在这个日志文件组中还有两个重要的属性,分别是write pos、checkpoint

  • write pos是当前记录的位置,一边写一边后移
  • checkpoint是当前要擦除的位置,也是往后推移

每次刷盘redoLog记录到日志文件组中,write pos位置就会后移更新。

每次MySQL加载日志文件组恢复数据时,会清空加载过的redoLog记录,并把checkpoint后移更新。write poscheckpoint之间的还空着的部分可以用来写入新的redoLog记录。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如果write pos追上checkpoint ,表示日志文件组满了,这时候不能再写入新的redoLog记录,MySQL得停下来清空一些记录,把checkpoint推进一下。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

redolog存储格式

在逻辑意义上,每份redoLog对应一个page的粒度;而在物理意义redoLoglog block为单元进行存储,整个redoLog`` buffer 可以视为一个队列,而

每个log block则为队列中的一个元素。

每个log block大小固定为512B,刚好契合了磁盘扇区的大小,其由三部分组成:

  • log block header:block头元信息部分,共计12B大小,由下述字段组成:
    • block hdr no(4B):该block在redoLog buffer中的index
    • lock block hdr data len(2B):该block中已使用的空间大小,单位Byte,占满时为 0x200(512B)
    • block first rec group(2B):该block中首条新redoLog对应的偏移量。(比如log block body中,前20B为上一个block末尾redoLog的内容拼接延续,则该项值为12(header)+ 20(last log) = 32 )
    • log block checkpoint no(4B):该block被写入时的检查点信息
  • log block body:核心日志内容,即redoLog正文部分,大小为492B
  • lock block tailer:block尾部,共计8B,包含一份lock block hdr no(4B)和填充空间(4B)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

逻辑意义上的一条redoLog对应一个page,其存储位置位于log block body当中。当block body仍有空间富余时,多条redoLog可以共用一个block;当block有剩余空间但不足以承载下一条redoLog时,则会对redoLog进行截断,并将剩余部分放置到下一个相邻block中。

不同数据库操作类型会对应不同的redoLog格式,但总体来看,其大概包含了如下所属的核心信息:

  • redo_log_type: 修改生成该份redoLog的数据库操作类型
  • space: 表空间对应的id
  • page_no: 该redoLog对应的是哪个page
  • redo_log_body: page中的具体数据内容

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

redolog使用机制

基于redoLog进行数据恢复的过程中,离不开对LSN的使用。LSN全称Log Sequence Number,其含义是,在某个时刻下,所有事务写入重做日志的总量。我们可以把LSN理解为一个逻辑时间轴。比如时刻A,总共已写入的redoLog大小为1KB,则全局LSN计数值1000;接下来在时刻 B,一个事务又写入了100B的redoLog,则此时全局的LSN计数值被更新为1100,且刚才生成这笔100B大小的redoLog也会记录其在生成时刻对应的LSN值,反映了其生成的时序。

除了redoLog之外,ibd文件中每个page中也会有一个FIL_PAGE_LSN值,记录了该page最后更新时的全局LSN计数值,反映了其数据的实时程度。这样后续在通过redoLog恢复该page数据时,所有redo.LSN <= FIL_PAGE_LSNredoLog都应该被忽略。

Innodb在启动时不管上次数据库是正常关闭还是宕机,都会基于redoLog对ibd文件开启数据恢复的流程。由于redoLog是基于page粒度的物理存储,因此恢复性能比较优秀。在恢复流程中,会将磁盘上的redoLog file一一加载到内存的redoLog buffer中(基于 LSN 先后顺序排列)。此时会有一个全局的checkpoint LSN,表示此前的redoLog都已经完整归并到ibd中,因此这前半部分的redoLog是已经可以清除了的。接下来只需要遍历处理checkpoint LSN后半部分的redoLog即可。每次在将一笔redoLog更新到ibd文件对应page之前,需要额外检查一下redoLog中的LSN和ibd page中FIL_PAGE_LSN的大小,只有redo.LSN > FIL_PAGE_LSN时,才进行更新操作。

💡ibd page LSN可能大于redo.LSN,是因为对应的 dirty page可能因为被提前淘汰出内存,已经进行过磁盘溢写操作,因此其数据的实时程度可能更高

两阶段提交

redoLogInnoDB拥有了崩溃恢复能力。binlog保证了MySQL集群架构的数据一致性。虽然它们都属于持久化的保证,但是侧重点不同。

在执行更新语句过程,会记录redoLogbinlog两块日志,以基本的事务为单位redoLog在事务执行过程中可以不断写入,而binlog只有在提交事务时才写入,所以redoLogbinlog的写入时机不一样。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

再将两阶段提交之前,我们先思考两个问题:

  1. 如果redoLogbinlog两份日志之间的逻辑不一致,会出现什么问题呢?

我们以update语句为例,假设id=2的记录,字段c值是0,把字段c值更新成1SQL语句为update T set c = 1 where id = 2

假设执行过程中写完redoLog日志后,binlog日志写期间发生了异常,会出现什么情况呢?

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

由于binlog没写完就异常,这时候binlog里面没有对应的修改记录。因此,之后用binlog日志恢复数据时,就会少这一次更新,恢复出来的这一行c值是0,而原库因为redoLog日志恢复,这一行c值是1,最终数据不一致。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

为了解决两份日志之间的逻辑一致(即数据一致)问题,InnoDB使用两阶段提交方案。原理很简单,将redoLog的写入拆成了两个步骤preparecommit,这就是两阶段提交。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

使用两阶段提交后写入binlog时发生异常也不会有影响,因为MySQL根据redoLog恢复数据时,发现redoLog还处于prepare阶段,并且没有对应binlog,就会回滚该事务。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. redoLog设置commit阶段发生异常会不会回滚事务呢?

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

由上图可知并不会回滚事务,虽然redoLog是处于prepare阶段,但是能通过事务id找到对应的binlog日志,所以MySQL认为是完整的,就会提交事务恢复数据。

MVCC

MVCC最大的优势:读不加锁,读写不冲突。在读多写少的场景下极大的增加了系统的并发性能。

为什么需要MVCC

InnoDB相比MyISAM有两大特点,一是支持事务二是支持行级锁,事务的引入带来了一些新的挑战。相对于串行处理来说,并发事务处理能大大增加数据库资源的利用率,提高数据库系统的事务吞吐量,从而可以支持更多的用户。但并发事务处理也会带来一些问题,主要包括以下几种情况:

  1. 更新丢失:当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题——最后的更新覆盖了其他事务所做的更新。如何避免这个问题呢,最好在一个事务对数据进行更改但还未提交时,其他事务不能访问修改同一个数据。
  2. 脏读:一个事务正在对一条记录做修改,在这个事务并提交前,这条记录的数据就处于不一致状态。这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些尚未提交的脏数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象地叫做脏读。
  3. 不可重复读:一个事务在读取某些数据已经发生了改变、或某些记录已经被删除了!这种现象叫做不可重复读。
  4. 幻读:一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为幻读。

以上是并发事务过程中会存在的问题,解决更新丢失可以交给应用,但是后三者需要数据库提供事务间的隔离机制来解决。实现隔离机制的方法主要有两种:

  1. 加读写锁
  2. 一致性快照读,即MVCC

但本质上,隔离级别是一种在并发性能和并发产生的副作用间的妥协,通常数据库均倾向于采用Weak Isolation

基本概念

当前读

读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。对于我们日常的操作,如:select … lock in share mode(共享锁),select …for updateupdateinsertdelete(排他锁)都是一种当前读。

快照读

简单的select(不加锁)就是快照读。快照读,读取的是记录数据的可见版本,有可能是历史数据,不加锁,是非阻塞读。

不同的隔离级别,生成ReadView的时机不同:

  • Read Committed:在事务中每一次执行快照读时生成ReadView。
  • Repeatable Read:仅在事务中第一次执行快照读时生成ReadView,后续复用该ReadView。
  • Serializable:快照读会退化为当前读。

RCRR两种隔离级别的事务在执行普通的读操作时,通过访问版本链的方法,使得事务间的读写操作得以并发执行,从而提升系统性能。RCRR这两个隔离级别的一个很大不同就是生成ReadView的时间点不同,RC在每一次SELECT语句前都会生成一个ReadView,事务期间会更新,因此在其他事务提交前后所得到的m_ids列表可能发生变化,使得先前不可见的版本后续又突然可见了。而RR只在事务的第一个SELECT语句时生成一个ReadView,事务操作期间不更新。

MVCC

全称Multi-Version Concurrency Control,多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突,快照读为MySQL实现MVCC提供了一个非阻塞读功能。MVCC的具体实现,还需要依赖于数据库记录中的三个隐式字段、undoLog日志、readView

隐藏字段
隐藏字段含义
DB_TRX_ID最近修改事务ID,记录插入这条记录或最后一次修改该记录的事务ID。
DB_ROLL_PTR回滚指针,指向这条记录的上一个版本,用于配合undoLog,指向上一个版本。
DB_ROW_ID隐藏主键,如果表结构没有指定主键,将会生成该隐藏字段。
undolog

回滚日志,在insert、update、delete的时候产生的便于数据回滚的日志。当insert的时候,产生的undoLog日志只在回滚时需要,在事务提交后,可被立即删除。而update、delete的时候,产生的undoLog日志不仅在回滚时需要,在快照读时也需要,不会立即被删除。

readview

ReadView(读视图)是快照读SQL执行时MVCC提取数据的依据,记录并维护系统当前活跃的事务(未提交的)id。ReadView中包含了四个核心字段:

字段含义
trx_ids当前活跃的事务ID集合
min_trx_id最小活跃事务ID
max_trx_id预分配事务ID,当前最大事务ID+1(因为事务ID是自增的)
creator_trx_idReadView创建者的事务ID

ReadView中规定了版本链数据的访问规则:

trx_id代表当前undoLog版本链对应事务ID。

条件是否可以访问说明
trx_id == creator_trx_id可以访问该版本成立,说明数据是当前这个事务更改的。
trx_id < min_trx_id可以访问该版本成立,说明数据已经提交了。
trx_id > max_trx_id不可以访问该版本成立,说明该事务是在ReadView生成后才开启。
min_trx_id <= trx_id <= max_trx_id如果trx_id不在m_ids中,是可以访问该版本的成立,说明数据已经提交。

rw_trx_ids:当前活跃的事务ID数组,即ReadView初始化时当前未提交的事务列表;

low_limit_id:当前行最大事务ID。事务ID大于等于此值的事务对于view都是不可见的;

up_limit_id:rw_trx_ids 最小值,当前行最小活跃事务ID。事务ID小于此值的事务对于view一定是可见的;

low_limit_no:trx_no小于此值的undo log对于view是可以purge的。

实现原理

InnoDBMVCC的实现方式为:每一行记录都有两个隐藏列:DATA_TRX_IDDATA_ROLL_PTR(如果没有主键,则还会多一个隐藏的主键列)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

DATA_TRX_ID

记录最近更新这条行记录的事务ID,大小为6个字节。每处理一个事务,值自动加一。InnoDB中每个事务都有一个唯一的事务ID叫做transaction id。在事务开始时向InnoDB事务系统申请得到,是按申请顺序严格递增的。每行数据是有多个版本的,每次事务更新数据时都会生成一个新的数据版本,并且把transaction id赋值给这个数据行的DB_TRX_ID

DATA_ROLL_PTR

表示指向该行回滚段(rollback segment)的指针,大小为7个字节。InnoDB通过这个指针找到上个版本的数据。该行记录上一个旧版本,在undo中都通过链表的形式组成。

DB_ROW_ID

行标识(隐藏的自增ID),大小为6字节,如果声明了主键,InnoDB以用户指定的主键构建B+Tree,如果未声明主键,InnoDB会自动生成一个隐藏主键。另外,每条记录的头信息(record header)里都有一个专门的bit(deleted_flag)来表示当前记录是否已经被删除。

如何组织Undo Log版本链

在多个事务并行操作某行数据的情况下,不同事务对该行数据的UPDATE会产生多个版本,然后通过回滚指针组成一条undoLog版本链,下面我们通过一个例子来看一下undoLog链是如何组的,DATA_TRX_IDDATA_ROLL_PTR两个参数在其中又起到什么样的作用。

事务1对值a进行更新之后,该行即产生一个新版本和旧版本。假设之前插入该行的事务ID100,事务AID200,该行的隐藏主键为1

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

事务1的操作过程为:

  1. DB_ROW_ID = 1的这行记录加排他锁
  2. 把该行原本的值拷贝到undoLog中,DB_TRX_IDDB_ROLL_PTR都不动
  3. 修改该行的值,这时产生一个新版本,更新DATA_TRX_ID为修改记录的事务ID,将DATA_ROLL_PTR指向刚刚拷贝到undoLog链中的旧版本记录,这样就能通过DB_ROLL_PTR找到这条记录的历史版本。如果对同一行记录执行连续的UPDATEundoLog会组成一个链表,遍历这个链表可以看到这条记录的变更
  4. 记录redoLog,包括undoLog中的修改

那么INSERTDELETE会怎么做呢?其实相比UPDATE 这二者很简单,INSERT会产生一条新记录,它的DATA_TRX_ID为当前插入记录的事务ID;DELETE某条记录时可看成是一种特殊的UPDATE,其实是软删,真正执行删除操作会在commit时,DATA_TRX_ID则记录下删除该记录的事务ID。

如何实现一致性读ReadView

RU隔离级别下,直接读取版本的最新记录,对于SERIALIZABLE隔离级别,则是通过加锁互斥来访问数据,因此不需要MVCC的帮助。所以MVCC是运行在RCRR这两个隔离级别下,当InnoDB隔离级别设置为二者其一时,在SELECT数据时就会用到版本链。核心问题是版本链中哪些版本对当前事务可见?

InnoDB为了解决这个问题,设计了ReadView。要实现RC在另一个事务提交之后其他事务可见和RR在一个事务中SELECT操作一致,就是依靠ReadView

RU级别下,ReadView会在事务中的每一个SELECT语句查询发送前生成(也可以在声明事务时显式声明START TRANSACTION WITH CONSISTENT SNAPSHOT),因此每次SELECT都可以获取到当前已提交事务和自己修改的最新版本。

RR级别下,每个事务只会在第一个SELECT语句查询发送前或显式声明处生成,其他查询操作都会基于这ReadView,这样就保证了一个事务中的多次查询结果都是相同的,因为他们都是基于同一个ReadView下进行查询操作的。

InnoDB为每一个事务构造了一个数组m_ids用于保存一致性视图生成瞬间当前所有活跃事务(开始但未提交事务)的ID,将数组中事务ID最小值记为m_up_limit_id,当前系统中已创建事务ID最大值+1记为m_low_limit_id,构成如图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

一致性视图下查询操作的流程如下:

  1. 当查询发生时根据以上条件生成ReadView,该查询操作遍历undoLog链,根据当前被访问版本(可以理解为undoLog链中每一个记录即一个版本,遍历都是从最新版本向老版本遍历)的DB_TRX_ID,如果DB_TRX_ID小于m_up_limit_id,则该版本在ReadView生成前就已经完成提交,该版本可以被当前事务访问。DB_TRX_ID在绿色范围内的可以被访问
  2. 若被访问版本的DB_TRX_ID大于m_low_limit_id,说明该版本在ReadView生成之后才生成,因此该版本不能被访问,根据当前版本指向上一版本的指针DB_ROLL_PT访问上一个版本,继续判断。DB_TRX_ID在灰色色范围内的都不允许被访问
  3. 若被访问版本的DB_TRX_ID[m_up_limit_id, m_low_limit_id)区间内,则判断DB_TRX_ID是否等于当前事务ID,等于则证明是当前事务做的修改,可以被访问,否则不可被访问, 继续向上寻找。只有DB_TRX_ID等于当前事务ID才允许访问红色范围内的版本
  4. 最后,还要确保满足以上要求的可访问版本的数据的delete_flag不为true,否则查询到的是删除的数据。

以上总结就是只有当前事务修改的未commit版本和所有已提交事务版本允许被访问。

RR下的ReadView生成

RR隔离级别下,每个事务在执行第一个 SELECT 语句时,后续所有的SELECT都是复用这个ReadView,其它update, delete, insert语句和一致性读快照的建立没有关系,会将当前系统中的所有的活跃事务拷贝到一个列表生成ReadView

下图中事务1第一条SELECT语句在事务2更新数据前,因此生成的ReadView在事务1过程中不发生变化,即使事务2在事务1之前提交,但是事务1第二条查询语句依旧无法读到事务2的修改。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

下图中,事务1的第一条SELECT语句在事务2的修改提交之后,因此可以读到事务2的修改。但是注意,如果事务1的第一条SELECT语句查询时,事务2还未提交,那么事务1也查不到事务 B 的修改。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

RC下的ReadView生成

RC隔离级别下,每个SELECT语句开始时,都会重新将当前系统中的所有的活跃事务拷贝到一个列表生成ReadView。与RR的区别就在于生成ReadView的时间点不同,一个是事务之后第一个SELECT语句开始、一个是事务中每条SELECT语句开始。

ReadView中将当前活跃的事务ID列表,称之为m_ids,其中最小值为up_limit_id,最大值为low_limit_id,事务ID是事务开启时InnoDB分配的,其大小决定了事务开启的先后顺序,因此我们可以通过ID的大小关系来决定版本记录的可见性,具体判断流程如下:

  1. 如果被访问版本的trx_id小于m_ids中的最小值up_limit_id,说明生成该版本的事务在ReadView生成前就已经提交了,所以该版本可以被当前事务访问。
  2. 如果被访问版本的trx_id大于m_ids中的最大值low_limit_id,说明生成该版本的事务在生成ReadView后才生成,所以该版本不可以被当前事务访问。需要根据undoLog链找到前一个版本,然后根据该版本的DB_TRX_ID重新判断可见性。
  3. 如果被访问版本的trx_id属性值在m_ids列表中最大值和最小值之间(包含),那就需要判断一下trx_id的值是不是在m_ids列表中。如果在,说明创建ReadView时生成该版本所属事务还是活跃的,因此该版本不可以被访问,需要查找undoLog链得到上一个版本,然后根据该版本的DB_TRX_ID再从头计算一次可见性;如果不在,说明创建 ReadView时生成该版本的事务已经被提交,该版本可以被访问。
  4. 此时经过一系列判断我们已经得到了这条记录相对ReadView来说的可见结果。此时,如果这条记录的delete_flagtrue,说明这条记录已被删除,不返回。否则说明此记录可以安全返回给客户端。
MySQL 事务是指一组数据库操作,这些操作要么全部执行,要么全部不执行,其目的是保证在并发环境下,数据的一致性和完整性。MySQL 事务具有 ACID 性质,即原子性、一致性、隔离性和持久性。 MySQL 中使用事务需要使用 BEGIN、COMMIT 和 ROLLBACK 语句,其中 BEGIN 表示开启一个事务,COMMIT 表示提交事务,ROLLBACK 表示回滚事务事务的基本语法如下: ``` BEGIN; -- 执行一组数据库操作 COMMIT; -- 提交事务 -- 或者 ROLLBACK; -- 回滚事务 ``` 在 MySQL 中,事务的隔离级别分为四个等级,分别是 Read Uncommitted、Read Committed、Repeatable Read 和 Serializable。隔离级别越高,数据的一致性和完整性越高,但同时也会影响数据库的性能。 MySQL 事务的 ACID 性质有以下含义: 1. 原子性(Atomicity):事务中的所有操作要么全部执行成功,要么全部失败回滚,不会只执行其中的一部分操作。 2. 一致性(Consistency):事务执行前后,数据库中的数据必须保持一致性状态,即满足数据库的约束条件和完整性规则。 3. 隔离性(Isolation):事务之间应该是相互隔离的,一个事务的执行不应该被其他事务干扰,保证事务之间的数据相互独立。 4. 持久性(Durability):事务提交后,对数据库的修改应该是永久性的,即使出现系统故障或电源故障,也不应该对数据产生影响。 总之,MySQL 事务是一组数据库操作,具有 ACID 性质,可以通过 BEGIN、COMMIT 和 ROLLBACK 语句来实现,隔离级别越高,数据的一致性和完整性越高,但同时也会影响数据库的性能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值