事务隔离级别与MVCC(看这一篇就够了)

一、事务简介

1.1 事务四大特性(ACID)

  • 原子性(Atomicity):数据库把“要么全做,要么全部做”的这种规则称为原子性
  • 隔离性(Isolation):事务之间相互隔离,不受影响,这与事务的隔离级别密切相关
  • 一致性(Consistency):事务执行前后的状态要一致,可理解为数据一致性
  • 持久性(Durable):事务完成之后,她对数据的修改是永恒的,即时出现故障也能够正常保持。

二、隔离性与隔离级别

本次主要说一下关于隔离性在数据库中是怎么来实现的。
当数据库中有多个事务同时执行的时候,就可能会出现脏读,不可重复读,幻读的问题,这是并不是好的现象,所以为了避免出现这些情况,我们引出了事务的隔离性这一特点。

2.1 事务并发执行时遇到会遇到的问题

  • 脏写(Dirty Write)
    如果一个事务修改了另一个事务提交修改过的数据,就意味着发生了脏写现象。
  • 脏读(Dirty Read)
    如果一个事务读到了另一个未提交事务修改过的数据,就意味着发生了脏读现象。
  • 不可重复读
    如果一个事务修改了另一个未提交事务读取的数据,就意味着发生了不可重复度现象。
  • 幻读
    如果一个事务先根据某些查询条件查询出一些记录,在该事务未提交时,另一个事务写入了一些符合那些收缩条件的记录(这里指INSERT,DELETE,UPDATE 操作),就以为着发生了幻读现象。

2.2 SQL 标准中的 4 中隔离级别

上面我们介绍了事务执行过程中可能会遇到的一些现象,这些现象会对事务的一致性产生不同程度的影响。上面严重性现象排序

脏写 > 脏读 > 不可重复读 > 幻读

为了解决上面的问题,数据库就指定了一个隔离级别标准,隔离级别越低,就越可能发生严重的问题。

  1. READ UNCOMMITTED:未提交读(读未提交)
  2. READ COMMITTED:已提交读(读已提交)
  3. REPEATABLE READ:可重复读
  4. SERIALIZABLE:可串行化

针对不同的隔离界别,并发事务执行过程中可以发生不同的现象,看下标

隔离级别脏读不可重复读幻读
READ UNCOMMITTED可能可能可能
READ COMMITTED不可能可能可能
REPEATABLE READ不可能不可能可能
SERIALIZABLE不可能不可能不可能

也就是说:

  • 在 READ UNCOMMITTTED 隔离级别下,可能发生脏读,不可重复读和幻读现象;
  • 在 READ COMMITTED 隔离级别下,可能发生不可重复读和幻读现象,但是不可能发生脏读现象;
  • 在 REPEATABLE READ 隔离级别下,可能发生幻读现象,但是不可能发生脏读和不可重复读现象;
  • 在 SERIALIZABLE 隔离级别下,上述各种现象都不可能发生。

脏写: 脏写这个现象对一致性影响太严重了,无论哪种隔离级别,都不允许出现脏写的情况发生。

2.3 案例说明上述情况

假设我们现在有一张表,表中有 主键 ID 和 年龄 age 两个字段
执行一条 SQL:insert into t value (1, 1)

接下来我们开启两个事务,从上到下时间线

执行时间顺序事务A事务B
1启动事务,查询得到值 1启动事务
2-查询得到值 1
3-1 改成 2
4查询得到值 V1-
5-提交事务 B
6查询得到值 V2-
7提交事务A-
8查询得到值 V3-

分析一下在不同的隔离级别下来,事务 A 会有哪些不同的返回结果,也就是表格中的 V1,V2,V3 的返回值分别是什么。

2.3.1 读未提交隔离级别下

按照每一步来分析

  1. 两个事务同时开启
  2. 事务B 获取值1
  3. 事务B 将其1改成2
  4. 事务A 得到 V1 的值为 2,原因:此隔离级别下,事务B 虽然还没有提交,但是 事务A 是可以看到被修改的结果的
  5. 事务B 提交
  6. 事务A 拿到 V2 的值为 2
  7. 事务A 提交
  8. 事务A 拿到 V3 的值为 2

2.3.2 读已提交隔离级别下

按照每一步来分析

  1. 两个事务同时开启
  2. 事务B 获取值1
  3. 事务B 将其1该为2
  4. 事务A 得到 V1 的值为 1,原因:读已提交隔离级别下,事务B 的更新再提交后才能被 A 看到,此刻还没有提交
  5. 事务B 提交
  6. 事务A 查询 V2 的值为 2,原因:事务B已经提交过了,则事务A 可以看到事务B的改动
  7. 事务A 提交
  8. 事务A 查询 V3 的值为 2

