不可重复读问题
基于上面的操作,先将 trx_id = 100 事务提交,然后再到 trx_id =120 事务更新表中 technology_column.id 为 1 的记录,也就是执行如下 SQL 语句:
Trx_id-100:Commit; Trx_id-120:update technology_column set category_name ='分布式' where id = 1; Trx_id-120:update technology_column set category_name ='Linux' where id = 1;
此时,technology_column 表中 id=1 记录的版本链如下图所示:
接着使用 READ COMMITTED 隔离级别事务,在 Trx_id-120 begin;
语句后查询的数据为 Spring
,而在 Trx_id-100:Commit;
语句执行之后,Trx_id-120:Commit;
语句执行之前,使用 Trx_id 为 120 的事务,查询 id=1 记录,此时查询到的数据为 Redis,整个的执行过程如下:
- 在执行 SELECT 语句时又会单独生成一个 ReadView
- ReadView 活跃集合 m_ids 内容为 120,min_trx_id 为 120,max_trx_id 为 121,creator_id 为 0
Trx_id 为 100 的事务已经提交了,所以再次生成 ReadView 快照时就没有它了
- 接着从版本链中挑选可见的记录,从图中可以看出,最新版本的 category_name 内容为 Linux,该版本的 Trx_id 值为 120,在 m_ids 集合内,所以不符合可见性要求
- 通过 Roll_ptr 指针跳到下一个版本中,下一个版本 category_name 内容为 分布式,该版本的 Trx_id 值为 120,在 m_ids 集合内,也不符合可见性要求
- 通过 Roll_ptr 指针跳到下一个版本中,下一个版本 category_name 内容为 Redis,该版本的 Trx_id 为 100,小于 ReadView 中 min_trx_id 值 120,所以这个版本的内容是符合可见性要求的,最后返回给用户的版本就是这条 category_name 内容为 Redis 的数据
- 此时,在 Trx_id 为 120 的事务执行期间,就发生了两次查询数据不一致的问题,这就是不可重复读问题,由此说明,
READ COMMITTED 读已提交隔离级别事务避免不了此问题的发生
依此类推,若之后 Trx_id 为 120 的事务也提交了,再次使用 READ COMMITTED 隔离级别事务,查询 technology_column.id 为 1 的记录时,得到的结果就是 Linux,具体流程类似于上面
通过 m_ids(活跃事务集合)、min_trx_id(最小事务 id) 结合隔离级别的特性,来比对版本链的记录是否符合可见性要求,而读已提交是在每次执行 SELECT 语句时都会生成 ReadView 视图快照.
REPEATABLE READ
REPEATABLE READ 隔离级别的事务只有在第一次读取数据时才会生成一个 ReadView,之后的查询就不会重复生成了
比如:现有系统中有两个事务 > Trx_id 100、120 在执行,事务 > Trx_id 100、120 SQL 语句如下:
Trx_id-100、120 Begin; Trx_id-100、120:select * from technology_column where id = 1; Trx_id-100:update technology_column set category_name ='MySQL' where id = 1; Trx_id-100:update technology_column set category_name ='Redis' where id = 1; Trx_id-100:select * from technology_column where id = 1; Trx_id-100:Commit; Trx_id-120:update technology_column set category_name ='分布式' where id = 1; Trx_id-120:update technology_column set category_name ='Linux' where id = 1; Trx_id-120:select * from technology_column where id = 1; Trx_id-120:Commit;
不可重复读问题
在事务 100、120 执行前都会先生成 ReadView 读取视图,也就是它们读取到的 category_name 内容都是 Spring,在前面介绍过,READ COMMITTED 读已提交隔离级别会在 Trx_id 为 120 的事务执行期间发生不可重复读问题
,所以在这里主要分析 REPEATABLE READ 可重复读隔离级别是如何解决此问题的, 它在执行事务时对应的版本链表如下:
Trx_id-120
在 Trx_id 为 120 第一次 SELECT 语句执行以后
,生成了 ReadView 读取视图快照
ReadView 内容:m_ids 活跃事务集合为 100、120,min_trx_id 为 121,creator_trx_id 为 0
因为当前事务隔离级别为 REPEATABLE READ,所以在 Trx_id 为 120 的事务执行期间会直接复用上面所生成的 ReadView 快照信息,也就是前后两次 SELECT 查询语句执行后的结果都是一致的,读取到的 category_name 内容都是 Spring,这就是可重复读解决不可重复读问题的核心所在
因为它在事务执行期间,一直都是使用的第一次生成的 ReadView,自然而然也不会发生脏读问题,不会读取到其他事务已提交的数据信息
总结一下 ReadView 比较规则,如下:
- 若被访问版本的 Trx_id 属性值与 ReadView 中 creator_trx_id 值相同,那么就意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务所访问
- 若被访问版本的 Trx_id 属性值小于 ReadView 中 min_trx_id 值,表明生成该版本的事务在当前事务生成 ReadView 之前已经提交,所以该版本可以被当前事务所访问
- 若被访问的版本 Trx_id 属性值大于或等于 ReadView 中 max_trx_id 值,表明生成该版本的事务在当前事务生成 ReadView 之后才开启,所以该版本不可以被当前事务所访问
- 若被访问版本的 Trx_id 属性值在 ReadView 中 min_trx_id、max_trx_id 之间(min_trx_id < trx_id < max_trx_id)那就需要判断一下 trx_id 属性值是否在 m_ids 活跃事务集合中;如果在的话,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被其他事务所访问;如果不在的话,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被其他事务所访问
幻读问题
REPEATABLE READ 隔离级别在 MVCC 机制下可以解决不可重复读问题,也可以在一定程度下解决幻读问题
幻读问题:一个事务在某个相同条件多次读取记录时,后读取时读到了之前没有读到的记录,而这个记录来自于另外一个事务添加的新记录
比如:在 REPEATABLE READ 隔离级别下,事务 T1 先通过某个搜索条件读取到多条记录,然后事务 T2 插入一条符合这个搜索条件的记录并提交完成,此时 T1 再次通过这个搜索条件执行查询,结果如下:无论事务 T2 比 事务 T1 是否先开启,事务 T1 都是看不到 T2 的提交信息的,因为在 REPEATABLE READ 隔离级别下,只会在第一次查询时才生成 ReadView 读取视图快照,后续的查询会延续使用第一次生成的 ReadView 信息,由此可见,REPEATABLE READ 可以在一定程序下解决幻读问题的发生
但在某些情况下,REPEATABLE READ 隔离级别,InnoDB MVCC 会发生幻读问题,结合案例如下:
首先在事务 T1 中执行查询语句,如下:
select * from technology_column where id between 2 and 3;
此时 technology_column 表中只有一条数据,id 为 2、3 的数据并未存在;接着我们在事务 T2 中执行 INSERT 语句,如下图:
通过以上语句,在 technology_column 表中插入了一条 id 为 2 的数据,此时,再回到事务 T1 执行如下语句:
update technology_column set category_name = 'RocketMQ' where id = 2; select * from technology_column where id between 2 and 3;
执行结果如下:
很明显事务 T1 出现了幻读现象,在 REPEATABLE READ 隔离级别下,事务 T1 第一次执行普通的 SELECT 语句时生成了一个 ReadView(但此时版本链表中没有生成的对应的条目)之后 T2 事务向表中新插入一条记录并提交,然后 T1 事务执行了 Update 语句;ReadView 并不能阻止事务 T1 执行 UPDATE 或 DELETE 语句来改动这个新插入的记录
,但这样一来,这条新记录的 Trx_id 隐藏列的值就变成了事务 T1 的事务 id
之后事务 T1 再次使用普通 SELECT 语句去查询这条记录时就可以看到了,也可以把这条记录返回给客户端,因为这种特殊现象的存在,所以 REPEATABLE READ 可重复隔离级别不能完全避免幻读问题的发生
事务 T1 第一次读是空的情况,事务 T2 新增了这条数据,事务 T1 在自己事务中对这条数据进行了修改
小结
所谓的 MVCC(Multi-Version Concurrency Control)多版本并发控制,指的就是在使用 READ COMMITTED、REPEATABLE READ 这两种隔离级别事务时,执行普通 SELECT 操作时访问数据的版本链过程,这样子可以使不同的事务读写、写读操作并发执行,从而提高系统的性能
READ COMMITTED、REPEATABLE READ 这两种隔离级别事务一个最大不同:生成 ReadView 时机不同,READ COMMITTED 在每次进行普通 SELECT 操作时都会生成一个 ReadView,而 REPEATABLE READ 只有在第一次进行普通 SELECT 操作时生成一个 ReadView,之后的查询操作都重复使用这个 ReadView 即可
,从而基本上可以避免幻读问题的发生,但如果第一次读 ReadView 是空数据的情况下 > 某些场景则无法避免幻读的发生
最后,所谓的 MVCC 只是在我们进行普通 SELECT 查询时才生效,对于锁定读就是不普通的查询,所以它就无法让我们的 MVCC 机制发挥作用了.
总结
该篇博文,简而言之说明了 MySQL 中事务四大特性,详细阐述了由于四种隔离级别下各自会产生什么样的问题,从实战方面进行演示了 MySQL 基于事务上的一些基本操作,最重要的是讲述了 InnoDB 引擎下的 MVCC 机制,有提及它的版本链、ReadView 以及其下一些比较重要的概念,最后,重点说明了 READ COMMITTED 读已提交、REPEATABLE READ 可重复读,这两种隔离级别会是如何解决一些并发问题以及会在什么样的场景下发生一些异常问题。希望您能够喜欢,能帮助到你是我最大的快乐!
另外,博主有专门讲解 Spring 事务是如何结合 MySQL 一起应用的,博文链接如下:
Spring 事务传播机制、隔离级别以及事务执行流程源码结合案例分析
如果觉得博文不错,关注我 vnjohn,后续会有更多实战、源码、架构干货分享!
推荐专栏:Spring、MySQL,订阅一波不再迷路
大家的「关注❤️ + 点赞👍 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下文见!