27.mvcc之readview

undo log版本链

今天我们正式开始切入讲解MySQL中多个事务并发执行时的隔离到底是怎么做的,因为我们知道默认是RR隔离级别,也就是说脏写、脏读、不可重复读、幻读,都不会发生,每个事务执行的时候,跟别的事务压根儿就没关系,甭管你别的事务怎么更新和插入,我查到的值都是不变的,是一致的!

但是这到底是怎么做到的呢?

这就是由经典的MVCC多版本并发控制机制

InnoDB的内部实现中为每一行数据增加了三个隐藏列用于实现MVCC。trxid就是最近一次更新这条数据的事务id,rollpointer就是指向你了你更新这个事务之前生成的undo log。

| 列名 | 长度(字节) | 作用 | | ----------- | ---------- | ----------------------------------------------------- | | DBTRXID | 6 | 插入或更新行的最后一个事务的事务id。删除视为更新,将其标记为已删除。 | | DBROLLPTR | 7 | 写入回滚段的撤消日志记录即undolog,若行已更新,则撤消日志记录包含在更新行之前重建行内容所需的信息。 | | DBROWID | 6 | 行标识(隐藏单调自增id) |

image.png

我们给大家举个例子,现在假设有一个事务A(id=50),插入了一条数据,那么此时这条数据的隐藏字段以及指向的undo log如下图所示,插入的这条数据的值是值A,因为事务A的id是50,所以这条数据的txrid就是50,rollpointer指向一个空的undo log,因为之前这条数据是没有的。

image.png

接着假设有一个事务B跑来修改了一下这条数据,把值改成了值B,事务B的id是58,那么此时更新之前会生成一个undo log记录之前的值,然后会让roll_pointer指向这个实际的undo log回滚日志,如下图所示。

image.png

大家看上图是不是觉得很有意思?事务B修改了值为值B,此时表里的那行数据的值就是值B了,那行数据的txrid就是事务B的id,也就是58,rollpointer指向了undo log,这个undo log就记录你更新之前的那条数据的值。

所以大家看到rollpointer指向的那个undo log,里面的值是值A,txrid是50,因为undo log里记录的这个值是事务A插入的,所以这个undo log的txr_id就是50,我还特意把表里的那行数据和undo log的颜色弄成不一样的,以示区分。

接着假设事务C又来修改了一下这个值为值C,他的事务id是69,此时会把数据行里的txr_id改成69,然后生成一条undo log,记录之前事务B修改的那个值

此时如下图所示,看起来如下。

image.png

我们在上图可以清晰看到,数据行里的值变成了值C,txrid是事务C的id,也就是69,然后rollpointer指向了本次修改之前生成的undo log,也就是记录了事务B修改的那个值,包括事务B的id,同时事务B修改的那个undo log还串联了最早事务A插入的那个undo log,如图所示,过程很清晰明了。

所以这就是今天要给大家讲的一点,大家先不管多个事务并发执行是如何执行的,起码先搞清楚一点,就是多个事务串行执行的时候,每个人修改了一行数据,都会更新隐藏字段txrid和rollpointer,同时之前多个数据快照对应的undo log,会通过roll_pinter指针串联起来,形成一个重要的版本链!

今天要让大家明白的,就是这个多个事务串行更新一行数据的时候,txrid和rollpinter两个隐藏字段的概念,包括undo log串联起来的多版本链

ReadView机制

接着上次我们讲过的undo log多版本链条,我们来讲讲这个基于undo log多版本链条实现的ReadView机制

把这个机制讲明白了,下一次我们再正式讲解RC和RR隔离级别下的MVCC多版本并发控制机制,就很容易理解了。

这个ReadView呢,简单来说,就是你执行一个事务的时候,就给你生成一个ReadView,里面比较关键的东西有4个

  • 一个是m_ids,这个就是说此时有哪些事务在MySQL里执行还没提交的;即活跃事务id
  • 一个是mintrxid,就是m_ids里最小的值;
  • 一个是maxtrxid,这是说mysql下一个要生成的事务id,就是最大事务id;
  • 一个是creatortrxid,就是你这个事务的id

那么现在我们来举个例子,让大家通过例子来理解这个ReadView是怎么用的

假设原来数据库里就有一行数据,很早以前就有事务插入过了,事务id是32,他的值就是初始值,如下图所示。

image.png

接着呢,此时两个事务并发过来执行了,一个是事务A(id=45),一个是事务B(id=59),事务B是要去更新这行数据的,事务A是要去读取这行数据的值的,此时两个事务如下图所示。

image.png

现在事务A直接开启一个ReadView,这个ReadView里的mids就包含了事务A和事务B的两个id,45和59,然后mintrxid就是45,maxtrxid就是60,creatortrx_id就是45,是事务A自己。