2.3.3 可重复读

用户在执行当前事务期间看到的数据前后必须是一致的。即从事务开始之前,没有外部事务进行写操作,则到该事务提交这段时间访问到的数据是一致的,只能看到自己所修改的数据。

按照每一步来分析

  1. 两个事务同时开启
  2. 事务B 获取值1
  3. 事务B 将其1该为2
  4. 事务A 得到的值为 1,原因:可重复读隔离级别下,参考上面红色文字
  5. 事务B 提交
  6. 事务A 查询 V2 得到的值为 1,原因:可重复读隔离级别下,参考上面红色文字
  7. 事务A 提交
  8. 事务A 查询 V3 得到的值为 2,原因:当前事务已经提交,可以访问比当前事务提交更早一些提交事务对数据库进行的写操作

思考:

  1. 对于上面不同的隔离级别他们是怎么做到的,原理是什么?
  2. 我们常用的 MySQL 数据库 运用的 InnoDB 存储引擎默认隔离级别是那一级别?
  3. 幻读问题到底能不能解决?

问题 2 答案:我们现在常用的InnoDB 数据库用的是 可重复读 这一隔离级别
问题 3 答案:在可重复读这一隔离级别下,只能解决部分幻读问题,不能解决全部的幻读问题。后续进行解释。


三、MVCC原理

3.1 版本链

InnoDB 存储引擎中有两个非常重要的隐藏列

  • trx_id: 一个事物每次对某条聚簇索引记录进行改动时,都会把该事务的事务 id 赋值给 trx_id 隐藏列。
  • roll_pointer: 每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到 undo 日志中。这个隐藏列就相当于一个指针,可以通过它找到该记录修改前的信息。

比如,我们创建一个 hero 表,字段有 numbernamecountry
执行SQL
select * from hero

numbernamecountry
1刘备

假如我们现在有两个事务对该条记录进行改动,分别是 trx_100trx_200

执行时间顺序事务A事务B
1BEGIN
2BEGIN
3UPDATE hero SET name=‘关羽’ WHER number =1
4UPDATE hero SET name=‘张飞’ WHER number =1
5COMMIT;
6UPDATE hero SET name=‘赵云’ WHER number =1
7UPDATE hero SET name=‘诸葛亮’ WHER number =1
8COMMIT;

上面表格中进行了4次修改的操作,并且对每次进行一次改动,都会形成一个条日志,每条日志中也都有一个 roll_pointer 属性。通过这个属性,可以将这些日志串成一个链表在这里插入图片描述
从上图中我们可以看出,新修改的指向原来,这样就形成了一个类似链表的结构。
随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,这个链表称为版本链。版本链的头节点就是当前记录的 最新值。我们之后会利用这个记录的版本链来控制并发事务访问相同记录时的行为,我们把这种机制称为 多版本并发控制,即MVCC。

3.2 ReadView

对于使用读已提交可重复读隔离级别的事务来说,都必须保证督导已经提交事务修改过的记录。也就是说假如另一个事务已经修改了记录但尚未提交,则不能直接读取最新版本的记录。最核心的问题是:需要判断版本里链中那个版本是当前事务可见的。

3.1 ReadView 包含的内容介绍

  • m_ids: 再生成 ReadView 时,当前系统中活跃的读写事务的事务 id 列表
  • min_trx_id: 在生成 ReadView 时,当前系统中活跃的读写事务中最小的事务 id;也就是 m_ids中的最小值;
  • max_trx_id: 在生成 ReadView 时,系统应该分配给下一个事务的事务 id 值。
  • creator_trx_id: 生成该 ReadView 的事务的事务 id。

有了这个 ReadView 后,在访问某条记录时,只需要按照下面的步骤来判断记录的某个版本是否可见。

  1. 如果被访问本版本的 trx_id 属性值与 ReadView 中的 creator_trx_id 值相同,意味和当前事务正在访问它自己修改过的记录,所以该版本可以被当前事务访问。
  2. 如果被访问版本的 trx_id 属性值小于 ReadView 中的 trx_id值,表明生成该版本的事务在当前事务生成 ReadView 前已经提交了,被访问的已经不活跃了,所以该版本可以被当前事务访问。
  3. 如果被访问版本的 trx_id 属性值大于或等于 ReadView 中的 max_trx_id 值,表明生成改本的事务在当前事务生成 ReadView 后开启,即事务还没有开启,所以该版本不能被当前事务所访问。
  4. 如果被访问版本的 trx_id 属性值在 ReadView 的 min_trx_id 和 max_trx_id 之间,则需要判断 trx_id 属性值是否在 m_ids 列表中。如果在,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可被访问;如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。

