MVCC 多版本并发控制机制详解,undo log版本链和ReadView是如何避免并发问题

一 undo log版本链是什么东西?

数据库的隔离级别,以及脏读,幻读,不可重复读
为什么mysql的默认隔离级别RR,可以让脏读,不可重复读,幻读都不会发生呢?

     这就是由经典的MVCC多版本并发控制机制做到的,讲解这个MVCC机制之前,还得先讲讲undo log版本链的故事。

      在数据库中每行数据都有两个隐藏字段,一个是trx_id,一个是roll_pointer,这个trx_id就是最近一次更新这条数据的事务id,roll_pointer就是指向当前这个事务之前生成的undo log。

比如,现在一个事务A(事务id=1),插入了一条数据,那么此时这条数据的隐藏字段以及指向的undo log如下图所示,插入的这个数据的值是X,因为事务A的id是1,所以这条数据的txr_id就是1,roll_pointer指向一个空的undo log,因为之前这条数据是没有的。
在这里插入图片描述


      下面如果一个线程过来修改了一下这个数据,也就是事务B,把值改成了值N,事务B的id是2,那么此时更新之前会生成一个undo log记录之前的值,然后会让roll_pointer指向这个实际的undo log回滚日志,如下图所示。

在这里插入图片描述
多个事务串行执行的时候,每个人修改了一行数据,都会更新隐藏字段trx_id和roll_pointer,同时之前多个数据快照对应的undo log,会通过roll_pointer指针串联起来,形成一个重要的版本链!

二 通过undo log多版本链条实现的ReadView机制

ReadView ,当执行一个事务的时候,就会生成一个对应的ReadView,里面比较关键的东西有4个

  • 一个是m_ids,这个就是说明此时有哪些事务在Mysql里执行还没有提交
  • 一个是min_trx_id,就是m_ids里最小的值
  • 一个是max_trx_id,mysql下一个要生成的事务id,就是最大事务id
  • 一个是creator_trx_id,就是你这个事务的id

假设原来数据库里就有一行数据,很早以前就有事务插入过了,事务id是10,它的值为M,如下图所示
在这里插入图片描述
接着呢,此时两个事务并发过来执行,一个事务A(id=20),一个事务B(id=30),事务B是要去更新这行数据的,事务A是要去读取这行数据的值的。

现在事务A直接开启了一个ReadView,这个ReadView里的m_ids就包含了事务A和事务B的两个id,20,30,min_trx_id就是20,max_trx_id就是31,creator_trx_id就是20,是事务A自己。

这个时候事务A第一次查询这行数据,会走一个判断,当前这行数据的txr_id是否小于min_trx_id,此时发现txr_id=10m,是小于min_trx_id的,说明事务开启之前,修改这行数据的事务早就提交了,所以此时可以查询到这条数据。
在这里插入图片描述


此时,事务B开始动手了,它把这行数据的值修改成了值N,然后这行数据的txr_id自然就是事务B的id,同时roll_pointer指向了修改之前生成的一个undo log,接着这个事务B就提交了,如下图所示。
在这里插入图片描述

       这个时候事务A再次查询,此时查询的时候,当查询到数据N行时,其中的trx_id=30,那么txr_id是大于ReadView里的min_txr_id(20),同时小于ReadView的max_trx_id的,说明更新这条数据的事务,很可能就跟自己差不多同时开启的,于是查看这个txr_id是否在ReadView的m_ids列表中,如果在列表中,这个修改数据的事务是跟自己同一时段并发执行然后提交的,所以对这行数据是不能查询的!只能查到trx_id等于10的数据。多个事务并发执行的时候,事务B更新了值,通过Read view+undo log日志链条的机制,就可以保证事务A不会读到并发执行的事务B更新的值,只会读到更早的值。
在这里插入图片描述


       接着假设事务A自己更新了这行数据的值,改成J,trx_id也就是20,同时保存之前事务B修改值的快照,如下图所示
在这里插入图片描述

       此时事务A查询这条数据的值,会发现这个trx_id=20,和自己的ReadView里的creator_trx_id是一样的,说明这行数据就是自己修改的啊!自己修改的值当然是可以看到的了!


       接着在执行事务A的过程中,突然开启了一个事务C,这个事务的id为40,然后它更新了那行的值为K,还提交了,如下图所示
在这里插入图片描述
这个时候事务A再去查询,会发现当前数据的trx_id=40,大于了自己的ReadView中的max_trx_id,此时说明什么?
说明是这个事务A开启之后,然后有一个事务更新了数据,自己当然是不能看到的了!

此时就会顺着undo log多版本链条往下找,自然先找到值A自己之前修改的过的那个版本,因为那个trx_id=20跟自己的ReadView里的creator_trx_id是一样的,所以此时直接读取自己之前修改的那版本。

通过undo log多版本链条,加上你开启事务时候生产的一个ReadView,然后再有一个查询的时候,根据ReadView进行判断的机制,你就知道你应该读取哪个版本的数据。

而且它可以保证你只能读到你事务开启前,别的提交事务更新的值,还有就是你自己事务更新的值。假如说是你事务开启之前,就有别的事务正在运行,然后你事务开启之后 ,别的事务更新了值,你是绝对读不到的!或者是你事务开启之后,比你晚开启的事务更新了值,你也是读不到的!

通过这套机制就可以实现多个事务并发执行时候的数据隔离。

三 Read Committed隔离级别是如何基于ReadView机制实现的

当一个事务处于RC隔离级别的时候,它每次发起查询,都重新生成一个ReadView!


