MySQL--事务、日志及MVCC
T00 Lv2

本文将从事务着手,讲述事务的四大特性,并发事务可能带来的问题以及不同的隔离级别,然后聚焦于日志中的undolog和redolog,解析其中的本质,进而了解MVCC的含义以及其如何实现隔离级别等等内容。

事务

事务是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作 要么同时成功,要么同时失败。

默认MySQL的事务是自动提交的,也就是说,当执行一条DML语句,MySQL会立即隐式的提交事务。

接下来我们手动进行事务的控制来体会一下其中的含义。

事务控制

事务控制主要三步操作:开启事务、提交事务/回滚事务。

  1. 需要在这组操作执行之前,先开启事务 ( start transaction; / begin;)

  2. 所有操作如果全部都执行成功,则提交事务 ( commit; )

  3. 如果这组操作中,有任何一个操作执行失败,都应该回滚事务 ( rollback )

正常操作

1
2
3
4
5
6
7
8
9
10
11
12
13
-- 开启事务
start transaction;
-- 1. 保存员工基本信息
insert into emp
values (39, 'Tom', '123456', '汤姆', 1, '13300001111', 1, 4000, '1.jpg', '2023-11-01', 1, now(), now());
-- 2. 保存员工的工作经历信息
insert into emp_expr(emp_id, begin, end, company, job)
values (39,'2019-01-01', '2020-01-01', '百度', '开发'),
(39,'2020-01-10', '2022-02-01', '阿里', '架构');
-- 提交事务(全部成功)
commit;
-- 回滚事务(有一个失败)
rollback;

只执行1,2但不执行commit和rollback

image

可以看到表中没有插入数据

image image

这是为什么呢?

这是由于事务的隔离性,一个事务的成功或者失败对于其他的事务是没有影响,当前的事务并没有提交,在其他页面是看不见的。

在使用commit提交之后,便可以查看到数据。

image image

异常情况

1
2
3
4
5
6
7
8
9
10
11
12
13
-- 开启事务
start transaction;
-- 1. 保存员工基本信息
insert into emp
values (40, 'Jim', '123456', '吉姆', 1, '133000011122', 1, 4000, '1.jpg', '2023-11-01', 1, now(), now());
-- 2. 保存员工的工作经历信息
insert into emp_expr(emp_id, begin, end, company, job)
values (40,'2019-01-01', '2020-01-01', '百度', '开发')
(40,'2020-01-10', '2022-02-01', '阿里', '架构');
-- 提交事务(全部成功)
commit;
-- 回滚事务(有一个失败)
rollback;

我们将2中的一个逗号删除,这样就造成了异常情况

image

这时使用查询语句可以发现员工表中新增了数据,但是经理历表没有

image image

这时应该使用回滚,数据录入就取消了,保证了数据的一致性和完整性。

四大特性及其实现原理

上面我们自己体验一遍事务的流程,已经有了个大致的了解,接下来我们来学习有关于事务的四大特性。

四大特性分别是:

  1. 原子性(Atomicity)

  2. 隔离性(Isolation)

  3. 持久性(Durability)

  4. 一致性(Consistency)

事务的四大特性简称为:ACID

原子性

原子性是指事务是不可分割的最小单元, 这就是刚才说的要么都成功, 要么都失败, 不会在某个没执行成功的中间态结束。

原子性是通过undolog实现的,每一次数据的更新都会在undolog中记录一条对应的信息,存放在版本链中,也就是存放在一个链表结构中,如果某个操作会打破原子性,就可以通过undolog回滚保证其原子性。

持久性

原子性是指一个事务一旦提交或者回滚,它对数据库中数据的改变就是永久的,即使数据库崩溃了,它的修改也不会丢失。

持久性是通过redolog实现的,每一次事务的更新都会通过redolog buffer向redolog中刷盘,将对应的数据顺序存入,可以很好的保证持久性。