如果某个版本的数据对当前事务是不可见的,那就顺着版本链找到下一个版本的数据,并继续执行上面的步骤来判断记录的可见性;以此类推,知道版本链中最后一个版本。如果记录的最后一个版本也不可见,就意味着该条记录对当前事务完全不可见,查询结果就不包含该记录。

上面是非常重要的原理,不好理解,希望读者可以耐心思考专研

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

3.3.1 读已提交(READ COMMITTED)

该隔离级别下,每次读取数据前都会生成一个 ReadView

3.3.2 可重复读(REPEATABLE READ)

该隔离级别下,在第一次读取数据时生成一个 ReadView

四、为什么可重复读只能解决部分幻读问题?

4.1 前期准备

假设我们有一张表 hero,字段有 numbernamecountry,目前该表中有 3 条数据,如下表

numbernamecountry
1刘备
2赵云
3曹操

4.2 两个并发事务

4.2.1 第一种情况

执行时间顺序事务A事务B
1BEGIN
2BEGIN
3SELECT * FROM hero
4INSERT INTO hero value(5, ‘孙权’, ‘吴’)
5COMMIT;
6SELECT * FROM hero
7COMMIT;

分析

  1. 事务A 开始执行
  2. 事务B 开始执行
  3. 事务A 插入一条记录
  4. 事务A 提交事务
  5. 事务B 进行全表查询
  6. 事务B 提交事务

此时,在可重复读隔离级别下来,我们看看事务B 所查询到的数据

第一次查询结果

numbernamecountry
1刘备
2赵云
3曹操

第二次查询结果

numbernamecountry
1刘备
2赵云
3曹操

发现两次查询的结果是一样的,没有读到 事务A 新插入的数据。

4.2.2 第二种情况

执行时间顺序事务A事务B
1BEGIN
2BEGIN
3SELECT * FROM hero
4INSERT INTO hero value(5, ‘孙权’, ‘吴’)
5COMMIT;
6UPDATE hero SET country=‘吴蜀魏’ WHER number < 10
7SELECT * FROM hero
8COMMIT;

我们发现第二种情况是事务B 在执行了一次修改操作之后,再次查询。看两次查询到的数据

第一次查询结果

numbernamecountry
1刘备
2赵云
3曹操

第二次查询结果

numbernamecountry
1刘备吴蜀魏
2赵云吴蜀魏
3曹操吴蜀魏
5孙权吴蜀魏

这个时候我们发现两次查到的结果并不一样,莫名奇妙多了一条记录,出现这样的情况,读者可能也会感到疑惑,这是为什么呢?

原因:当事务A 插入一条记录之后,事务B 如果不对其进行写的操作,那么事务B 在提交之前再次查询并会出现幻读情况,如果对其他事务做了修改并且再次查询,那么就会将其他事务写的操作归到自己的事务所有,这样自己的事务查询自己所修改的数据当然是可以查出来了。由于这样的操作,我们就可以明白“在可重复读”隔离级别下只能解决部分幻读问题这句话了

五、二级索引与MVCC

通过上面的讲解,我们知道,只有在聚簇索引记录中才有 trx_idroll_pointer如果某个查询语句是使用的二级索引来查询,该如何判断可见性?

  1. 二级索引页面的 Page Header 部分有一个名为 PAGE_MAX_TRX_ID 的属性,执行增删改操作时,如果执行该操作的事物的事务 id 大于 PAGE_MAX_TRX_ID 的属性值,则将其值设置为执行操作的事务id,这就意味着 PAGE_MAX_TRX_ID 属性值设置为执行该操作的最大事务 id。
  2. 当 SELECT 语句访问某个二级索引记录时,如果 ReadView 的 min_trx_id > PAGE_MAX_TRX_ID 属性值?如果是,则说明该页面中的所有记录对该 ReadView 可见;否则就需要执行回表判断
  3. 利用二级索引记录中的主键值进行回表操作,得到对应的聚簇索引记录后在按照聚簇索引的方式,判断该可见性。

补充:

  1. 执行写操作会会生成 事务id 和 ReadView
  2. 执行读操作只会生成 ReadView

以上就是有关隔离级别以及MVCC 原理的重要知识点,很重要!!!


参考文章:

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值