MVCC

MVCC

首先我们都知道,mysql中的读操作最容易出现的三大问题:脏读、幻读、不可重复读

脏读(一个事务读取到了另一个事务未提交的数据):

时间轴(可以自行把时间的流逝当做一条时间线)事务A事务B
T1开始事务开始事务
T2修改任意一张表中的任一记录(例如修改account表中的zhangsan余额,将余额由1000修改为2000)
T3查询zhangsan的余额,结果为2000
T4提交事务

以上事务A读取到了事务B还未提交的数据,就会产生脏读

不可重复读(一个事务中,两次读取到的数据不一致):

时间轴(可以自行把时间的流逝当做一条时间线)事务A事务B
T1开始事务开始事务
T2查询zhangsan的余额,结果为1000
T3修改任意一张表中的任一记录(例如修改account表中的zhangsan余额,将余额由1000修改为2000)
T4提交事务
T5查询zhangsan的余额,结果为2000

我们可以很明显的看出来脏读与不可重复读的区别:脏读是一个事务读取另一个事务未提交的数据,然而不可重复读是一个事务读取另一个事务已经提交的数据。

幻读(在一个事务中,按照某个条件先后查询两次数据库,两次查询的结果条数不同):

时间轴(可以自行把时间的流逝当做一条时间线)事务A事务B
T1开始事务开始事务
T2查询0 < id < 5 的所有用户的余额。
假设当前查出来的用户只有一条
T3账户余额表中插入一条新的用户
T4提交事务
T5查询0 < id < 5 的所有用户的余额。
现在查出来的当前用户有两条

由此我们可以通俗的理解:

不可重复读是数据变了,然而幻读是数据的行数变了。

Mysql中解决脏读、幻读、不可重复读,使用的技术是MVCC(Multi-Version Concurrency Control)—多版本并发控制(mysql事务的隔离级别为 REPEATABLE-READ,可重复读

首先说明MVCC中的数据结构(或者说MVCC是由什么组成的)

1)隐藏列:InnoDB存储引擎中每行数据列都有隐藏列,隐藏列中包含了本行数据的事务id、指向undo log的指针等。

补充一个知识点:undo log(撤销或回滚日志)可以这样理解:

它记录了事务发生前的数据状态,但是除了SELECT之外。如果修改数据的时候发生异常,可以使用undo log 来实现回滚操作。

但是它仅仅是逻辑日志,仅仅是将数据从逻辑上恢复事务之前的状态,而不是从物理上实现的,如果想从物理上实现回滚操作,还需要redo log来配合

redo log现在暂时用不上,后面如果涉及到它的作用之后我们在说。

2)基于undo log的版本链:每条undo log也会指向更早的undo log,这样就形成了一条数据的版本链。

3)read-view:一致性视图,可以理解当第一条SELECT进来的时候,InnoDB存储引擎会在内存中生成一个当前所有事务的状态的快照(记住是所有),由于我们mysql是REPEATABLE-READ,那么这个视图会一直沿用下去,ORACLE数据库使用的是READ-COMMITTED(读已提交),ORACLE每次SELECT的时候都会生成一个read-view。

上图中每一个数据行的后面,没有任何填充的格子我们可以叫他undo log,上面说过undo log的指向,由此就构成了一条数据的版本链。

我们先看一些操作:

温馨提示:如果看不清的话可以点击这里

最左边的,有序增长的数据,我们依旧可以当做是一条时间线。

当执行查询sql时会生成一致性视图read-view,它由执行查询时所有未提交事务id数组(数组里面最小的id为 min_id )和已经创建的最大事务id(max_id)组成,查询的数据结果需要跟read-view做比对从而得到快照结果。

然后我们在来看一下MVCC的规则:

版本链比对规则:
1、如果落在绿色部分(trx_id < min_id),表示这个版本是已提交的事务生成的,那么这个数据是可见的;
2、如果落在红色的部分(trx_id > max_id),表示这个版本是由将来启动的事务生成的,是肯定不可见的;
3、如果落在黄色部分(min_id <= trx_id <= max_id),那就包含两种情况
     a.若 row的trx_id在数组中,表示这个版本是由还没提交的事务生成的,不可见,当前自己的事务是可见的
     b.若row的trx_id 不在数组中,表示这个版本是已经提交了的事务生成的,可见。

对于删除的情况可以认为是update的特殊情况,会将版本链上最新的数据复制一份,然后将trx_id修改成删除操作的trx_id,同时在该条记录的头信息(record header)里的(deleted flag)标记位写上true,来表示当前记录已经被删除,在查询时按照上面的规则查到对应的记录,如果delete flag标记为true,意味着记录已经被删除,则不返回数据。

得知这些之后,我们来看一下在序号7这个时间点上进行了SELECT操作之后,会有什么样的效果:

1、首先会生成一个一致性的视图read-view,当前的read-view中:

2、未提交的事务数组中:最小的事务id是100,最大的未提交事务id是200,然后最大的事务id是300(已经提交)

ps:上方图片的右上角

3、这时我们看以序号7为时间点上的undo log版本链:

4、此时我们可以在上述规则中进行对比,发现它落在了黄色部分,但是事务id为300 的事务不在数组中(注意数组是未提交事务id形成的数组,不要记混

结论:所以此时查到的结果就显而易见:name=lilei300

然后我们再继续往下走,来到序号为10的时间点上

我们之前已经说过,mysql的隔离级别是REPEATABLE-READ,那么这个视图会一直沿用下去(思考未提交事务数组和最大事务id现在是多少?

此时我们看一下当前时间点上的undo log的版本链:

之后再按照上面的规则进行比较,发现现在仍然是落在了黄色部分,但是该行的事务id为100,在未提交的事务数组中,所以对本次SELECT操作时不可见的,就会继续顺着版本链向上找,直到找打了事务id为300这行记录。

所以当前结果很显然:name=lilei300

所以当前是不是就解决了我们说的脏读的问题?

那么假如我们在时间点序号为13处添加一条sql语句

select name from account where id = 1;

思考一下它现在的undo log版本链,以及当前的read-view。

现在我们来进行分析:

同样,如果看不清可以点击这里

我们使用的read-view仍然是第一次SELECT的时候使用的read-view(原因上面已经说过,不在赘述)。

此时undo log版本链:

现在的未提交事务数组:[100,200]

最大事务id:300

再与上面的规则进行比对:发现仍然是落在了黄色的部分,但是当前事务id在未提交数组中,此次查询是不可见的,然后按着顺序向上找,找到了事务id为100的记录,但是当前事务id仍然在未提交的数组中,所以此次查询仍然是不可见的。接着找到了事务id为300的那一行数据。

结果:name=lilei300。

思考:此时是否解决了不可重复读的问题?

答案:是的。

最后:大家可以按照此方法来试一下,MVCC解决幻读的过程。
本文参考:

​ 1、https://www.cnblogs.com/kismetv/p/10331633.html

​ 2、https://www.bilibili.com/video/BV1YJ411J7vb?t=1378

如有不对,欢迎指正。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值