但其实也有可能导致数据丢失,只不过大部分数据是可以保存的,仅仅少部分会丢失,具体细节也在日志一栏中论述。

隔离性

隔离性是指并发访问数据库时,一个用户的事务不会被其他事务的操作所干扰. 多个并发事务之间相互隔离,每个事务不受并发影响,独立执行。

隔离性是通过锁机制,undolog及MVCC来实现的,在不同的SQL语句执行时会加不同的锁,这本身就是实现隔离性的做法,同时undolog和MVCC配合达到实现不同的隔离级别,从而实现事务的隔离性。

隔离级别及其原理也在下文中阐述。

一致性

事务完成时,必须使所有的数据都保持一致状态,总量不变,类似于物质守恒定理。

一致性的实现就很简单了,只要我们能保证以上三条性质都能实现,一致性自然就实现了。

所以可以概括为下图(一致性包括了原子性和持久性):

image

并发事务带来的问题

并发事务带来的问题总结起来也就以下三点:

  1. 脏读
  2. 不可重复度
  3. 幻读

他们三者的关系由上及下越来越顽固,也就是说幻读解决了,其它两种问题肯定也被解决了。

脏读

脏读是指一个事务读到另一个事务没有提交的数据。

如下图,在事务A更新数据后,即便还没提交事务,事务B也能查询到更改以后的数据,这就是脏读

image

不可重复读

不可重复读是指一个事务先后读取同一条记录,但两次读取的数据不同。

如下图,在事务A两次查询数据的中间,对数据进行了更新,导致这两次查询结果即使在同一事务中,但是查询的结果不同,这就是不可重复读

image

幻读

幻读是指一个事务查询数据时,没有对应的行,但是插入数据时,又发现这行数据已经存在了,好像出现幻影一样。

如下图,事务A首先查询id为1的数据,没有查询到,如何事务B直接插入数据,此时事务A再进行数据插入,发现数据无法插入,但是我们查询数据还是查不到(因为已经解决了幻读),就像闯鬼了一样,这就是幻读

image

隔离级别

隔离级别粉四种:

  1. 读未提交(read uncommitted)
  2. 读已提交(read committed)
  3. 可重复读(repeatable read)默认
  4. 串行化(serializable)

他们的隔离级别由上及下越来越高,性能越来越低。

他们可以解决的问题如下表(O表示可以解决,X表示不可以解决):

隔离级别 脏读 不可重复读 幻读
读未提交 X X X
读已提交 O X X
可重复读 O O X
串行化 O O O

可以通过以下代码查询当前隔离级别:

1
select @@transaction_isolation;

通过以下代码改变隔离级别:

1
set session/gloab transaction isolation level ... ;

读未提交

读未提交的隔离级别下我们就可以体会到什么是脏读。

我们使用stu表来演示,表结构如下:

image

我们把左侧客户端的隔离级别设置为read uncommitted

在右侧还没提交事务前,已经可以查询到更新后的数据,明显就是脏读的现象。

image

读已提交

读已提交的隔离级别解决了脏读的问题,但是我们还是可以展示不可重复读的现象。

我们把左侧客户端的隔离级别设置为read committed

右侧未提交事务时,即使更改了数据,也是查不到的,解决了脏读问题。

image

但是右侧一旦提交事务,左侧在同一个事务中查到了不同的数据,这就是不可重复读。

image

可重复读

可重复读的隔离级别下,解决了不可重复读,但是依旧可以展示幻读的情况。

我们把左侧客户端的隔离级别设置为repeatable read

右侧提交事务后,左侧查询的数据还是不变,解决了不可重复度。

image

但是右侧插入数据之后,左侧依旧无感,但是无法插入数据,这就是幻读。

image

在可重复读这种隔离级别下,当第一次读的时候,会生成一份数据快照,生成快照后, 其他事务的修改对当前事务来说是不可见的,然后你自己这个事务内用的是同一个数据快照,所以也不用担心不可重复读的问题。

