一文讲清,MySQL如何解决多事务并发问题

共 3124字,需浏览 7分钟

 ·

2021-10-21 08:00

MySQL默认事务隔离级别是repeatable-read(RR),脏读、不可重复读、幻读,都不会发生。它是怎么做到的呢?


这就是由经典的MVCC多版本并发控制机制做到的,MVCC的实现,又是基于undo log版本链的。


前面讲MySQL一行数据的存储格式,讲到了每行数据有两个隐藏的字段:trx_id、roll_pointer。trx_id就是最近一次更新这条数据的事务id,roll_pointer指向了你更新这个事务之前生成的undo log。


假设有一个事务A(id = 50),插入了一条数据A,它的数据格式如下:

图1 undo log版本链


接着事务B修改这条数据把值修改为B,事务B的id是58,此时会生成一个undo log记录之前的值,roll_pointer指向这个undo log日志。

图2 undo log版本链


假设再来了一个事务C,它的事务id是68,把数据值改为了C,此时undo log版本链就变成这样了。


图3 undo log版本链


事务执行的时候,都会更新隐藏的字段trx_id和roll_pointer,同时之前多个数据快照对应的undo log也会通过roll_pointer串联起来,最终形成一个版本链。


基于undo log实现的ReadView


执行一个事务的时候,会生成一个ReadView,里面包含这些东西:


  • m_ids,此时有哪些事务在MySQL中还没有提交的事务id;

  • min_trx_id,m_ids里最小的;

  • max_trx_id,MySQL下一个要生成的事务id;

  • creator_trx_id,表示生成该ReadView的事务的事务id。


假设数据库中有一行数据,值是A,事务id是32,如下图所示:

图4 初始情况下,数据库中有一行数据


此时有两个事务并发过来执行,事务A(id=45),事务B(id=59),事务A要去读取这行数据,事务B要去修改这行数据。


事务A开启一个ReadView,此时它长这样:

图5 ReadView


ReadView的m_ids包含事务A和事务B的两个id,45和49,min_trx_id是45,max_trx_id是60,creator_trx_id就是45,就是事务A自己。


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

图6 事务A读取数据


接着事务B开始修改这行数据,事务B把值修改为B,然后这行数据的trx_id设置为自己的id,也就是59,同时roll_pointer指向了修改之前生成的undo log。

图7 事务B修改数据


这时候事务A第二次查询,发现此时数据行里的trx_id=59,大于ReadView里的min_trx_id=45,同时小于max_trx_id=60,说明更新这条数据的事务,很可能跟自己差不多同时开启。果然ReadView的m_ids里有45和59两个事务id,事务B是跟自己并发执行提交的,所以这行数据是不能查询的。

图8 事务A第二次读数据


事务A不能查修改后的值,那怎么办?顺着undo log版本链查询之前的版本!


于是就会查到trx_id=32的数据,trx_id=32是小于ReadView里min_trx_id=45的,可以查出来。


看到这里,大家能不能猜想到多事务并发的时候,MySQL是如何解决那一堆问题的?就是通过undo log版本链 + ReadView解决的!


假设事务A执行的过程中,事务C来更新这行数据为C,事务id=78。


图9 事务C修改数据


此时事务A第三次去查,发现当前数据的trx_id=78,比ReadView中的max_trx_id=60还大,说明这条数据是事务A开启之后修改的,不应该查到!


于是事务A顺着undo log版本链往下找,先找到trx_id=59的数据,上面分析过了,这条数据也不能查,于是继续向undo log版本链向下找,最终返回trx_id=32的数据。


通过undo log版本链和ReadView,MySQL就可以保证你只能读取到事务开启前别的事务更新的值,和自己更新的值。


总的来说,就是一个事务只能读取到事务id小于等于自己的数据。


读已提交(RC)如何基于MVCC实现多事务并发控制?


只要你搞明白了上面的undo log版本链 + ReadView机制,对于RC、RR如何基于这套机制实现多版本并发控制,就非常好理解了。


首先,有一点非常重要,RC隔离级别下,一个事务每次发起查询,都会生成一个ReadView。


假设库里有一行数据,trx_id=50,现在有两个事务A(id=60),事务B(id=70)并发执行。


事务B修改数据值为B,此时trx_id=70,如图:

这时候,事务B还没提交,事务A发起查询,那么就会生成已给ReadView。

ReadView的m_ids里活跃的事务由60和70,此时事务A是无法查出事务B修改的值B的。于是顺着版本链向下找,就找到trx_id=50的数据了。


接着,事务B提交了,事务A再次发起查询,又生成了一个ReadView。

事务A再次基于ReadView查询,发现这条数据的trx_id虽然在min_trx_id和max_trx_id之间,却不在m_id里,说明事务B在生成本次ReadView之前已经提交了,那么本次就可以查询到事务B修改的这个值了。


RC隔离级别如何实现的,级别就讲完了,其关键在于每次查询都会生成一个新的ReadView。


可重复读(RR)如何基于MVCC实现多事务并发控制?


可重复读隔离级别下,解决了脏读、不可重复读、幻读这些问题,它是如何实现的呢?


假设,数据库有一条数据trx_id=50,现在有两个事务A(id=60),事务B(id= 70)并发执行。


事务A发起一个查询,会生成一个ReadView。

这个事务A基于这个ReadView去查这条数据,会发现trx_id =50,小于ReadView里的min_trx_id,可以直接查出来。


接着事务B修改数据值为B,此时会修改trx_id=70,然后提交事务。

接着事务A第二次去查询这条数据,要知道它的ReadView没有变。它会发现此时数据的trx_id=70在min_trx_id和max_trx_id之间,并且在m_ids中。那肯定不能查询出来。于是顺着undo log版本链向下找。


找到了trx_id=50的数据,这条数据是事务A开启查询之前提交了,可以返回。


所有,RR隔离级别下,事务多次查询,它的ReadView是不变的,这与RC是不同的,RC隔离级别下,每次查询都会生成应给ReadView。


RR隔离级别下,就这样解决了不可重复读问题。


由于RR隔离级别下,ReadView只会生成一次,那么你可以简单的理解成,MySQL多事务并发执行时,只能查询到事务id比小于等于自己的数据。


其实幻读的解决方法与解决不可重复读原理是一样的,笔者这里就不再多赘述,有兴趣的同学可以自己整理下思路,在脑子里过一下它内部的运行流程。


有道无术,术可成;有术无道,止于术

欢迎大家关注Java之道公众号


好文章,我在看❤️

浏览 26
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报