如果数据库里面有一行数据,是事务id=10的一个事务插入进去的,初始值为M,现在活跃着两个事务,一个是事务A(id=20),一个事务B(id=30),此时如下图所示
在这里插入图片描述


此时,事务B发起了一次update操作,更新了这条数据,把这条数据的值修改成了N,所以此时数据的trx_id会变成事务B的id=30,同时会生成一条undo log,由roll_pointer来指向,如下图:
在这里插入图片描述


然后,事务A要发起一次查询操作,此时它一发起查询操作,就会生成一个ReadView,此时ReadView里的min_trx_id=20,max_trx_id=31,creator_trx_id=20

当事务A发起查询,发现当前这条数据的trx_id是30。属于ReadView的事务id范围之间,说明是它生成ReadView之前就有这个活跃的事务,是这个事务修改了这条数据的值,但是此时这个事务B还没提交,所以ReadView的m_ids活跃事务列表里,有这个id,所以此时根据ReadView的机制,此时事务A是无法查到事务B的值。

接着就顺着undo log版本链条往下查找,就会找到一个原始值,发现他的trx_id是10,小于当前ReadView里的min_trx_id,说明是他生成ReadView之前,就有一个事务插入了这个值并且早就提交了,因此可以查到这个原始值.


接着,假设事务B此时就提交了,好了,那么提交了就说明事务B不会活跃于数据库里了,是不是?可以的,大家一定记住,事务B现在提交了。那么按照RC隔离级别的定义,事务B此时一旦提交了,说明事务A下次再查询,就可以读到事务B修改过的值了,因为事务B提交了。

事务A下次发起查询,再次生成一个ReadView。此时再次生成ReadView,数据库内活跃的事务只有只有事务A了,这个时候查询数据,这条数据的trx_id=30,虽然在min_trx_id和max_trx_id范围之间,但是此时并不在m_ids列表内,说明事务B在生成本次ReadView之前就已经提交了。

那么既然在生成本次ReadView之前,事务B就已经提交了,就说明这次你查询就可以查到事务B修改过的这个值了,此时事务A就会查到值B,如下图所示。
在这里插入图片描述

四 ReadView是如何实现RR隔离级别的

Mysql的RR隔离级别,能够同时避免不可重复读和幻读问题。RR隔离级别下,在事务中,第一次查询生成ReadView,在这个事务中查询都会使用这个ReadView,但是更新是基于最新值进行更新。


和上面一样,还是假设一条数据是事务id=10的事务插入的,它的值为M,同时此时有事务A和事务B同时在运行,事务A的id=20,事务B的id=30,如下图所示。
在这里插入图片描述


此时,事务A发起了一次查询,着RR隔离级别下,它就是第一次查询时候生成一个ReadView,此时ReadView里的creator_trx_id=20,min_trx_id=20,max_trx_id=31,m_ids是[20,30],此时ReadView如下图。
在这里插入图片描述
这个时候事务A基于这个ReadView去查这条数据,会发现这条数据的trx_id为10,是小于ReadView里的min_trx_id的,说明他发起查询之前,早就有事务插入这条数据还提交了,所以此时可以查到这条原始值的.


接下来,事务B更新了这条数据的值为值N,此时会修改trx_id为30,同时生成一个undo log,而且关键是事务B此时还提交了,也就是说此时事务B已经结束了,如下图
在这里插入图片描述
这个时候,当事务A再次查询,使用的ReadView还是第一次生成的ReadView,所以还是只能查询到trx_id那行数据。
事务A多次读同一个数据,每次读到的都是一样的值,除非是它自己修改了值,否则读到的一直会是一样的值。不管别的事务如何修改数据,事务A的ReadView始终是不变的,它基于这个ReadView始终看到的值是一样的!

五 总结

多版本并发控制,它本质是协调你多个事务并发运行的时候,并发的读写同一批数据,此时应该如何协调互相的可见性。在MySQL中让多个事务并发运行的时候能够互相隔离,避免同时读写一条数据的时候有影响,是依托undo log版本链条和ReadView机制来实现的。

  • 15
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
MySQL的多版本并发控制MVCC)是一种并发控制机制,它主要是为了解决并发读写冲突的问题。在MVCC机制中,每个事务都可以看到数据库中的一个快照,这个快照是在事务开始时确定的。事务读取数据时,实际上是读取了该快照中的数据,而不是实际的数据。当事务需要修改数据时,MySQL会根据数据的版本号来判断是否可以进行修改。 MVCC的实现原理主要是在每一行数据后面保存多个版本号,并且还需要保存该版本号对应的事务ID。当开始一个事务时,MySQL会为该事务分配一个唯一的事务ID,该事务ID会被用于标记事务对应的数据版本号。当一个事务需要读取数据时,MySQL会根据该事务的事务ID和版本号来判断是否允许读取该数据。如果该事务的事务ID小于等于该数据的版本号,那么就可以读取该数据。如果该事务需要修改数据,则MySQL会为该数据在数据库中创建一个新版本,并将该新版本版本号和事务ID保存下来。这样,其他事务就可以继续读取原来的版本,而该事务则可以读取新版本并修改数据,从而实现并发控制。 需要注意的是,MVCC只能解决读写冲突的问题,而不能解决写写冲突的问题。此外,MVCC也会占用一定的存储空间,因为每个数据行都需要保存多个版本号和事务ID。因此,在使用MVCC机制时,需要注意存储空间和性能方面的问题

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值