串行化

解决了所有隔离性的问题,但是并发性能是最差的。

我们把左侧客户端的隔离级别设置为serializable

在右侧执行更新语句时,由于左侧事务没有提交,所以它一直处于阻塞状态,这也是能解决幻读的原因,当然,这样的话,性能就变得很差了,但是数据安全性最好。

image

串行化要求事务不能并发执行, 必须一个接一个顺序执行. 你都无法并发执行了, 那些并发问题自然而然就不存在了。

所以事务隔离级别等级越高,就越能保证数据的一致性和完整性, 但是执行效率也越低. 事务的隔离级别越低, 执行效率就越高, 但是数据的一致性就越差

具体的实现将在下文中的MVCC中进行解析。

日志

架构

MySQL5.5 版本开始,默认使用InnoDB存储引擎,它擅长事务处理,具有崩溃恢复特性,在日常开发 中使用非常广泛。

下面是InnoDB架构图,左侧为内存结构,右侧为磁盘结构。

image

内存结构

在左侧的内存结构中,主要分为这么四大块儿: Buffer Pool、Change Buffer、Adaptive Hash Index、Log Buffer。 接下来介绍一下这四个部分。

  1. Buffer Pool

我们都知道MySQL的数据是存在磁盘上的, 但是每次从磁盘中读数据,写数据是不是太慢了, 怎么办呢?

你想想,写业务代码的时候, 查数据太慢了怎么办? 加一层缓存。

你再想想, CPU执行速度那么快,内存速度又比较慢,怎么办? CPU三级缓存

InnoDB存储引擎,为了尽可能提升性能,设计了一个缓冲池, 叫buffer pool,MySQL 存数据是以页为单位的,一个页16kb,每查询一条数据, 会从硬盘把一页的数据加载出来,把这个数据页放到BufferPool中。

为什么要一次放一个页呢?

  1. 因为磁盘访问是按块(页)读取的,一次取整页的速度比多次取单行快得多。

  2. MySQL一次把一个页的数据放入bufferpool中,那对于取连续数据来说, 命中率就很高了,这就是利用了程序的空间局部性原理。


缓冲池Buffer Pool,是主内存中的一个区域,里面可以缓存磁盘上经常操作的真实数据,在执行增删改查操作时,先操作缓冲池中的数据(若缓冲池没有数据,则从磁盘加载并缓存),然后再以一定频率刷新到磁盘,从而减少磁盘IO,加快处理速度。

缓冲池以Page页为单位,底层采用链表数据结构管理Page。根据状态,将Page分为三种类型:

  • free page:空闲page,未被使用。
  • clean page:被使用page,数据没有被修改过。
  • dirty page:脏页,被使用page,数据被修改过,也中数据与磁盘的数据产生了不一致。
  1. Change Buffer

Change Buffer,更改缓冲区(针对于非唯一二级索引页),在执行DML语句时,如果这些数据Page没有在Buffer Pool中,不会直接操作磁盘,而会将数据变更存在更改缓冲区 Change Buffer 中,在未来数据被读取时,再将数据合并恢复到Buffer Pool中,再将合并后的数据刷新到磁盘中。

Change Buffer的意义是什么呢?

二级索引与聚集索引不同,二级索引通常是非唯一的,并且以相对随机的顺序插入二级索引。同样,删除和更新可能会影响索引树中不相邻的二级索引页,如果每一次都操作磁盘,会造成大量的磁盘IO。有了 ChangeBuffer之后,我们可以在缓冲池中进行合并处理,这是顺序的,减少了磁盘IO。

  1. Adaptive Hash Index

自适应hash索引,用于优化对Buffer Pool数据的查询。MySQL的innoDB引擎中虽然没有直接支持hash索引,但是给我们提供了一个功能就是这个自适应hash索引。因为前面我们讲到过,hash索引在 进行等值匹配时,一般性能是要高于B+树的,因为hash索引一般只需要一次IO即可,而B+树,可能需要几次匹配,所以hash索引的效率要高,但是hash索引又不适合做范围查询、模糊匹配等。

