MVCC由浅入深学习


我们都知道,当多个事务并发运行的时候,很容易会产生各种各样的数据问题。如脏读,脏写,不可重复读,幻读等。

那么,mysql是如何解决多个事务并发运行时数据的查询与更新问题的呢?

这其实就是通过Mysql的MVCC机制,而MVCC机制就是通过版本链与ReadView实现的。

事务的几种隔离级别

首先,要了解MVCC机制必须得知道mysql事务里的几种隔离级别。

  • 读未提交
  • 读已提交
  • 可重复读
  • 串行化

读未提交: 在当前事务里,能读取到其他未提交的事务修改后的值。
会产生脏读、不可重复读、幻读。

读已提交: 在当前事务中,只能读到已提交后的事务的值。会产生不可重复读、幻读。

可重复读: 当事务开启后,在运行过程中所查询到的值是不会发生改变的。(不会在运行过程中,因为某个值被其他事务修改并提交就读取进来)。也就是事务一旦开启,多次查询同一个值,无论那个值在运行过程中有没有被其他事务修改并提交,你查询出来的值都是一样的。会产生幻读问题(在mysql中此事务隔离级别中通过mvcc+锁的机制把幻读也解决了)

串行化: 就是直接加锁排队串行化运行。不允许多个事务并发执行。

篇幅有限,如果对脏读,脏写,不可重复读和幻读有点不了解的可以搜搜其他文章看看

版本链

我们都知道,事务是可以回滚的。那么必然就要保存每次事务修改前的数据,不然到时候回滚是根据什么回滚呢?

而当你一个值被修改多次的时候,就需要有个版本链,记录每次的值,从而才可知道回滚到哪个对应的版本上。

而在innodb存储引擎上,聚集索引中都会包含两个隐藏列。一个是trx_id,用于记录事务id,一个是roll_pointer,回滚指针。

那么其实undo log 版本链看上去就大概是这个样子。

那么可以试想下以下场景:

一开始事务A(id为10)对某张表id为1的某个字段更改,把值改为A:

image

然后又有事务B(id为20)对那条数据进行修改,把值改成B:

image

最后事务C(id为30)把那条数据的值改成C:

image

不难看出,每次更新,都会将旧的值保存起来,放到undo log的版本链中,形成一个链表。链表的头节点就是记录最新的值。

那么此时我们可以看到,虽然每次修改每个版本的值我都保存记录下来了,但这么多版本,我又怎么知道我应该获取的是版本链上哪个版本的值呢???

这个时候就需要我们的 ReadView 登场了。

ReadView

readview 你可以简单的理解为是一个视图。通过它,我们可以知道在版本链上哪个版本对于当前事务以及当前事务的隔离级别是可见的。

首先我们要知道readView是由什么组成的,为什么通过它我们可以判断版本链上哪个版本是我们应该获取的值,哪些又是不可获取的值。

那么readView的组成主要可以分为四部分:

  • m_ids: 是一个数组,记录的是当前活跃的事务id(就是还没提交的)
  • min_trx_id: m_ids数组里面的最小值
  • max_trx_id: 表示下一个事务生成时分配给它的id。(不是m_ids里面的最大值)
  • creator_trx_id: 当前事务id

好了,在我们知道readview的组成后,我们接下来可以看看在mysql中不同事务隔离级别下是怎么实现的。

如何实现

不难看出,在读未提交和串行化这两种隔离级别下,都是直接获取最新的值即可,不存在那种多版本并发控制的问题。

因此,我们主要研究的是在读已提交和可重复读条件下是如何通过版本链与readview获取到对应的正确版本的值。

MVCC机制

首先,我们要知道MVCC机制是如何通过readview和版本链就可决定对应版本链上的数据能否被读取。也就是要知道MVCC里面内部的大致规则是怎样的。

其实,有了这个readview,判断某个数据能否被读取,只需按照以下步骤来即可:

  • 如果被访问版本的trx_id属性值与ReadView中的creator_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列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃
    的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版
    本可以被访问。

读已提交

读已提交隔离级别下就是只能读取到其他事务已经提交过的值,若某个值被其他事务修改了,但那个事务还未提交的时候是无法被其他事务读取到的。

那么,这个时候我们可以试下想以下场景:

此时,版本链上存在一条被事务id为5的插入进去的数据。且有两个活跃事务A和B

image

这时候,事务B对数据进行修改,把值改成B, 那么在此时的trx_id就会变为20,同时生成一个undolog,由roll_pointer来指向。

image

接下来,事务A就要开始查询。这时候就要生成一个readview,不难看出,readview的m_ids为[10,20],min_trx_id:10,max_trx_id:21(按递增为1算),creator_trx_id:10。

image

那么此时,事务A去查询的时候就会发现,版本链上第一个时trx_id为20的数据,在[min_trx_id,max_trx_id)中,且也在m_ids里面。

这就证明此版本号所对应数据的事务还在活跃状态中(还未被提交)。因此就不读取,继续顺着roll_pointer找下一条数据。

image

然后接下来,找到下一条数据发现trx_id=5。发现小于min_trx_id。这说明这条数据所对应的事务在很早之前就被提交了,因此可以读取。

image

这个时候,事务B提交了。那按照读已提交的特性,当事务B提交的时候,事务A就应该可以读取到事务B的值,那怎么能让事务A根据MVCC机制能读取到呢?

很简单,只需要重新生成一个readview即可。重新生成readview后,这个时候readview里面的值就有:m_ids:[10],min_trx_id:10,max_trx_id:21(按递增为1算),creator_trx_id:10。

那事务A开始查询的时候,根据MVCC机制,发现版本链上的头节点数据trx_id=20,在[min_trx_id,max_trx_id)范围里面,且不在m_ids数组里面,因此可以读取。

image

因此我们不难看出,对于读已提交这种隔离级别下的实现机制就是:每次查询都得重新生成一个readview。

可重复读(默认)

对于可重复读隔离级别下,当事务开启后,对于同一条数据,从头到尾所获取到的值都不会改变。

一样的,我们不妨试想下有两个事务,事务A和事务B。且有一条原始数据。

image

这时候,事务A开始查询,就会生成一个对应的readview,查询的时候发现trx_id<min_trx_id。证明这条数据所对应的事务在很早前就被提交了。可以读取。

image

然后事务B对数据修改了,把值改为20,并且还提交了!!!

这个时候就会生成一个新的节点插入到版本链中了,且trx_id为20。

image

那么这个时候,事务A又重新获取数据,它能查询到事务B修改并提交后的值吗???

不能!!!因为在可重复读隔离级别下,事务开启后就只会生成一个reaview,并且一直沿用下去。

因此当事务A再次获取的时候,会发现头节点是trx_id为20的。在[min_trx_id,max_trx_id)范围中,但因为也在m_ids数组中,因此根据MVCC机制,不会读取这个版本的值,会继续根据roll_pointer找下一个值。并成功读取到trx_id为5的那个版本的值。

image

因此不难看出,实现可重复读这种隔离级别的机制就是:在事务开启后只生成一个readview,并不断沿用下去。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值