对MySQL事务隔离级别及MVCC的理解笔记

写在正文前面,以下仅MySQL小白进阶
用于记录对MySQL事务隔离级别及MVCC的理解。
https://www.cnblogs.com/CodeBear/p/12710670.html
https://blog.csdn.net/qq_38538733/article/details/88902979后总结。
以上博文均来自于掘金小册:MySQL 是怎样运行的:从根儿上理解 MySQL


建表如下

    CREATE TABLE t (
        id INT PRIMARY KEY,
        name VARCHAR(80)
    ) Engine=InnoDB CHARSET=utf8mb4;

插入SQL

INSERT INTO t VALUES(1, '鸣人');

隔离级别

之前的笔记:对脏写、脏读、不可重复度、幻读的理解笔记 说过事务具备隔离性,理论上在某个事务对某个数据进行访问时,其他事务应该进行排队,当该事务提交之后,其他事务才可以继续访问这个数据。但是这样子的话对性能影响太大,所以设计数据库的大叔提出了各种隔离级别,来最大限度的提升系统并发处理事务的能力,但是这也是以牺牲一定的隔离性来达到的。
隔离级别包括:
读未提交(READ UNCOMMITTED),
读已提交(READ COMMITTED),
可重复读(REPEATABLE READ),是MySQL默认隔离级别,
串行化(SERIALIZABLE)。

读未提交(READ UNCOMMITTED)

当前隔离级别下,一个事务读到了另一个未提交事务修改过的数据。参考之前笔记中“脏读”。同时也会出现“不可重复读”,“幻读”的问题。所以是十分不安全的隔离级别。

读已提交(READ COMMITTED)

当前隔离级别下,一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值。
参考之前笔记中的“不可重复读”,举例如下

时间节点sessionAsessionB
1begin
2SELECT * FROM t WHERE id=1(这里查询结果必然是鸣人)begin
3begin >> UPDATE t SET name=‘佐助’ WHERE id=1 >> commit
4SELECT * FROM t WHERE id=1(这里查询结果是佐助)
5begin >> UPDATE t SET name=‘小樱’ WHERE id=1 >> commit
6SELECT * FROM t WHERE id=1(这里查询结果是小樱)
7

也就是当前隔离级别下,不会出现“脏读”,但是“不可重复读”和“幻读”的问题依然会出现。

可重复读(REPEATABLE READ)

当前隔离级别下,一个事务只能读到另一个已经提交的事务修改过的数据,且当第一次读过某条记录后,即使其他事务修改了该记录的值并且提交,该事务之后再读该条记录时,读到的仍是第一次读到的值,而不是每次都读到不同的数据。
依然参考之前笔记中的“不可重复读”,举例如下

时间节点sessionAsessionB
1begin
2SELECT * FROM t WHERE id=1(这里查询结果必然是鸣人)begin
3begin >> UPDATE t SET name=‘佐助’ WHERE id=1 >> commit
4SELECT * FROM t WHERE id=1(这里查询结果依然是鸣人)
5begin >> UPDATE t SET name=‘小樱’ WHERE id=1 >> commit
6SELECT * FROM t WHERE id=1(这里查询结果依然是鸣人)
7

也就是在当前隔离级别下,可以避免“脏读”,“不可重复读”两个问题,会有“幻读”问题。但是在MySQL中,此隔离级别解决了“幻读”问题(待继续研究)。

串行化(SERIALIZABLE)

当前隔离级别下,一个事务执行的时候不允许别的事务并发执行,完全串行化的读,只要存在读就禁止写,但可以同时读。
以上3种隔离级别都允许对同一条记录进行读-读、读-写、写-读的并发操作,而当前隔离级别不允许读-写、写-读的并发操作,以此消除了幻读。
依然参考之前笔记中的“不可重复读”,举例如下

时间节点sessionAsessionB
1begin
2SELECT * FROM t WHERE id=1(这里查询结果必然是鸣人)begin
3begin >> UPDATE t SET name=‘佐助’ WHERE id=1 (到这里就等待)
4SELECT * FROM t WHERE id=1(这里查询结果依然是鸣人)
5commit
6节点2的SQL修改成功
7begin
8SELECT * FROM t WHERE id=1(到这里就等待,因为sessionB还没有提交)
9commit
10节点8的SQL查询结果为佐助
11SELECT * FROM t WHERE id=1(这里再次查询结果依然是佐助)
12