InnoDB存储引擎会监控对表上各索引页的查询,如果观察到在特定的条件下hash索引可以提升速度, 则建立hash索引,称之为自适应hash索引,自适应哈希索引,无需人工干预,是系统根据情况自动完成

  1. Log Buffer

Log Buffer:日志缓冲区,用来保存要写入到磁盘中的log日志数据(redo log 、undo log), 默认大小为16MB,日志缓冲区的日志会定期刷新到磁盘中。如果需要更新、插入或删除许多行的事 务,增加日志缓冲区的大小可以节省磁盘 I/O。

参数:

innodb_log_buffer_size:缓冲区大小

innodb_flush_log_at_trx_commit:日志刷新到磁盘时机,取值主要包含以下三个:

1: 日志在每次事务提交时写入并刷新到磁盘,默认值。

0: 每秒将日志写入并刷新到磁盘一次。

2: 日志在每次事务提交后写入,并每秒刷新到磁盘一次。

磁盘结构

  1. System Tablespace

系统表空间是更改缓冲区的存储区域。如果表是在系统表空间而不是每个表文件或通用表空间中创建的,它也可能包含表和索引数据。(在MySQL5.x版本中还包含InnoDB数据字典、undolog等)

  1. File-Per-Table Tablespaces

如果开启了innodb_file_per_table开关 ,则每个表的文件表空间包含单个InnoDB表的数据和索引 ,并存储在文件系统上的单个数据文件中。

开关参数:innodb_file_per_table ,该参数默认开启。

  1. General Tablespaces

通用表空间,需要通过 CREATE TABLESPACE 语法创建通用表空间,在创建表时,可以指定该表空间。

创建表空间

1
CREATE TABLESPACE ts_name  ADD  DATAFILE  'file_name' ENGINE = engine_name;

创建表时指定表空间

1
CREATE  TABLE  xxx ...  TABLESPACE  ts_name;
  1. Undo Tablespaces

撤销表空间,MySQL实例在初始化时会自动创建两个默认的undo表空间(初始大小16M),用于存储 undo log日志。

  1. Temporary Tablespaces InnoDB

使用会话临时表空间和全局临时表空间。存储用户创建的临时表等数据。

  1. Doublewrite Buffer Files

双写缓冲区,innoDB引擎将数据页从Buffer Pool刷新到磁盘前,先将数据页写入双写缓冲区文件 中,便于系统异常时恢复数据。

  1. Redo Log

重做日志,是用来实现事务的持久性。该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log),前者是在内存中,后者在磁盘中。当事务提交之后会把所 有修改信息都会存到该日志中, 用于在刷新脏页到磁盘时,发生错误时, 进行数据恢复使用。

隐藏字段

我们创建一张表时,除了可以直接看见的字段以外,InnoDB还会自动的给我们添加三个隐藏字段及其含义分别是:

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

而上述的前两个字段是肯定会添加的, 是否添加最后一个字段DB_ROW_ID,得看当前表有没有主键, 如果有主键,则不会添加该隐藏字段。

1
ibd2sdi employee.ibd

undolog

回滚日志,在insert、update、delete的时候产生的便于数据回滚的日志。

undolog中最重要的一个概念是版本链,简单来说,版本链就是MySQL中用来管理同一条数据不同版本的数据结构。

  1. 当insert的时候,产生的undolog日志只在回滚时需要,在事务提交后,可被立即删除。

  2. update、delete的时候,产生的undolog日志不仅在回滚时需要,在快照读时也需要,不会立即被删除。

版本链

有一张表原始数据为:

image

**DB_TRX_ID **: 代表最近修改事务ID,记录插入这条记录或最后一次修改该记录的事务ID,是 自增的。

DB_ROLL_PTR : 由于这条数据是才插入的,没有被更新过,所以该字段值为null。

