mysql事务隔离级别实现原理全面解读?

2022-10-17 修正版~

修正内容(工作后修正的内容):

  1. 可重复度没有解决幻读,只是极大程度避免了快照读下会发生的幻读情况
  2. 增加 Mvcc Readview、版本控制链条内容的诠释,帮助读者更好的理解可重复度与幻读的关系
  3. 增加实验,验证RR隔离级别下,幻读还是有可能发生的,以及诠释发生的原因

2023-02-16 新增~

更新点:结合自己阅读源码的经验,新增面试专栏,后续会一直更新,如果回答的造成了误解,望斧正。看到并采纳会及时修成。

夺命6连call,本文高能全是干货!!!!

  • 天天用事务可是你知道事务是怎么来实现的吗?
  • 什么叫做事务?事务是什么?
  • mysql中的锁了解嘛?mysql中有哪些锁?mysql中的锁和事务有什么联系?
  • 事务有什么特性呢?事务隔离级别有哪几种?不同事务隔离级别解决了什么问题?
  • spring中的事务与mysql中的事务有什么区别?
  • 开启事务的方式有哪几种?

莫慌本文一一来探究~~~~~来自大三时候码的文章

什么叫做事务?事务是什么?

  • 事务:
    1. java硬编码角度:保证代码片段伪原子性执行的一种手段
    2. sql角度:保证sql语句伪原子执行的一种手段

事务有什么特性?(基本概念不过多解读)

  • 原子性、持久性、一致性、 隔离性

事务隔离级别有哪些?(基本概念不过多解读)

  • 读已经提交、读未提交、可重复读(mysql默认隔离级别)、串行化

不同事务隔离级别解决了什么问题?(先搞清楚有什么问题!下文着重有研究)

  • 事务能解决的问题
    1. 脏读:一个事务读取到另一个事务未提交的数据
    2. 幻读:情况一:开启一个事务,第一次查 name =1 的数据查不到,第二次查到了。情况二:开启一个事务:第一次查 name =1 的数据只有一条,第二次查到了多条。情况三:开启一个事务:第一次查所有数据只有一条,第二次查到了一条以上的数据
    3. 不可重复读:一个事务内,对于同一条数据的读取,读取 内容 前后不一致(mvcc解决了)
  • 读未提交:啥问题都没解决,不愧是地主家的傻儿子啊💩
  • 读已提交:解决了脏读
  • 可重复读:极大的避免了 当前读 下幻读的发生、解决了脏读、不可重复读
  • 串行化:解决了幻读、脏读、不可重复读

可能很多读者很疑惑,到底可重复度解决了幻读还是没解决呢?我在刚入坑mysql的时候也为此疑惑过,毕竟当初也是看着网上的教程一步步走过来的(网上的资源鱼龙混杂)。要搞明白这个问题首先就要知道什么是幻读?如上事务能解决的问题二

为什么说可重复读隔离级别下极大的避免当前读下幻读的发生、解决了不可重复读呢?(严谨点来说是这一切都是针对mysql下的innodb存储引擎来说的)

解决不可重复读演示

先后开启事务一、事务二,事务一查询 id 为 3 的数据,查到结果如下图一,然后切换事务二查 id 为 3 的数据,查到的结果和下图一一致,紧接着事务一执行下图一中的更新操作,并且提交事务一,事务二接着查 id 为 3 的数据,查到的结果依然和下图一一致。

事务一

在这里插入图片描述
事务二

在这里插入图片描述

  • 剖析上述图片是如何:解决不可重复读的
    1. 可重复读隔离级别是基于MVCC(版本控制链)解决了不可重复读的问题,而所谓的MVCC即版本控制链,可以这么来理解这个版本控制链:某条数据的各个历史版本的链条,且内部根据修改时间排序。
    2. 可重复读只会在第一次 select 的时候生成一个 Readview,后续的快照读,都要做判断,是否满足 Readview 可见性,满足才读取反之不读取,这其中的Readview 算法保证了可重复读,解决了不可重复读的问题

不知道读者思考过:快照读、拍快照 Readview 是个什么东西?其实不管是什么高深抽象的名字,在开发上一定都是有他自己的一套运作逻辑的,其实 Readview 就是一个类,里面装载了:Readview 生成时刻的活跃事务id列表、Readview 生成时刻的活跃事务最小id、Readview 生成时刻的已出现过的事务id+1、生成 Readview 的事务id,然后根据这些条件,去版本控制链上依次去做条件判断,找出当前事务可见的数据