总结MVCC前先了解前置概念,版本链和ReadView。

版本链

数据库将数据分为两个部分来存储,一个是数据行的额外信息(待研究),一个是真实的数据记录。
对于使用InnoDB存储引擎的表来说,在真实数据记录里的聚簇索引记录中都包含两至三个隐藏列(我对隐藏列的理解就是字段):

  • row_id,非必要。表中有PRIMARY KEY或者NOT NULL UNIQUE KEY时都不会包含row_id列,如果两者都没有,MySQL会自主添加row_id列。
  • trx_id(schema_information库中的innodb_trx表??),必要。理解为事务id,每次对某条聚簇索引记录进行改动时,都会把对应的事务id赋值给trx_id列。
    只用begin/start transaction是不会正式开启一个事务的,它们只是声明即将启动一个事务,理解为事务id=0,执行select语句也不会。只有执行insert/update/delete语句才能获得事务id。
  • roll_pointer,必要。每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。

“鸣人”这条记录图示(假设是事务50写入的):
在这里插入图片描述每次对数据进行改动,都会记录一条undo日志,每条undo日志也都有一个roll_pointer属性(INSERT操作对应的undo日志没有该属性,因为该记录并没有更早的版本)。所以这里roll_pointer写空。
实际上,roll_pointer并不是空的,理解成空的,问题也不大。(待研究)

当对该数据进行操作时(假设两个事务id分别为100和200):

时间节点trx_id=100trx_id=200
1begin
2UPDATE t SET name=‘佐助’ WHERE id=1begin
3UPDATE t SET name=‘小樱’ WHERE id=1
4commit
5UPDATE t SET name=‘纲手’ WHERE id=1
6UPDATE t SET name=‘蛇叔’ WHERE id=1
7commit

这里举例没有在两个事务中同时修改一条数据,因为涉及锁的概念。

那么上图操作产生的版本链如图:
在这里插入图片描述小结:
每次更新后,都会将旧值放到一条undo日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被roll_pointer属性连接成一个链表,这个链表就是版本链,版本链的头节点就是当前记录最新的值。

ReadView

对于READ UNCOMMITTED,直接读取到其他事务还没有提交的数据,也就是读最新的。
对于SERIALIZABLE,是用加锁的方式来访问记录。

而在剩下的READ COMMITTED和REPEATABLE READ这两个事务隔离级别,都要保证读到的数据是其他事务已经提交的,也就是不能无脑把一行数据的最新版本给读出来了,而这就需要用到版本链。
其核心问题就是:需要判断版本链中的哪个版本是当前事务可见的。为此引入ReadView的概念。
ReadView包含四个比较重要的内容:

  • m_ids:表示在生成ReadView时,系统中活跃的事务id集合。
  • min_trx_id:表示在生成ReadView时,系统中活跃的最小事务id,也就是 m_ids中的最小值。
  • max_trx_id:表示在生成ReadView时,系统应该分配给下一个事务的id。
  • creator_trx_id:表示生成该ReadView的事务id。

在访问某条记录时,按照下边的步骤判断该记录的某个版本是否可见。

//伪代码仅代表理解
if (被访问版本的trx_id == 当前ReadView的creator_trx_id) {	
	可以访问;//表明该访问版本就是当前事务生成的,故可以访问
} else {
	if (被访问版本的trx_id >= 当前ReadView的min_trx_id && 被访问版本的trx_id <= 当前ReadView的max_trx_id) {
		
		if(in_array(被访问版本的trx_id , 当前ReadView的m_ids)) {
			不可以访问;//表明生成该访问版本的事务还是活跃的,故不可以访问
		} else {
			可以访问;//表明生成该访问版本的事务已经提交,故可以访问
		}
		
	} else {
		
		if (被访问版本的trx_id < 当前ReadView的min_trx_id) {
			可以访问;//表明该访问版本在当前事务之前就已经提交了,故可以访问
		} else {//这里else指 >当前ReadView的max_trx_id
			不可以访问;//表明生成该被访问版本的事务在当前事务生成ReadView后,故不可以访问
		}
	
	}
	
}

如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本,如果最后一个版本也不可见的话,那么就意味着该条记录对该事务不可见,查询结果就不包含该记录。

READ COMMITTED级别下,每次读取数据前都生成一个ReadView