此时,有四个并发事务同时在访问这张表。

  1. 第一步
image

当事务2执行第一条修改语句时,会记录undolog日志,记录数据变更之前的样子; 然后更新记录, 并且记录本次操作的事务ID,回滚指针,回滚指针用来指定如果发生回滚,回滚到哪一个版本。

版本链更新如下图:

image
  1. 第二步
image

当事务3执行第一条修改语句时,也会记录undolog日志,记录数据变更之前的样子; 然后更新记录,并且记录本次操作的事务ID,回滚指针。

image
  1. 第三步
image

当事务4执行第一条修改语句时,也会记录undolog日志,记录数据变更之前的样子; 然后更新记录,并且记录本次操作的事务ID,回滚指针。

image

最终我们发现,不同事务或相同事务对同一条记录进行修改,会导致该记录的undolog生成一条记录版本链表,链表的头部是最新的旧记录,链表尾部是最早的旧记录。

这就是版本链的生成过程。

undolog就是通过版本链来管理不同版本的数据,从而达到某些数据的回滚,进而实现原子性。

redolog

重做日志,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性

该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log file),前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都存到该日志文件中,用于在刷新脏页到磁盘,发生错误时,进行数据恢复使用。

我们先来看看没有redolog的情况:

image

我们知道,在InnoDB引擎中的内存结构中,主要的内存区域就是缓冲池,在缓冲池中缓存了很多的数据页。

  1. 当我们在一个事务中,执行多个增删改的操作时,InnoDB引擎会先操作缓冲池中的数据,并判断其中是否有数据
  2. 如果缓冲区没有对应的数据,会通过后台线程将磁盘中的数据加载出来,
  3. 存放在缓冲区中,
  4. 然后将缓冲池中的数据修改,修改后的数据页我们称为脏页。
  5. 而脏页则会在一定的时机,通过后台线程刷新到磁盘 中,从而保证缓冲区与磁盘的数据一致。

而缓冲区的脏页数据并不是实时刷新的,而是一段时间之后将缓冲区的数据刷新到磁盘中,假如刷新到磁盘的过程出错了,而提示给用户事务提交成功,而数据却没有持久化下来,这就出现问题了,没有保证事务的持久性。

那么,如何解决上述的问题呢? 在InnoDB中提供了一份日志redo log,接下来我们再来分析一 下,通过redo log如何解决这个问题。

image

有了redolog之后,

  1. 当对缓冲区的数据进行增删改之后,会首先将操作的数据页的变化,记录在redo log buffer中。

  2. 在事务提交时,会将redo log buffer中的数据刷新到redo log磁盘文件中。

这个过程是在buffer pool将数据刷盘到磁盘上之前的,所以可以解决buffer pool未及时刷盘,数据丢失的问题,也就是保证了事务的持久性

那为什么我们不直接刷盘到磁盘上,而是选择刷盘redo log呢?

因为buffer pool刷盘是随机io,刷盘的时候需要找到某个磁盘页, 然后修改,然后再去找另外一个磁盘页,再修改,本来磁盘操作就 慢,随机io的话就更慢了,而redo log在往磁盘文件中写入数据,由于是日志文件,所以都是顺序写的。顺序写的效率,要远大于随机写。

如果我们redo log没刷盘, MySQL就崩溃了, 会不会丢失数据?

我们知道在向redo log中写入数据之前,我们会先向redo log buffer中写入数据,然后统一把数据刷入磁盘当中。

