1、可重复读是怎么实现的?
这是一道高频面试题,大多数人接触这个问题时都会被告知是在事务开始时,基于当前数据创建了一个临时视图,后面的读操作都是在这个视图上进行的。但这只是个笼统的说法或者说一个形象的比喻,没有涉及到视图的存在方式和实现原理,是应付不了面试官的。
想要回答这个问题,就必须了解MVCC的工作机制
2、MVCC维护快照视图的方法
(1)视图的逻辑存在模型
- MVCC又叫多版本控制机制,顾名思义,它是用来控制多个版本的数据的。
- 在MVCC的模型中,每一行数据都是有不同的版本的,事务在进行查询会选择对当前事务可见的数据版本,这些可见的数据版本共同组成了视图。那么,数据版本是怎么控制的呢?
(2)数据版本的控制方式
- 实际上是通过事务的ID来控制的,每个事务开始时,都会跟系统申请一个id,叫做transaction id。这个id是严格递增的,确保不会重复。
- 当事务对一行记录进行修改时,就会生成一条新的版本记录,并将事务的transaction id作为该记录的版本号。
那么旧的版本记录去哪里了呢?
(3)数据版本的实际存在方式
- 事实上,旧的版本记录不是直接以物理形式存在的,它是通过undo log(回滚日志)与最新的记录版本运算得出的。事务查询时,若记录的最新版本对当前事务不可见,就会进行运算回滚,然后检查回滚后的记录版本对当前事务是否可见,不可见则回滚到可见为止。
那么事务是如何判断数据版本对自己是否可见呢?
3、MVCC的可见版本判断方法
在事务开始时,MVCC会生成一个数组,用于记录当前系统中活跃的事务ID,所谓活跃,即已经开始但未提交的事务。数组中的最小版本号称之为低水位, 最大版本号 + 1称之为高水位。而判断数据版本是否可见就是根据这三个数据来进行的
- 若数据版本号小于低水位,低水位是本事务开始时未提交事务中id最小的事务,在此之前的事务必定已经提交,说明该数据版本是在本事务开始前已经提交的,对本事务可见
- 若数据版本号大于或等于高水位,则说明该数据版本是晚于本事务开始的事务生成的,对本事务不可见
- 若数据版本号介于低水位和高水位之间,那么则有两种情况
- 若该数据版本号位于数组中,则说明该数据在本事务开始前还未提交,不可见
- 若该数据版本号不在数组中,那么说明该数据时在本事务开始前提交的,可见
- 若数据版本号是本事务,那么当然是可见的。
前文提到了事务开始概念,如果使用的是begin/start transaction开启的事务,那么事务开始时机是在第一次select(快照读)之后,而采用start transaction with consistent snapshot则会立即开启事务。
而在RC(提交读)的隔离级别下,就是通过操作前重新生成新视图,也就是不断更新高低水位和活跃数组来实现提交读的
4、写同步问题
前文提到在RR(可重复读)级别对未提交的数据版本不可见,那么倘若事务需要修改记录,就有可能不是在最新版本的基础上进行修改,那么其他事务的修改不就失效了吗?
实际上,在涉及到修改操作时,MySQL是先读再写,这个读不同于之前的快照读,而是获取最新版本数据的当前读。进行当前读需要对记录加上写锁,写锁是一个互斥锁,持有锁的事务在提交前都不会释放锁,后续申请锁的事务会陷入阻塞,以此来保证写操作的一致性
除了进行写操作,也可以通过以下方式对记录加上读锁或写锁进行当前读:
- select … from … lock in share mode;
- select … from … for update;
前者是加上读锁,读锁可共享,如果加锁的记录只有读锁则可以获取到最新已提交的数据版本,但如果加读锁的记录上有写锁,则会阻塞;
后者是加上写锁,只有在加锁记录没有读写锁的情况下才能加锁成功,会获取到已提交的最新数据版本。记录被加上写锁时,其他事务的当前读(加读锁或写锁读)都会被阻塞。
也就是说,可重复读是在快照读的维度上进行实现的。对于当前读,需要获取到最新的版本数据,会绕开版本控制,已经不在可重复读的范畴内了
如果换一个角度思考,当前事务在进行当前读时会进行加锁,加锁成功后其他事务无法进行修改,多次当前读的结果也是一样的,也算是一种可重复读吧