举例:

时间节点trx_id=100trx_id=200trx_id=0
1begin
2UPDATE t SET name=‘佐助’ WHERE id=1begin
3UPDATE t SET name=‘小樱’ WHERE id=1begin
4SELECT * FROM t WHERE id = 1(这里查询结果为鸣人)
5commit
6SELECT * FROM t WHERE id = 1(这里查询结果为小樱)
7UPDATE t SET name=‘纲手’ WHERE id=1
8UPDATE t SET name=‘蛇叔’ WHERE id=1
9commit
10SELECT * FROM t WHERE id = 1(这里查询结果为蛇叔)
11

分析节点4的trx_id=0的查询过程

在这里插入图片描述

  • 当前ReadView的m_ids包括[100,200,0](均活跃未提交);
  • 查询记录最新版本’小樱’的trx_id=100,100在当前m_ids列表中,根据规则,该记录版本不可见,则根据roll_pointer找下个版本;
  • 查询记录版本’佐助’,同上;
  • 查询记录版本’鸣人’的trx_id=50,小于m_ids中的最小值,则该版本可见。

分析节点6的trx_id=0的查询过程

  • 当前ReadView的m_ids包括[200,0](100已提交,200活跃未提交);
  • 查询记录最新版本’小樱’的trx_id=100,小于m_ids中的最小值,则该版本可见。

而这里也就出现了不可重复读的问题,在一个事务中,同一查询结果不一致。

分析节点10的trx_id=0的查询过程

同上节点6。一样出现不可重复读问题。

REPEATABLE READ级别下,在第一次读取数据时生成一个ReadView

依然用上面的流程举例,但结果不同:

时间节点trx_id=100trx_id=200trx_id=0
1begin
2UPDATE t SET name=‘佐助’ WHERE id=1begin
3UPDATE t SET name=‘小樱’ WHERE id=1begin
4SELECT * FROM t WHERE id = 1(这里查询结果为鸣人)
5commit
6SELECT * FROM t WHERE id = 1(这里查询结果为鸣人)
7UPDATE t SET name=‘纲手’ WHERE id=1
8UPDATE t SET name=‘蛇叔’ WHERE id=1
9commit
10SELECT * FROM t WHERE id = 1(这里查询结果为鸣人)
11

分析节点4的trx_id=0的查询过程

同上节点4。
注意,节点4查询时的ReadView的m_ids为[100,200,0]。

分析节点6的trx_id=0的查询过程

在这里插入图片描述 - 当前ReadView的m_ids依然是[100,200,0](沿用第一次查询时的ReadView,即使事务100已经提交,这里的区别很重要);

  • 查询记录最新版本’小樱’的trx_id=100,100在当前m_ids列表中,根据规则,该记录版本不可见,则根据roll_pointer找下个版本;
  • 查询记录版本’佐助’,同上;
  • 查询记录版本’鸣人’的trx_id=50,小于m_ids中的最小值,则该版本可见。

由此也就可以发现,当前隔离级别下不可重复读的问题被解决了。

分析节点10的trx_id=0的查询过程

在这里插入图片描述- 当前ReadView的m_ids依然是[100,200,0](沿用第一次查询时的ReadView,即使事务100和200都已经提交了);

  • 查询记录最新版本’蛇叔’的trx_id=200,200在当前m_ids列表中,根据规则,该记录版本不可见,则根据roll_pointer找下个版本;
  • 查询记录版本’纲手’、‘佐助’、‘小樱’,同上;
  • 查询记录版本’鸣人’的trx_id=50,小于m_ids中的最小值,则该版本可见。

同时节点6的查询分析,当前隔离级别下不可重复读的问题被解决了。

MVCC总结

MVCC(Multi-Version Concurrency Control ,多版本并发控制)指的就是在使用READ COMMITTD、REPEATABLE READ这两种隔离级别的事务在执行普通的SEELCT操作时访问记录的版本链的过程,这样子可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能。
可以理解为其中多版本就如版本链描述,而并发控制就是ReadView相关。

READ COMMITTD、REPEATABLE READ这两个隔离级别的一个很大不同就是生成ReadView的时机不同,READ COMMITTD在每一次进行普通SELECT操作前都会生成一个ReadView,而REPEATABLE READ只在第一次读取数据时生成一个ReadView,之后的查询操作都沿用这个ReadView。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值