这里有一个参数叫innodb_flush_log_at_trx_commit,这个参数就是控制redo log刷盘时间的。

  1. 如果值为0(延迟写,延迟刷)每隔1s将redo log buffer中的数据写入redo log中,这样就有可能造成这1s内的数据丢失。
  2. 如果值为1(实时写,实时刷)事务提交时会把redo log的数据刷入磁盘,刷盘完成后才会告诉客户端事务执行成功了。这也就能保证事务只要完成,即便MySQL崩溃数据也不会丢失。所以一般情况下,把这个参数设置为1,那事务提交redo log就能刷盘,事务的持久性也能得到保 证。
  3. 如果值为2(实时写,延迟刷)事务提交时会把redo log的数据写入操作系统文件缓存中, 也就是写入page Cache。数据写入page cache后, 操作系统就会在某个时间真正把数据写入磁盘。那只要操作系统不崩溃,那就能保证持久性. 但是如果说电脑突然断电了, 那数据就丢了。

binlog

binlog是在客户端发送SQL请求之后,就会将对应的数据写入binlog当中,这里也有一个参数,可以全局用作数据恢复,它的取值可以为:

  1. 值为0(实时写,延迟刷)它在提交事务之后,写到page cache中,然后操作系统它自己决定啥时候把binlog中的数据刷入磁盘。
  2. 值为1(实时写,实时刷)提交时直接就刷入磁盘。
  3. 值为N(实时写,延迟刷)每次提交事务时先写入page cache,然后达到N个事务以后,再统一刷入磁盘。

当然,binlog也是有binlog buffer的,在写入磁盘前或者page cache之前,都是先传给binlog buffer的。

binlog和redolog的区别

redolog的底层原理就只适合做局部的数据恢复:

  1. redolog是循环写,它把它本身的空间占满了会删除最开始的数据,而且它只能记录还未被刷盘的数据,被刷盘的数据就已经被删除了;
  2. redolog是事务级的数据记录,不是数据库级,只能恢复某些原因导致的buffer pool中未刷盘的数据,并不能做到全局数据恢复;
  3. redolog是物理日志,记录内容是在某个数据页上做了什么修改。

binlog更适合做全局恢复:

  1. 但是无论MySQL是做了任何操作,都会记录一条日志进入binlog,这是数据库级的数据记录;

  2. 同时binlog是追加写,它记录的是全量日志;

  3. binlog是逻辑日志,记录的内容就类似于sql语句本身,所以 binlog非常适合做备份恢复和主从同步。

两阶段提交

事务提交后,redolog和binlog都要刷盘,但是如果一个刷盘成功了, 一个失败了,两份日志就不一致了,怎么办?

这时就是两阶段提交的发挥空间:

image

为了解决两份日志之间的一致性问题,MySQL将redolog的写入拆成了两个步骤prepare和commit,这就是两阶段提交。

整个执行流程分了这么几步:

  1. 开始事务;
  2. 更新数据;
  3. 写入redo log (此时redo log是prepare阶段)
  4. 提交事务,写入binlog,redolog设置为commit阶段

如果写入redolog异常了,也就是prepare异常了,那redolog和binlog都没有数据,那就直接回滚事务。

如果写入binlog异常,那么mysql根据redolog进行日志数据恢复时,会发现redolog处于prepare阶段,并且没有对应binlog日志,那就回滚事务。

如果redolog设置commit异常了,那么mysql就发现redolog为prepare阶段,但是能找到对应的binlog日志,就证明redolog和binlog是一致的,MySQL也会认为数据是完整的,直接提交事务。

所以两阶段提交最终还是要看binlog,,只要binlog刷盘了,那就能提交事务。


MVCC

全称 Multi-Version Concurrency Control,多版本并发控制。指维护一个数据的多个版本, 使得读写操作没有冲突。

快照读为MySQL实现MVCC提供了一个非阻塞读功能。

MVCC的具体实现,还需要依赖于数据库记录中的三个隐式字段、undo log日志readView。


当前读

读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。

对于我们日常的操作,如:select … lock in share mode(共享锁),select … for update、update、insert、delete(排他锁)都是一种当前读。

image

在测试中我们可以看到,即使是在默认的可重复读隔离级别下,事务A中依然可以读取到事务B最新提交的内容,因为在查询语句后面加上了lock in share mode 共享锁,此时是当前读操作。