这个时候事务A第一次查询这行数据,会走一个判断,就是判断一下当前这行数据的txrid是否小于ReadView中的mintrxid,此时发现txrid=32,是小于ReadView里的mintrxid就是45的,说明你事务开启之前,修改这行数据的事务早就提交了,所以此时可以查到这行数据,如下图所示。

image.png

接着事务B开始动手了,他把这行数据的值修改为了值B,然后这行数据的txrid设置为自己的id,也就是59,同时rollpointer指向了修改之前生成的一个undo log,接着这个事务B就提交了,如下图所示。

image.png

这个时候事务A再次查询,此时查询的时候,会发现一个问题,那就是此时数据行里的txrid=59,那么这个txrid是大于ReadView里的mintxrid(45),同时小于ReadView里的maxtrxid(60)的,说明更新这条数据的事务,很可能就跟自己差不多同时开启的,于是会看一下这个txrid=59,是否在ReadView的mids列表里?

果然,在ReadView的m_ids列表里,有45和59两个事务id,直接证实了,这个修改数据的事务是跟自己同一时段并发执行然后提交的,所以对这行数据是不能查询的!如下图所示。

image.png 那么既然这行数据不能查询,那查什么呢?

简单,顺着这条数据的rollpointer顺着undo log日志链条往下找,就会找到最近的一条undo log,trxid是32,此时发现trxid=32,是小于ReadView里的mintrx_id(45)的,说明这个undo log版本必然是在事务A开启之前就执行且提交的。

好了,那么就查询最近的那个undo log里的值好了,这就是undo log多版本链条的作用,他可以保存一个快照链条,让你可以读到之前的快照值,如下图。

image.png

看到这里,大家有没有觉得很奇妙?多个事务并发执行的时候,事务B更新的值,通过这套ReadView+undo log日志链条的机制,就可以保证事务A不会读到并发执行的事务B更新的值,只会读到之前最早的值。

接着假设事务A自己更新了这行数据的值,改成值A,trx_id修改为45,同时保存之前事务B修改的值的快照,如下图所示。

image.png

此时事务A来查询这条数据的值,会发现这个trxid=45,居然跟自己的ReadView里的creatortrx_id(45)是一样的,说明什么?

说明这行数据就是自己修改的啊!自己修改的值当然是可以看到的了!如下图。

image.png

接着在事务A执行的过程中,突然开启了一个事务C,这个事务的id是78,然后他更新了那行数据的值为值C,还提交了,如下图所示。

image.png

这个时候事务A再去查询,会发现当前数据的trxid=78,大于了自己的ReadView中的maxtrx_id(60),此时说明什么?

说明是这个事务A开启之后,然后有一个事务更新了数据,自己当然是不能看到的了!如下图。

image.png

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

image.png

不知道大家看到这里感觉如何?通过一系列的图,我相信每个人都能彻底理解这个ReadView的一套运行机制了

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

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

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

ReadView实现原理

ReadView中主要包含4个比较重要的内容:

  • m_ids:表示在生成ReadView时当前系统中活跃的读写事务的事务id列表。代表未提交的事务id集合。
  • mintrxid:表示在生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值。
  • maxtrxid:表示生成ReadView时系统中应该分配给下一个事务的id值。
  • 注意maxtrxid并不是mids中的最大值,事务id是递增分配的。比方说现在有id为1,2,3这三个事务,之后id为3的事务提交了。那么一个新的读事务在生成ReadView时,mids就包括1和2,mintrxid的值就是1,maxtrxid的值就是4。
  • creatortrxid:表示当前事务的事务id。
  • 只有在对表中的记录做改动时(执行INSERT、DELETE、UPDATE这些语句时)才会为事务分配事务id,否则在一个只读事务中的事务id值都默认为0。

有了这个ReadView,这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见:

  • 如果被访问版本的trxid属性值与ReadView中的creatortrx_id值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。

  • 如果被访问版本的trxid属性值小于ReadView中的mintrx_id值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。

  • 如果被访问版本的trxid属性值大于或等于ReadView中的maxtrx_id值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。

  • 如果被访问版本的trxid属性值在ReadView的mintrxid和maxtrxid之间,那就需要判断一下trxid属性值是不是在m_ids列表中。

    如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问。

    即当mintrxid=1 maxtrxid=4时,被访问版本的trxid为2。mids包括1,2,那么trx_id为2的数据不可见

    如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问 。

    即当mintrxid=1 maxtrxid=4时,被访问版本的trxid为3。mids包括1和2,那么trx_id为3的数据可见

如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录。

参考:https://blog.csdn.net/zht245648124/category119754892.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值