Readview可见性算法

先来说下版本控制链可以比作是一个 List ,由不同事务按照先后顺序修改同一条数据形成的记录依次链接组成,而且每一条记录都有一个事务id对应,表明此记录是哪个事务修改的。

LinkedList<Object>

版本控制链 里面装载了一个个相同 id 的行数据(内含了一些隐藏列,譬如事务 id 等),当我们开启事务第一次快照读:查某个 id 或者 id 区间的数据的时候,就根据 Readview 做条件判断然后从版本控制链中取得对当前事务可见得数据,逻辑如下
1:获取版本控制链条中最新数据的事务id,与 Readview 中最小活跃事务 id 逐个进行比较,如果小于,则这条数据对当前事务可见。
2:如果大于等于,会接着与 Readview 中的出现过的最大事务 id+1 进行比较,如果大于等于,那么这条数据对当前事务不可见(说明生成 Readview 之前是没有这个版本的数据的),如果小于的话,判断 Readview 中最新的这条数据有无 commit(Readview 中维护着一个活跃事务 id 的数组(排自己),活跃事务数据包含此事务 id就表示未 commit,反之 commit),commit 了的就是可见,没提交就是不可见。

规避幻读的演示及解读

表初始数据

在这里插入图片描述
事务一

start TRANSACTION;
INSERT INTO `xiaomi`.`goods` (`id`, `name`, `price`, `stock`, `goods_type_id`, `remark`, `version`) VALUES (3, '3', 3, 3, 3, '3', 3);
UPDATE `xiaomi`.`goods` SET `name` = '2', `price` = 2, `stock` = 2, `goods_type_id` = 2, `remark` = '2', `version` = 1 WHERE `id` = 1;
commit;

事务二

start TRANSACTION;
SELECT * from goods;
commit;

先后开启事务一、事务二,事务一中可以查到的数据(id=1),事务二可以查到的数据(id=1) ,然后事务一更新 id =1 的数据,且插入 id =3 的数据,并且提交事务,事务二接着查数据,执行多少遍结果都是只能查到(id=1)的数据。根本原因就是 快照读在第一次 select 的时候会生成一个 Readview 如下:

  1. 活跃事务id列表:1
  2. 活跃事务最小id:1
  3. 生成 Readview 事务id:2
  4. 出现过的最大事务id+1:2+1=3

直到事务一 commit 然后有如下版本控制链如下:

id为1的版本控制链条
在这里插入图片描述
id为3的版本控制链条
在这里插入图片描述

查询id为1的数据,Readview会做出如下判断:

  1. 最新数据事务id:1 >= Readview 中最小活跃事务id:1
  2. 最新数据事务id:1 <= Readview 中最大事务id+1:2+1=3
  3. Readview 中活跃事务id数组:1 包含最新数据事务id:1(说明在事务二在生成 Readview 的时候,最新的这条数据还未 commit,为了防止幻读,最新的这条数据此时对事务二就不可见)

特殊情况下发生的幻读

表初始数据

在这里插入图片描述

事务一

start TRANSACTION;
INSERT INTO `xiaomi`.`goods` (`id`, `name`, `price`, `stock`, `goods_type_id`, `remark`, `version`) VALUES (3, '3', 3, 3, 3, '3', 3);
commit;

事务二

start TRANSACTION;
SELECT * from goods;
UPDATE `xiaomi`.`goods` SET `name` = '4', `price` = 4, `stock` = 4, `goods_type_id` = 4, `remark` = '4', `version` = 1 WHERE `id` = 3;
commit;

先后开启事务一、事务二,事务二查所有数据,查到只有id =1 的数据,并且生成 Readview,然后事务一插入id = 3的数据并且commit,事务二预判了你的预判,更新id = 3 的数据,然后再次查数据可以查到 id = 3 的数据了。

此时的版本控制链为:根据 Readview 可见性算法,不难计算出结论,id=3 的数据此时事务二也是可以查到的

在这里插入图片描述

探究完了幻读,顺便提一嘴mysql中的锁机制,mysql中的锁有很多大体如下:

innodb使用的锁类型:

  • 意向锁是一把表级锁
  • 意向排他锁:IX
    1. 作用:获取X锁之前先尝试获取IX锁,获取成功才能获取X锁
  • 意向共享锁:IS
    1. 作用:获取X锁之前先尝试获取IS锁或者更强的锁,获取成功才能获取S锁

