简单来说,数据库事务可以保证多个对数据库的操作构成一个逻辑上的整体,这些数据库操作遵循着要么全部执行成功,要么全部不执行 。
关系型数据库(例如:MySQL
、SQL Server
、Oracle
等)事务都有ACID特性:
- 原子性(
Atomicity
):事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用 - 一致性(
Consistency
):执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的 - 隔离性(
Isolation
):并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的 - 持久性(
Durability
):一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响
👉 只有保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。也就是说 A、I、D 是手段,C 是目的!
而对于这四大特性,实际上分为两个部分。 其中的原子性、一致性、持久化,实际上是由InnoDB
中的两份日志来保证的,即redo log
和undo log
。 而隔离性是通过数据库的锁,加上MVCC
来保证的。
事务的隔离级别
- READ-UNCOMMITTED(读取未提交) :最低的隔离级别,允许读取尚未提交的数据,可能会导致脏读、幻读或不可重复读。
- READ-COMMITTED(读取已提交) :允许读取事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
- REPEATABLE-READ(可重复读) :对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
- SERIALIZABLE(可串行化) :最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 | 可能 | 可能 | 可能 |
读已提交 | 不可能 | 可能 | 可能 |
可重复读 | 不可能 | 不可能 | 可能 |
串行化 | 不可能 | 不可能 | 不可能 |
并发事务带来了哪些问题
在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对同一数据进行操作)。并发虽然是必须的,但可能会导致以下的问题。
- 脏读:一个事务读取数据并且对数据进行了修改,这个修改对其他事务来说是可见的,即使当前事务没有提交。这时另外一个事务读取了这个还未提交的数据,但第一个事务突然回滚,导致数据并没有被提交到数据库,那第二个事务读取到的就是脏数据,这就是脏读。
- 丢失修改:在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。
- 不可重复读:指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
- 幻读:幻读与不可重复读类似。它发生在一个事务读取了几行数据,接着另一个并发事务插入了一些数据时。在随后的查询中,第一个事务就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
不可重复读和幻读有什么区别?
- 不可重复读的重点是内容修改或者记录减少比如多次读取一条记录发现其中某些记录的值被修改;
- 幻读的重点在于记录新增比如多次执行同一条查询语句时,发现查到的记录增加了。
幻读其实可以看作是不可重复读的一种特殊情况,单独把幻读区分出来的原因主要是解决幻读和不可重复读的方案不一样。
举个例子:执行 delete
和 update
操作的时候,可以直接对记录加锁,保证事务安全。而执行 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-COMMITTED
和 REPEATABLE-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 segment
,undo log segment
包含在rollback segment
中。事务开始时,需要为其分配一个rollback segment
。每个rollback segment
有1024个undo log segment
,这有助于管理多个并发事务的回滚需求。
通常情况下, rollback segment header
(通常在回滚段的第一个页)负责管理rollback segment
。rollback segment header
是rollback segment
的一部分,通常在回滚段的第一个页。history list是rollback segment header
的一部分,它的主要作用是记录所有已经提交但还没有被清理(purge)的事务的undolog
。这个列表使得purge线程能够找到并清理那些不再需要的undolog
记录。
另外,MVCC
的实现依赖于:隐藏字段、Read View
、undolog
。在内部实现中,InnoDB
通过数据行的DB_TRX_ID
和Read View
来判断数据的可见性,如不可见,则通过数据行的DB_ROLL_PTR
找到undolog
中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建Read View
之前已经提交的修改和该事务本身做的修改。
undolog对比redolog
undoLog
与redoLog
相比二者都算是用来提供数据恢复的日志。不同的是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
操作为例进行说明:
- 生成
undoLog
: 基于更新前的行数据版本,生成对应的undoLog
,插入到该行对应undo log list
的起点位置 - 修改行数据: 接下来再对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对行A进行修改,也生成对应行A上一版本数据的
undoLog
- 时刻2:事务2对行B进行修改,也会生成对应行的
undoLog
- 时刻3:事务2提交了,这样基于force log at commit机制,本次提交行为会把page对应
redoLog
持久化到磁盘的redo log file
中(注意,此时事务1还没提交,因此该page 中行A还处于脏数据状态,但同样被连带着持久化到redo log file
中了) - 时刻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才是将数据持久化到磁盘的操作。write
和fsync
的时机,可以由参数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
中
本质上这个持久化动作又可以被拆解为两个小步骤:
- 投递文件系统缓冲区: 首先将
redoLog
提交到文件系统缓冲区中,此时仍可能存在数据丢失的风险,当操作系统崩溃时,这部分内容会丢失 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
文件里。
redoLog
是InnoDB
存储引擎所特有的一种日志,用于记录事务操作的变化物理日志文件,记录的是数据修改之后的值,不管事务是否提交都会记录下来。可以做数据恢复并且提供crash-safe
能力。当有增删改相关操作时,会先记录到redoLog
中,并修改缓存也中的数据,等到MySQL闲下来的时候才会真正的将redoLog
中的数据写入磁盘。
在每次进行写数据的时候都会发生IO,对于InnoDB
来说每次修改一次数据都发生IO在性能上肯定是不被允许的,所以加入了事务日志缓存这个概念,这就是redoLog`` buffer
(日志缓冲区)。
在概念上,InnoDB
通过force log at commit
机制实现事务的持久性,即在事务提交的时候,必须先将该事务的所有事务日志写入到磁盘上的redo log file
和undo log file
中进行持久化。为了确保每次日志都能写入到事务日志文件中,在每次将log buffer
中的日志写入日志文件的过程中都会调用一次操作系统的fsync
操作,调用fsync
的作用就是将缓冲中的日志刷到磁盘上。
刷盘时机
redoLog
刷盘有几种情况:
-
事务提交:当事务提交时,
redoLog`` buffer
里的redoLog
会被刷新到磁盘(可以通过innodb_flush_log_at_trx_commit
参数控制)。 -
redoLog`` buffer
空间不足:redoLog`` buffer
中缓存的redoLog
已经占满了redoLog`` buffer
总容量(通过innodb_log_buffer_size
参数设置)的大约一半左右,就需要把这些日志刷新到磁盘上。 -
事务日志缓冲区满:
InnoDB
使用一个事务日志缓冲区(transaction log buffer
)来暂时存储事务的重做日志条目。当缓冲区满时,会触发日志的刷新,将日志写入磁盘。 -
Checkpoint(检查点):
InnoDB
定期会执行检查点操作,将内存中的脏数据(已修改但尚未写入磁盘的数据)刷新到磁盘,并且会将相应的重做日志一同刷新,以确保数据的一致性。 -
后台刷新线程:
InnoDB
启动了一个后台线程,负责周期性(每隔1秒)地将脏页(已修改但尚未写入磁盘的数据页)刷新到磁盘,并将相关的重做日志一同刷新。后台线程每隔
1
秒就会把redoLog`` buffer
中的内容写到文件系统缓存(page cache
),然后调用fsync
刷盘。也就是说,一个没有提交事务的redoLog
记录,也可能会刷盘。这是因为在事务执行过程redoLog
记录是会写入redoLog`` buffer
中,这些redoLog
记录会被后台线程刷盘。 -
正常关闭服务器:
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
redoLog
是Innodb
独有的日志,而binlog
是server层的,所有的存储引擎都有使用到redoLog
记录了具体的数值,对某个页做了什么修改,binlog
记录的操作内容binlog
大小达到上限或者flush log
会生成一个新的文件,而redoLog
有固定大小只能循环利用binlog
日志没有crash-safe
的能力,只能用于归档。而redoLog
有crash-safe
能力
我们对redoLog
和binlog
之间的关系作个对比:
- 产生源头:
binlog
是Mysql
数据库层面产生的,不依附于任何存储引擎,属于全局共用的二进制日志;redoLog
是Innodb
专属的,供引擎内部使用 - 存储内容:
binlog
的记录内容是逻辑层面的增量执行SQL语句,主要用途可能用于数据库之间的主从复制,通过重放增量SQL的方式复刻出完整的数据内容;redoLog
以page为粒度存储物理意义上的页数据,其目的是为了兼顾事务的持久性以及写操作的高性能
🤔其实通过
binlog
是不是也能闭环实现数据的持久化,Innodb
引入redoLog
是否存在重复设计?
redoLog
的采用是有必要的,这个问题的核心就在于数据恢复流程的效率问题。在Innodb
中,每次启动数据库时,都会统一基于redoLog
执行数据恢复流程,而不会刻意区分此前数据库是异常宕机还是正常终止。同样是通过持久化日志还原数据,基于binlog这种逻辑增量记录的方式,其效率是远远不如基于redoLog
这种物理日志的。另外在
Innodb
中,除了普通数据外,针对回滚日志undoLog
也需要持久化,这些都需要通过redoLog
的能力加以保证。
针对binlog
和redoLog
的写入时机进行对比分析:
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 pos
和checkpoint
之间的还空着的部分可以用来写入新的redoLog
记录。
如果write pos
追上checkpoint
,表示日志文件组满了,这时候不能再写入新的redoLog
记录,MySQL
得停下来清空一些记录,把checkpoint
推进一下。
redolog存储格式
在逻辑意义上,每份redoLog
对应一个page的粒度;而在物理意义redoLog
以log 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被写入时的检查点信息
- block hdr 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_LSN
的redoLog
都应该被忽略。
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可能因为被提前淘汰出内存,已经进行过磁盘溢写操作,因此其数据的实时程度可能更高
两阶段提交
redoLog
让InnoDB
拥有了崩溃恢复能力。binlog
保证了MySQL
集群架构的数据一致性。虽然它们都属于持久化的保证,但是侧重点不同。
在执行更新语句过程,会记录redoLog
与binlog
两块日志,以基本的事务为单位redoLog
在事务执行过程中可以不断写入,而binlog
只有在提交事务时才写入,所以redoLog
与binlog
的写入时机不一样。
再将两阶段提交之前,我们先思考两个问题:
- 如果
redoLog
与binlog
两份日志之间的逻辑不一致,会出现什么问题呢?
我们以update
语句为例,假设id=2
的记录,字段c
值是0
,把字段c
值更新成1
,SQL
语句为update T set c = 1 where id = 2
。
假设执行过程中写完redoLog
日志后,binlog
日志写期间发生了异常,会出现什么情况呢?
由于binlog
没写完就异常,这时候binlog
里面没有对应的修改记录。因此,之后用binlog
日志恢复数据时,就会少这一次更新,恢复出来的这一行c
值是0
,而原库因为redoLog
日志恢复,这一行c
值是1
,最终数据不一致。
为了解决两份日志之间的逻辑一致(即数据一致)问题,InnoDB
使用两阶段提交方案。原理很简单,将redoLog
的写入拆成了两个步骤prepare
和commit
,这就是两阶段提交。
使用两阶段提交后写入binlog
时发生异常也不会有影响,因为MySQL
根据redoLog
恢复数据时,发现redoLog
还处于prepare
阶段,并且没有对应binlog
,就会回滚该事务。
redoLog
设置commit
阶段发生异常会不会回滚事务呢?
由上图可知并不会回滚事务,虽然redoLog
是处于prepare
阶段,但是能通过事务id
找到对应的binlog
日志,所以MySQL
认为是完整的,就会提交事务恢复数据。
MVCC
MVCC最大的优势:读不加锁,读写不冲突。在读多写少的场景下极大的增加了系统的并发性能。
为什么需要MVCC
InnoDB
相比MyISAM
有两大特点,一是支持事务二是支持行级锁,事务的引入带来了一些新的挑战。相对于串行处理来说,并发事务处理能大大增加数据库资源的利用率,提高数据库系统的事务吞吐量,从而可以支持更多的用户。但并发事务处理也会带来一些问题,主要包括以下几种情况:
- 更新丢失:当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题——最后的更新覆盖了其他事务所做的更新。如何避免这个问题呢,最好在一个事务对数据进行更改但还未提交时,其他事务不能访问修改同一个数据。
- 脏读:一个事务正在对一条记录做修改,在这个事务并提交前,这条记录的数据就处于不一致状态。这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些尚未提交的脏数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象地叫做脏读。
- 不可重复读:一个事务在读取某些数据已经发生了改变、或某些记录已经被删除了!这种现象叫做不可重复读。
- 幻读:一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为幻读。
以上是并发事务过程中会存在的问题,解决更新丢失可以交给应用,但是后三者需要数据库提供事务间的隔离机制来解决。实现隔离机制的方法主要有两种:
- 加读写锁
- 一致性快照读,即
MVCC
但本质上,隔离级别是一种在并发性能和并发产生的副作用间的妥协,通常数据库均倾向于采用Weak Isolation
。
基本概念
当前读
读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。对于我们日常的操作,如:select … lock in share mode
(共享锁),select …for update
、update
、insert
、delete
(排他锁)都是一种当前读。
快照读
简单的select
(不加锁)就是快照读。快照读,读取的是记录数据的可见版本,有可能是历史数据,不加锁,是非阻塞读。
不同的隔离级别,生成ReadView的时机不同:
- Read Committed:在事务中每一次执行快照读时生成ReadView。
- Repeatable Read:仅在事务中第一次执行快照读时生成ReadView,后续复用该ReadView。
- Serializable:快照读会退化为当前读。
RC
、RR
两种隔离级别的事务在执行普通的读操作时,通过访问版本链的方法,使得事务间的读写操作得以并发执行,从而提升系统性能。RC
、RR
这两个隔离级别的一个很大不同就是生成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_id | ReadView创建者的事务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的。
实现原理
InnoDB
中MVCC
的实现方式为:每一行记录都有两个隐藏列:DATA_TRX_ID
、DATA_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_ID
和DATA_ROLL_PTR
两个参数在其中又起到什么样的作用。
事务1对值a
进行更新之后,该行即产生一个新版本和旧版本。假设之前插入该行的事务ID
为100
,事务A
的ID
为200
,该行的隐藏主键为1
。
事务1的操作过程为:
- 对
DB_ROW_ID = 1
的这行记录加排他锁 - 把该行原本的值拷贝到
undoLog
中,DB_TRX_ID
和DB_ROLL_PTR
都不动 - 修改该行的值,这时产生一个新版本,更新
DATA_TRX_ID
为修改记录的事务ID,将DATA_ROLL_PTR
指向刚刚拷贝到undoLog
链中的旧版本记录,这样就能通过DB_ROLL_PTR
找到这条记录的历史版本。如果对同一行记录执行连续的UPDATE
,undoLog
会组成一个链表,遍历这个链表可以看到这条记录的变更 - 记录
redoLog
,包括undoLog
中的修改
那么INSERT
和DELETE
会怎么做呢?其实相比UPDATE
这二者很简单,INSERT
会产生一条新记录,它的DATA_TRX_ID
为当前插入记录的事务ID;DELETE
某条记录时可看成是一种特殊的UPDATE
,其实是软删,真正执行删除操作会在commit
时,DATA_TRX_ID
则记录下删除该记录的事务ID。
如何实现一致性读ReadView
在RU
隔离级别下,直接读取版本的最新记录,对于SERIALIZABLE
隔离级别,则是通过加锁互斥来访问数据,因此不需要MVCC
的帮助。所以MVCC
是运行在RC
和RR
这两个隔离级别下,当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
,构成如图所示:
一致性视图下查询操作的流程如下:
- 当查询发生时根据以上条件生成
ReadView
,该查询操作遍历undoLog
链,根据当前被访问版本(可以理解为undoLog
链中每一个记录即一个版本,遍历都是从最新版本向老版本遍历)的DB_TRX_ID
,如果DB_TRX_ID
小于m_up_limit_id
,则该版本在ReadView
生成前就已经完成提交,该版本可以被当前事务访问。DB_TRX_ID
在绿色范围内的可以被访问 - 若被访问版本的
DB_TRX_ID
大于m_low_limit_id
,说明该版本在ReadView
生成之后才生成,因此该版本不能被访问,根据当前版本指向上一版本的指针DB_ROLL_PT
访问上一个版本,继续判断。DB_TRX_ID
在灰色色范围内的都不允许被访问 - 若被访问版本的
DB_TRX_ID
在[m_up_limit_id, m_low_limit_id)
区间内,则判断DB_TRX_ID
是否等于当前事务ID,等于则证明是当前事务做的修改,可以被访问,否则不可被访问, 继续向上寻找。只有DB_TRX_ID
等于当前事务ID才允许访问红色范围内的版本 - 最后,还要确保满足以上要求的可访问版本的数据的
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的大小关系来决定版本记录的可见性,具体判断流程如下:
- 如果被访问版本的
trx_id
小于m_ids
中的最小值up_limit_id
,说明生成该版本的事务在ReadView
生成前就已经提交了,所以该版本可以被当前事务访问。 - 如果被访问版本的
trx_id
大于m_ids
中的最大值low_limit_id
,说明生成该版本的事务在生成ReadView
后才生成,所以该版本不可以被当前事务访问。需要根据undoLog
链找到前一个版本,然后根据该版本的DB_TRX_ID
重新判断可见性。 - 如果被访问版本的
trx_id
属性值在m_ids
列表中最大值和最小值之间(包含),那就需要判断一下trx_id
的值是不是在m_ids
列表中。如果在,说明创建ReadView
时生成该版本所属事务还是活跃的,因此该版本不可以被访问,需要查找undoLog
链得到上一个版本,然后根据该版本的DB_TRX_ID
再从头计算一次可见性;如果不在,说明创建ReadView
时生成该版本的事务已经被提交,该版本可以被访问。 - 此时经过一系列判断我们已经得到了这条记录相对
ReadView
来说的可见结果。此时,如果这条记录的delete_flag
为true
,说明这条记录已被删除,不返回。否则说明此记录可以安全返回给客户端。