浅谈Mysql的MVCC机制(RC与RR隔离级别)
MVCC(Multi-Version Concurrency Control)多版本并发控制
说这个我们先来了解一下Mysql的隔离级别,因为MVCC和Mysql的隔离级别是有关的。
Mysql默认的隔离级别是RR(可重复读)
其他的隔离级别是读未提交(RU)、读已提交(RC)、可重复读(RR)、可串行化
读未提交我们知道会产生脏读,脏读也就是一个事务读到另外一个事务未提交的数据,造成数据不一致问题。
读已提交不会造成脏读,从名字来看就是已提交,就是解决读未提交产生的问题,它不会产生脏读,但是它会产生不可重复读,不可重复读,指的就是一个事务一直读取同一条记录,但是会被其他事务的更新操作影响到读取的结果,造成数据不一致。
举个例子:
这时候最后一条读出来的结果就是zhangsan了。
这就是不可重复读。
可重复读,从名称上来看就是可以重复select,也就是为了解决读已提交造成的不可重复读。
那么你们有没有想过,为什么读已提交不会造成读未提交产生的脏读,却会造成不可重复读呢?可重复读为什么不会造成读已提交产生的不可重复读呢?其实背后都是MVCC控制的。
MySQL中的行数据,除了我们肉眼能看到的字段之外,其实还包含了一些隐藏字段,它们在内部使用,默认情况下不会显示给用户。
字段 | 含义 |
---|---|
DB_ROW_ID | 隐含的自增ID(隐藏主键),用于唯一标识表中的每一行数据,如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引。 |
DB_TRX_ID | 该字段存储了当前行数据所属的事务ID。每个事务在数据库中都有一个唯一的事务ID。通过 DB_TRX_ID 字段,可以追踪行数据和事务的所属关系。 |
DB_ROLL_PTR | 该字段存储了回滚指针(Roll Pointer),它指向用于回滚事务的Undo日志记录。 |
Read View
一致性视图,会用一个列表存放当前活跃的事务ID,也就是未提交的事务,当我们事务进行select 读操作的时候,就会生成这么一个ReadView视图,ReadView就会判断undo log版本链中的哪个版本对当前事务是可见的
这里提到了Undo log那我们就看看这个是什么东西,就是为了进行版本回滚的。
维护了一条版本链,链上都是每一行记录,包括刚刚提到的trx_id事务id,row_id,roll_pointer
当我们对数据进行更新的时候,会先将更新前原记录进行拷贝,然后undo Log日志就会开始存放,每次更新的时候都是这样,至此就会形成一条版本链。
那么ReadView是怎么判断哪些事务ID是可见的呢?
从Undo Log 版本链中的第一条记录开始往前找,获取当前版本的trx_id 事务id,只要事务ID在活跃的事务列表里面,那肯定是不可见的,如果比最小事务ID小,也是不可见,比最大事务ID还大,那也是不可见,也就是说只有在最小事务ID和最大事务ID之间才是可见的。
我们可以简单的举一个例子:
T4 时刻事务C查询的结果是什么呢?
我们看现在活跃的事务id有100,
最小事务ID是100,
最大事务ID是400,
那么从Undo Log版本链中找
第一条事务ID是100,100在活跃的事务列表里面,不可见。(所以这就验证了为什么RC级别下不会造成读未提交产生的脏读,未提交的在列表里面,直接不可见了)
继续往前找,还是100,还是不可见,
继续往前找,1,1不在事务ID列表里面,继续判断1是不是等于当前生成ReadView视图的事务ID300?不等于,1是否大于最大事务ID100?不大于。继续判断1是否小于最小事务ID100,1<100可见,所以RC级别下此时select查询出来的结果就是小明。RR也是小明。
我们来看一下T6时刻,如果是RC级别下,此时Undo Log的版本链是长这样的:
此时活跃的事务ID列表有:200,
最小事务ID是200,
最大事务ID是400,
创建ReadView视图的当前事务ID是300
从第一个版本看,200,在活跃的事务ID列表里面,不可见,
继续向前找200,还是在活跃的事务ID列表里面,不可见,
继续向前找,100,100 不在活跃的事务ID列表里面,100也不等于创建当前ReadView的事务ID300,100比最小事务ID还小,可以访问,所以RC级别下读到的结果就是小红,那么如果是RR级别下读到的是什么呢?还是小明,因为RR级别的ReadView视图会一直延用第一次相同Select创建的ReadView,这也就是为什么RR级别不会造成不可重复读,而RC会造成不可重复读的原因,RC每次Select创建的ReadView视图都是新创建的,而RR都是延用第一次Select产生的ReadView视图。所以也就验证了前面的那个疑问:为什么读已提交不会造成读未提交产生的脏读,却会造成不可重复读呢?可重复读为什么不会造成读已提交产生的不可重复读呢?
总结:
RC级别下,select的时候ReadView视图每次都是会创建新的,所以这就是会造成不可重复读的原因;ReadView在判断版本链中哪个版本对当前事务可见的时候,如果是未提交的事务ID,是直接不可见的,这就是为什么不会造成脏读的原因。
RR级别下,Select的时候ReadView视图都是延用第一次Select创建的ReadView视图,而不会产生新的ReadView视图,这也就是为什么RR级别不会造成不可重复读的原因。