问
MySQL的多版本并发控制(MVCC)到底是用来干什么的?
可重复读隔离级别下,是如何保证在事务中多次读取的数据的一致性的?
读提交的隔离级别能否保证事务中多次读取的数据是一致的?
答
这些问题一直困扰很多人,今天就要掰开了,揉碎了,彻彻底底地讲清楚。开整!
正文
先说一个结论,然后再逐步的分析是如何得出这个结论的。
MySQL的MVCC(多版本并发控制)是通过ReadView的字段和 聚簇索引中的两个隐藏字段进行比对来实现的。
对于 【读提交】 和 【可重复读】 两种隔离级别来说,快照读(普通的select)是通过ReadView的字段和 聚簇索引中的两个隐藏字段进行比对 来实现多版本控制的。它们之间的区别在于创建ReadView的时机不同
读提交隔离级别,在事务执行期间,每个select前都会生成一个新的 ReadView,也就意味着在事务期间多次读取同一条数据,前后两次读取的数据可能会出现不一致。因为,在这期间有其他事务修改了这条数据,并提交了事务。
可重复读隔离级别,在事务启动时生成一个ReadView,并且在整个事务执行期间都只使用这一个 ReadView,这样就保证了在事务期间读取到的数据都是事务启动前的记录。
Read View 在 MVCC 里如何工作的?
需要了解两个知识:
ReadView 中四个字段的作用
聚簇索引记录中的两个跟事务有关的隐藏列
ReadView 中有四个重要的字段:

creator_trx_id: 创建该ReadView的事务的事务id,也就是当前启动的事务的事务id。
m_ids:在创建ReadView时,当前数据库中 活跃事务的列表,注意这里是一个列表。活跃事务,指已经启动但没有提交的事务。
min_trx_id:在创建ReadView时,当前数据库中 活跃且未提交的事务列表中最小的事务id,也就是m_id中的最小值。
max_trx_id:这个并不是m_id中的最大值,指在创建ReadView时,当前数据库给下一个事务准备的事务id,也就是全局事务中的最大的事务id+1。
聚簇索引记录中的两个跟事务有关的隐藏列
假设插入一条商品信息的记录

使用innodb存储引擎的数据库表,聚簇索引记录中包含两个隐藏列
trx_id:当一个事务对某条聚簇索引记录进行改动时,会把 该事务的事务id记录在 trx_id隐藏列中。
roll_pointer:每次事务对聚簇索引记录做增删改操作时,都会把旧版本的记录写入到undo log中。同时,roll_pointer隐藏列存储的是一个指针,这个指针指向undo log中的旧版记录。于是,就可以通过这个指针回滚到修改前的记录。
在创建ReadView 后,记录中的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 值 之间,需要判断trx_id是否在 m_ids列表中:
trx_id 在 m_ids列表中,表示生成该版本记录的事务处于活跃状态,即事务还未提交,所以该版本记录对当前事务不可见。
trx_id 不在 m_ids列表中,表示生成该版本记录的事务已提交,所以该版本记录对当前事务可见。
这种通过 【版本链】 来控制并发事务访问同一记录时的行为就叫做 MVCC (多版本并发控制)
可重复读的MVCC是如何工作的?
可重复读的隔离级别,是在事务启动时生成一个 Read View,之后整个事务的执行过程中始终都使用整个Read View
假设事务A (事务id为51)启动后,事务B(事务id 为52)随后也启动了,这两个事务创建的Read View如下:


事务A和事务B 的Read View内容如下:
在事务A的Read View中,事务id 为51,由于这是第一个创建的事务,所以此时活跃的事务列表中就只有51,活跃的事务列表中最小的事务id就是事务A本身,因此下一个事务的id值为52。
在事务A的Read View中,事务id 为52,由于事务A还处于活跃状态中,因此,活跃事务列表中的id列表为51和52。活跃的事务id中最小的事务id为事务A,id 值为51,下一个事务id 值为 53。
在可重复读的隔离级别下,事务A 和 事务B 进行了如下操作:
事务B读取商品信息表,读到手机价格为2000;
事务A将商品信息表中手机的价格修改为3000,没有提交事务;
事务B再次读取商品信息表,手机价格仍然为2000;
事务A提交事务;
事务B第三次读取商品信息表,手机价格依然为2000;
接下来,具体分析以下为什么会是这种现象。
事务B第一次读取商品信息表,读取到该记录后,先查看隐藏列trx_id,发现这条记录的trx_id为50,小于事务B的Read View中 min_trx_id(51), 说明操作这条记录的事务在事务B启动前已经提交过了,所以这个版本的数据对事务B可见,也就是事务B可以读取到该记录。
事务A用update语句修改了这条记录,并且,未提交事务。将price字段修改为3000。此时,MySQL会记录相应的undo log ,并以链表的方式串联起来,形成版本链

