MySQL 事务的 ReadView

MySQL一共四种隔离级别,分别是Read Uncommitted、Read Committed、Repeatable Read 、Serialize;其中 Repeatable Read 是默认的事务隔离级别。每种隔离级别背后的实现机制是什么呢?今天写Read view探索一下隔离级别的奥秘。
MVCC 是多版本并发控制技术,是通过保存数据在某个时间点的快照来实现的。在Innodb中RC和RR的隔离级别,MVCC 是根据undo log 和 consistent Read view来实现的。
**版本链**
Innodb 有两个隐藏字段 trx_id 事务id和roll_pointer 回滚指针,指向上一事务版本的指针。
InnoDB里面每个事务有一个唯一的事务ID,叫作transaction id,它是在事务开始的时候向InnoDB的事务系统申请的,是按申请顺序严格递增的。
每行数据也都是有多个版本的,每次事务更新数据的时候,都会生成一个新的数据版本,并且把transaction id赋值给这个数据版本的事务ID,记为row trx_id。同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它。
也就是说,数据表中的一行记录,其实可能有多个版本(row),每个版本有自己的row trx_id
这是一个隐藏列,还有另外一个roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。
undo log的回滚机制也是依靠这个版本链,每次对记录进行改动,都会记录一条undo日志,每条undo日志也都有一个roll_pointer属性(INSERT操作对应的undo日志没有该属性,因为该记录并没有更早的版本),可以将这些undo日志都连起来,串成一个链表,所以现在的情况就像下图一样
Read View 就是一个保存事务ID的list列表,记录是的本事务执行时,MySQL还有哪些事务在执行。RC是每次执行读SQL语句的时候都创建一个Read View,而RR是在事务启动的时候创建一个Read View。
按照可重复读的定义,一个事务启动的时候,能够看到所有已经提交的事务结果。但是之后,这个事务执行期间,其他事务的更新对它不可见。
因此,一个事务只需要在启动的时候声明说,“以我启动的时刻为准,如果一个数据版本是在我启动之前生成的,就认;如果是我启动以后才生成的,我就不认,我必须要找到它的上一个版本”。
当然,如果“上一个版本”也不可见,那就得继续往前找。还有,如果是这个事务自己更新的数据,它自己还是要认的
在实现上, InnoDB为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务ID。“活跃”指的就是,启动了但还没提交。
数组里面事务ID的最小值记为低水位,当前系统里面已经创建过的事务ID的最大值加1记为高水位。
这个视图数组和高水位,就组成了当前事务的一致性视图(read-view)。而数据版本的可见性规则,就是基于数据的row trx_id和这个一致性视图的对比结果得到的。
ReadView中主要包含4个比较重要的内容:
活跃”指的就是,启动了但还没提交。
  • m_ids:表示在生成ReadView时当前系统中活跃的所有事务的事务id列表。
  • min_trx_id:表示在生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值。
  • max_trx_id:表示生成ReadView时系统中应该分配给下一个事务的id值。
  • creator_trx_id:表示生成该ReadView的事务的事务id
数据版本的可见性规则:一个数据版本的row trx_id有以下几种可能:
1 被访问的版本的trx_id属性值与ReadView中的creator_trx_id相等,意味着这是自己修改的记住,所以得认。
2 如果被访问版本trx_id小于Readview中的min_trx_id ,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问,这种也得认。
3 如果被访问版本的trx_id属性值大于ReadView中的max_trx_id值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问,这种不认
说完了等于,小于和大于。还有一种就是介于当前ReadView的min_trx_id和max_trx_id之间,那就要判断一下trx_id值大小是不是在m_ids中,如果在,说明创建ReadView时候生成该版本的事务还是活跃的,该版本不可以被访问;

如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。(这条我觉的是对RC来说的,因为RR 只生成一次ReadView不会出现这种情况)

举例:事务id列表【99,100,101,102】
RC隔离级别的时候,起初mids[99,100,101,102],当编号100第二次生成Read View时,101这个事务提交了,此时mids[99,100,102] 101 介于 99和102之间,但是又不在m_ids里面。所以101版本的修改对100来说是可见的。
当前读 current read
读取的是最新版本, 并且对读取的记录加锁, 阻塞其他事务同时改动相同记录,避免出现安全问题。当前读会更新当前版本的row trx_id。
问题:既然RR级别的readview在一开始就生成了,为什么还会出现幻读现象?

select * from T where id=1 for update是进行当前读的操作,他会重新从数据库去加载当前的最新的数据,每执行一次加载一次,如果在此时,另外一个事务为数据库添加了一个事务,再进行查询,会发现查询的数据与之前相比多了或者少了,这也就是幻读现象

如果A事务如果进行了快照读,然后通过B事务对数据就行增删,然后紧接着A事务进行当前读操作,两次读取数据不一致,不能算作幻读,因为幻读定义是同一个select语句,快照读和当前读的查询语句是不一样的

总结:
在标准的RR下并没有彻底解决幻读,但是在Mysql的innodb引擎中彻底解决了
innodb通过 Next-Key lock解决的幻读问题,其实也就是阻塞串行化了
不能把快照读和当前读在一个事务中进行比较是否出现幻读,两者不是同一个select,不满足幻读的官方定义
参考链接:https://blog.csdn.net/qq_45225798/article/details/119978841

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注