快照读

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

这也是各种隔离级别实现的一部分原理:

  • 读已提交:每次select,都生成一个快照读。
  • 可重复读:开启事务后第一个select语句才是快照读的地方。
  • 串行化:快照读会退化为当前读。
image

在测试中,我们看到即使事务B提交了数据,事务A中也查询不到。 原因就是因为普通的select是快照读,而在当前默认的可重复读隔离级别下,开启事务后第一个select语句才是快照读的地方,后面执行相同的select语句都是从快照中获取数据,可能不是当前的最新数据,这样也就保证了可重复读。

readview

ReadView(读视图)是快照读SQL执行时MVCC提取数据的依据,记录并维护系统当前活跃的事务 (未提交的)id。

上面我们在日志里面讲了undolog,这里我们就来讲讲怎么读取版本链中的数据。

ReadView中包含了四个核心字段:

字段 含义
m_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中, 是可以访问该版本的 成立,说明数据已经提交。

总结起来就是下面三张图片,也更形象:

image image

image不同的隔离级别,生成ReadView的时机不同,这就是隔离级别的实现原理。


隔离级别的实现

读未提交

不用实现,你根本不用管,多个并发事务同时执行天然就是读未提交。

读已提交

读已提交的本质就是每次select就生成一个新的readview,每次读都生成新的readview,就能每次读到新提交的数据版本。

我们就来分析事务5中,两次快照读读取数据,是如何获取数据的?

在事务5中,查询了两次id为30的记录,由于隔离级别为Read Committed,所以每一次进行快照读 都会生成一个ReadView,那么两次生成的ReadView如下。

image

那么这两次快照读在获取数据时,就需要根据所生成的ReadView以及ReadView的版本链访问规则, 到undolog版本链中匹配数据,最终决定此次快照读返回的数据。

先来看第一次快照读具体的读取过程:

image

在进行匹配时,会从undo log的版本链,从上到下进行挨个匹配:

  1. 先匹配
image

这条记录,这条记录对应的 trx_id为4,也就是将4带入右侧的匹配规则中。 ①不满足 ②不满足 ③不满足 ④也不满足 , 都不满足,则继续匹配undo log版本链的下一条。

  1. 再匹配第二条
image

这条记录对应的trx_id为3,也就是将3带入右侧的匹配规则中。①不满足 ②不满足 ③不满足 ④也不满足 ,都不满足,则继续匹配undo log版本链的下一条。

  1. 再匹配第三条
image

这条记录对应的trx_id为2,也就是将2带入右侧的匹配规则中。①不满足 ②满足终止匹配,此次快照读,返回的数据就是版本链中记录的这条数据。

再来看第二次快照读具体的读取过程:

image

匹配第二条

image

这条记录对应的trx_id为3,也就是将3带入右侧的匹配规则中。①不满足 ②满足 。终止匹配,此次 快照读,返回的数据就是版本链中记录的这条数据

可重复读

可重复读会在第一次select的时候生成一个readview,readview是用来判断数据版本对当前事务的可见性的,可重复读每次都会复用第一次生成的readview;

也就是说,当生成这个readview的时候,哪些版本的数据对当前事务可见就已经固定下来了, 后续每次通过同一个readview判断数据可见性。所以每次都读的是同一个数据版本。

总的来说就是可重复读是通过MVCC+复用第一次生成的readview来实现的。

下面进行剖析:

image

我们看到,在RR隔离级别下,只是在事务中第一次快照读时生成ReadView,后续都是复用该 readview,那么既然readview都一样, readview的版本链匹配规则也一样, 那么最终快照读返 回的结果也是一样的。

除此之外, 可重复读为了避免幻读问题, 还会加锁. 这个后面 详细讲

串行化

直接加锁,只有能拿到锁的事务才能执行,这样事务就串行化执行了。


Powered by Hexo & Theme Keep
Total words 55.8k Unique Visitor Page View