从上图可以看出,事务A修改了这条记录,以前的记录就变成了旧版记录了。于是,新记录和旧版记录通过链表的方式串联起来,同时最新记录的trx_id更新为事务A的事务id(trx_id=51)。
事务B 第二次读取该记录,发现这条记录的 trx_id 变成了51,在事务B的 Read View的 min_trx_id 和max_trx_id之间。需要再次判断 trx_id 是否在 m_ids中,判断结果是存在的,说明修改这条记录的事务尚未提交,事务B不会读取整个版本的记录。需要沿着undo log链条寻找旧版本的记录,直到找到trx_id值 小于事务B的Read View的 min_trx_id 的第一条记录。所以,事务B读取到的是 trx_id=50的记录,也就是price=2000的记录。
当事务A提交事务后,由于事务的隔离级别是 可重复读,事务B第三次读取事务时,还是基于事务启动时创建的Read View来判断记录是否可见。所以,即使事务A修改了price字段的值为3000并且提交了事务,但是事务B在第三次读取记录时,依然是price值为2000的记录。
通过这种方式,可重复读的隔离级别实现了在事务执行期间读取到的数据都是事务启动之前的数据。
读提交的MVCC是如何工作的?
读提交的隔离级别,在事务期间每次读取记录时都会生成一个新的Read View
也就是说,在事务执行期间,多次读取同一条记录,前后两次可能出现数据不一致的情况,因此可能在此期间有其他事务修改了数据,并提交了事务。
还根据上一个列子来分析一下,假设事务A (事务id为51)启动后,事务B(事务id 为52)随后也启动了
在读提交的隔离级别下,事务A 和 事务B 进行了如下操作:
事务B读取商品信息表,读到手机价格为2000;
事务A将商品信息表中手机的价格修改为3000,没有提交事务;
事务B再次读取商品信息表,手机价格仍然为2000;
事务A提交事务;
事务B第三次读取商品信息表,手机价格为3000;
接下来,具体分析以下为什么会是这种现象。
事务B第一次读取数据创建的Read View


事务B读取到该记录后,发现这条记录的trx_id为50,小于事务B的Read View中 min_trx_id(51), 说明操作这条记录的事务在事务B启动前已经提交过了,所以这个版本的数据对事务B可见,也就是事务B可以读取到该记录。
事务A用update语句修改了这条记录,并且,未提交事务。

事务B 第二次读取记录时,事务A的修改操作尚未提交。此时事务B在第二此读取时创建的Read View

事务B 第二次读取该记录,发现这条记录的 trx_id 变成了51,在事务B的 Read View的 min_trx_id 和max_trx_id之间。需要再次判断 trx_id 是否在 m_ids中,判断结果是存在的,说明修改这条记录的事务尚未提交,事务B不会读取整个版本的记录。需要沿着undo log链条寻找旧版本的记录,直到找到trx_id值 小于事务B的Read View的 min_trx_id 的第一条记录。所以,事务B读取到的是 trx_id=50的记录,也就是price=2000的记录。
事务A提交之后,事务B第三次读取该记录时创建的Read View

事务B 第三次读取该记录,发现这条记录的 trx_id 为51,小于事务B的Read View中 min_trx_id(52)。这就意味着修改这条记录的事务在事务B第三次创建Read View之前就已经提交了,所以该版本的记录对事务B是可见的。
正是因为在读提交的隔离级别下,事务每次读取数据时都会重新创建Read View。因此,在事务期间多次读取同一条记录,会出现前后两次读取结果不一致的情况。主要是因为在此期间有其他事务修改了记录,并提交了事务。
总结
对于 【读提交】 和 【可重复读】 两种隔离级别来说,快照读(普通的select)是通过ReadView的字段和 聚簇索引中的两个隐藏字段进行比对,来控制并发事务访问同一记录时的行为。称之为多版本并发控制(MVCC)。它们之间的区别在于创建ReadView的时机不同
读提交隔离级别,在事务执行期间,每个select前都会生成一个新的 ReadView,也就意味着在事务期间多次读取同一条数据,前后两次读取的数据可能会出现不一致。因为,在这期间有其他事务修改了这条数据,并提交了事务。
可重复读隔离级别,在事务启动时生成一个ReadView,并且在整个事务执行期间都只使用这一个 ReadView,这样就保证了在事务期间读取到的数据都是事务启动前的记录。