在这里插入图片描述

  • 读锁(S):lock in share mode

    1. 代码:select * from user where id = 1 lock in share mode;
    2. 解释:锁定id = 1的这一行数据,在锁释放之前,别人不能修改id = 1 的这行数据,但是能读取这行数据
  • 写锁(X):for update (根据使用场景可以细分如下)

    1. 记录锁(record lock):select * from user where id = 1 for update;锁定id = 1这行数据,阻止别人修改、删除更新此数据行。效果相当于给行上了一把锁,因此有人称之为行锁!!!!!
      • 误区:网上老有人说什么mysql有行锁,有个鸡儿行锁,还有说mysql索引是b树,b个鸡儿的树,明明是b树的改造版,mysql只是遵从规范那样定义的!还是看官网靠谱啊。官网定义如下
        在这里插入图片描述在这里插入图片描述
    1. 间隙锁(GAP):锁定数据之间的间隙(针对于查询加了非唯一索引的列for update,间隙锁锁住范围下文详解)
      在这里插入图片描述
    2. next-key lock: 记录锁(相当于行锁)+间隙锁的组合
      在这里插入图片描述
    3. 插入意图锁(Insert Intention Locks)
      • 本质:插入数据行位置的一把间隙锁
      • insert语句执行原理分析:获取插入数据行安放位置的Insert Intention Locks,接着设置插入数据行的X锁

官网的原话,我用浏览器翻译了一遍。出处链接在这里插入图片描述

select * from user where id = 1;快照读可以读取任何加了锁的数据

延伸出来的锁称呼

  • 乐观锁:查询使用普通sql、更新加版本号条件判断是否能更新(表层面实现控制并发,可以理解为轻量级锁)
  • 表锁:锁定整个表的数据(针对于未加索引的列for update,此时锁住整个表)
  • 悲观锁:在sql层面,查询使用for update(sql层面限制,可以理解为一把重量级锁)
  • 行锁:X,REC_NOT_GAP,本质是记录锁

读锁演示:
在这里插入图片描述

行锁演示
在这里插入图片描述

间隙锁演示
数据表user:id:主键、name:普通索引、age:未加索引的普通字段
在这里插入图片描述

开启事务一执行如下sql
在这里插入图片描述

开启事务二执行如下sql
在这里插入图片描述

我们发现了一个很有意思的现象,就是name在[c,g)的数据都被锁住了插入不进去了,这就是避免发生幻读的本质啊,要充分理解为什么mysql这么设计,必须要有对索引有一定的了解才行,不了解mysql索引的读者可以看这篇文章精通mysql索引,我这里还是多补充几个概念吧:

  • 当前读:读加锁,delete、update、insert都属于当前读~
  • 快照读:读不加锁,普通的select操作

我用b+树生成工具依据name索引构建了如下一颗b+树(b+树实际上不是这样的,mysql的b+树叶子节点是双向链表,这也是一个坑!)
在这里插入图片描述

首先我们要知道b+树的一大特性就是排好序的,那么无论我们插入多少条name = "e"的数据,这个辅助索引上的e的区间永远介于c到g之间
在这里插入图片描述

  • 幻读和锁住[c,g)区间的元素有什么关系?
    1. select * from user where name = “e” for update;
    2. 当有多条name = "e"的数据存在的时候,这多条数据必然是存在 c于g之间的,你说为啥锁住这些地方呢?还不是为了防止幻读吗!让你插都插不进去
  • 为什么锁住的区间是[c,g)而不是(c,g)、(c,g]、[c,g]呢?
    1. 其实这个和我们创建的索引有关,mysql创建索引默认是ASC(升序)
    2. 证明:下文详解
  • 很大程度上避免发生幻读是利用了什么类型的锁?
    1. 锁了间隙又锁住了行,依靠next-key lock极大程度上避免了幻读的发生

摘抄官网的原话:幻读的避免依靠next-key lock实现的
在这里插入图片描述

间隙锁区间详解

我现在喜欢把知识学透学精,知其然知其所以然感觉更有味道!夺命call来袭

  • 间隙锁是加在谁身上?
    1. 查看锁信息(mysql8):select * from performance_schema.data_locks;
    2. 执行select name,age from user where name = “e” for update;加锁情况如下
    3. g加上GAP、X组合锁,e数据对应的主键id被加上了X、X,REC_NOT_GAP锁、e被加上X锁、整个表加上IX锁
      在这里插入图片描述

现阶段的我还不能完全理解LOCK_MODE中的组合锁是啥意思,贴一段我阅读官方文档以及实操的心得吧

  • X:锁行,可以理解为行锁
  • X,REC_NOT_GAP:锁行不包括间隙,可以理解为作用与主键的行锁
  • X,GAP:锁行以及间隙 、可以理解为next-key lock
  • X,GAP对应的(‘g’, 187):说明g之前存在间隙锁、行锁,g只是一个上限值

在这里插入图片描述

  • 什么时候加间隙锁?什么时候加行锁?什么时候加表锁?
    1. 当前读使用的是普通索引查询,那么加间隙锁
    2. 当前读使用的是主键索引、唯一索引查询,那么加行锁
    3. 当前读使用压根就没有索引的字段查询,锁表
  • 为什么主键加的是X,REC_NOT_GAP锁?
    1. 主键索引具有唯一性,你总不能插入俩条主键相同的数据入库吧,主键查询压根就不会出现幻读的问题产生好吧。
    2. 加行锁是为了别的事务修改此条数据,造成同一事务俩次当前读,读取到同一条数据内容不一致的情况出现。
    3. X,REC_NOT_GAP:个人觉着可以理解为就是行锁
  • 为什么整个表加一把意向排他锁(IX)?
    1. 加意向排他锁(IX)的目的在于解决行锁与表锁的冲突问题的
    2. 当事务一向一个表中的某一行数据加上一把行锁,事务二想往该表加一个表锁,那么此时表锁是加入不进去的。事务二在进行加表锁之前会先判断表是否有IX锁,如果有那么表示此表有数据已经上了行锁,需要等待行锁释放,表锁才能添加成功。IX的好处就是不用在逐行的去扫描到底表中是否有添加了行锁的记录。

是不是现在满脑子问号呢?你们最最最最想知道的幻读是怎么解决的来来了😬,为什么锁住的区间是[c,g)呢?开始步入正题。我们要防止c、g之间插入e,很简单把c到e之间的间隙锁住、以及把中间的e数据加一把记录锁就好了。现在的问题就是能插入g,而不能插入c?

  • 原因很简单:建索引的时候默认是ASC(升序排序)的,如果可以插入c数据,那么必然插入的c数据位置介于c - g之间,插入成功我们把c数据改成e,那么c - g这个区间不就是有俩条e数据了吗。而插入g,插入的位置在c - g这个范围之外
  • 那么还有人要问了:我插入g,接着修改g为e不行吗?
    1. 小伙汁你很优秀啊very good这个问题,而这又涉及到update一条语句的加锁过程了
  • update操作加锁过程?
    1. 如果修改前后此行数据前后存储空间发生了改变,那么会先添加X锁给记录,接着删除记录,然后insert新记录,这个记录还不是要insert?insert到哪?还不是添加到c - g这个间隙嘛!所以说update g改成e压根就不会执行成功,这些间隙都被锁住了,压根insert不进去呀

在这里插入图片描述
在这里插入图片描述

如果建立索引是DESC那么锁住的间隙,用区间表示就是(c,g]。

InnoDB中由不同SQL语句设置的锁:
https://dev.mysql.com/doc/refman/8.0/en/innodb-locks-set.html

mysql幻读官方解释:
https://dev.mysql.com/doc/refman/8.0/en/innodb-next-key-locking.html

mysql锁官方解释:
https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html

mysql data_lock表官方解释:
https://dev.mysql.com/doc/refman/8.0/en/performance-schema-data-locks-table.html

思考

什么情况下,可重复度隔离级别会发生幻读呢?后续更新~~~~~

面试专栏

可重复读与读已提交本质区别是什么?

可重复读只会在事务开启后的第一次 select 的时候生成 readview ,而 读已提交在事务开启后的每次 select 都会生成最新的一个 readview ,同时由于遵循 readview 可见性算法,未提交的数据 ,在读已提交隔离级别下是读取不到的。

隔离级别的作用?(对四大隔离级别作用一一阐述一下就好了)

为了防止幻读、脏读、不可重复读的这些情况的发生,因此产生了隔离级别其中

  • 读未提交:啥都没解决
  • 读已提交:只能读取另一个事务已提交的数据,杜绝了脏读
  • 可重复读:极大的避免了幻读、杜绝了脏读、不可重复读
  • 串行化:解决了幻读、脏读、不可重复读

----------其他关于事务的面试问题,欢迎大家留言--------

小咸鱼的技术窝

关注不迷路,日后分享更多技术干货,B站、CSDN、微信公众号同名,名称都是(小咸鱼的技术窝)更多详情在主页
在这里插入图片描述