深入理解MVCC与BufferPool缓存机制

一、MVCC多版本并发控制机制

MySql在可重复读隔离级别下如何保证事务较高的隔离性,我们上篇文章中提到过,同样的sql查询语句在一个事务里多次执行查询结果相同,就算其它事务对数据有修改也不会影响当前事务sql语句的查询结果

这个隔离性就是靠MVCC(Multi-Version Concurrency Control)机制来保证的,对一行数据的读和写两个操作默认是不会通过加锁互斥来保证隔离性,避免了频繁加锁互斥,而在串行化隔离级别为了保证较高的隔离性是通过将所有操作加锁互斥来实现的。

Mysql在读已提交可重复读隔离级别下都实现了MVCC机制。

二、undo日志版本链与read view机制详解

undo日志版本链是指一行数据被多个事务依次修改过后,在每个事务修改完后,Mysql会保留修改前的数据undo回滚日志,并且用两个隐藏字段trx_id和roll_pointer把这些undo日志串联起来形成一个历史记录版本链。
在这里插入图片描述
在这里插入图片描述

可重复读隔离级别,当事务开启,执行任何查询sql时会生成当前事务的一致性视图(只在第一次查询的时候生成,中间如果没有修改操作则保持不变)read-view,该视图在事务结束之前都不会变化(如果是读已提交隔离级别在每次执行查询sql时都会重新生成),这个视图由执行查询时所有未提交事务id数组(数组里最小的id为min_id)和已创建的最大事务id(max_id)组成,事务里的任何sql查询结果需要从对应版本链里的最新数据开始逐条跟read-view做比对从而得到最终的快照结果。

在这里插入图片描述
read-view:[100, 200], 300

已提交事务:小于数组中最小id的所有事务
未开始事务:大于已创建事务id的所有事务
未提交与已提交事务: 在数组中的事务id, 包含了未提交的事务和已提交的事务。这里面包含的未提交的事务id一定是大于等于200;已提交的事务id可能小于200. 注意,这个数组中的事务id一定是小于300的,但不一定是大于等于100的。比如一个事务id为80的,可能事务提交的比较慢,就有可能落在这个区间中。

版本链比对规则:

1、如果 row 的 trx_id 落在绿色部分( trx_id<min_id ),表示这个版本是已提交的事务生成的,这个数据是可见的;

2、如果 row 的 trx_id 落在红色部分( trx_id>max_id ),表示这个版本是由将来启动的事务生成的,是不可见的(若row 的 trx_id 就是当前自己的事务是可见的);

3、如果 row 的 trx_id 落在黄色部分(min_id <=trx_id<= max_id),那就包括两种情况:

  • a. 若 row 的 trx_id 在视图数组中,表示这个版本是由还没提交的事务生成的,不可见(若 row 的 trx_id 就是当前自己的事务是可见的);
  • b. 若 row 的 trx_id 不在视图数组中,表示这个版本是已经提交了的事务生成的,可见。

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

注意

begin/start transaction 命令并不是一个事务的起点 ,在执行到它们之后的第一个修改操作InnoDB表的语句 ,事务才真正启动 ,才会向mysql申请事务id,mysql内部是严格按照事务的启动顺序来分配事务id的。

总结:

MVCC机制的实现就是通过read-view机制与undo版本链比对机制,使得不同的事务会根据数据版本链对比规则读取同一条数据在版本链上的不同版本数据。

三、实战分析(可重复读隔离级别)

在这里插入图片描述
1、事务id为100的在第3行进行了更新操作,其实才是才会生成事务id,并不是begin或者start transaction的时候就生成事务id的;

update test set c1 = '123' where id =1;

2、第4行的时候,事务id为200的执行更新操作;

update test set c1 = '666' where id =5;

3、第5行的时候,执行更新操作,生成事务id为300的;

update account set name = 'lilei300' where id = 1

4、第6行的时候,事务id300提交了事务。此时的read-view:[100, 200], 300 .

执行到表格第7行的时候,此时的read-view是:[100, 200],300。 read-view是一个数组,由数组里最小的事务id未提交的事务id已创建的最大的事务id组成。这里,100是数组中最小的事务id,100和200是未提交的事务id,此时最大的事务id是300, 由这三个事务id构成了此时的read-view。

在这里插入图片描述
5、继续执行第8行,select 1这个事务进行了查询操作。此时的第8行的查询结果是:lilei300. 这个是怎么计算出来的呢?

select name from account where id = 1;   

此时的read-view:[100, 200], 300

根据上面的比对规则,首先会从undo日志的最新记录开始查询,查询当前这个事务到底应该读到哪一条数据记录。此时需要判断是展示lilei还是liei300?此时最新的记录的事务id是300,300不在活跃的视图数组中,根据比较规则,这个版本是已经提交了的事务生成的,可见。

6、继续执行第10和第11行,事务id100进行了两次更新操作;

update account set name = 'lilei1' where id = 1;

update account set name = 'lilei2' where id = 1;

此时的undo日志链为:
在这里插入图片描述
7、此时select 1这个事务再执行12行的查询操作:

select name from account where id = 1;

因为此次查询的事务在之前已经生成了read-view: [100, 200] ,300. 之后这个事务也没有进行修改操作,所以它的read-view依然保持不变。 即此时的read-view: [100. 200] ,300. 而此次要查找的数据链的事务id为100(蓝色记录),应当是落在未提交与已提交事务的活跃数组中,。100在read-view的活跃事务数组中,属于未提交的事务,则对该记录不可见。继续随着版本链向上判断,找到lilei1, 100的数据,依然不可见。继续向上查询,直到找到事务lilei300id为300的可见。即此时查询到的结果为lilei300.

8、继续执行第13行,此时事务100进行了提交,事务200也发出了两次修改操作:

// 事务200
update account set name = 'lilei3' where id = 1
update account set name = 'lilei4' where id = 1

在这里插入图片描述

9、然后在第16行select 1这个事务执行了查询操作:

select name from account where id = 1;  

此时我们再来分析,select 1这个事务的read-view视图在可重复图的隔离级别下,只要不发生新的修改操作,一直是第一次查询时生成的read-view: [100, 200], 300 。根据判断规则,此时的版本连事务id是200,根据判断规则,200在活跃事务数组中,此条记录不可见。继续向上查询,200,100,100这三条记录都不见,知道找300,不再活跃事务数组中,即可见。所以此次查询结果为lilei300.

注意: 查询是不会生成新的事务id的。只有更新,添加的时候才会生成。我们可以这样理解,生成事务是为了回滚做准备,查询不需要回滚,自然不会生成。

注意: 不论我们的事务有没有提交,更新记录都会被记录到版本连中。

注意: 此时数据库中的落地的数据记录是lilei2! 而不是lilei4! 因为lilei4的事务还没有提交!这里和数据库的隔离级别也没有关系。隔离级别是对读取数据的限制,并不影响数据库中真实数据的存储。 至于未提交的lilei3和lilei4,这些未提交的记录会记录在版本连undo日志中,是作为回滚用的。

注意: 版本链只有一份,但是read-view是每个事务都有自己的。

10、此时在第16行的时候,select 2 开始了自己的第一次查询:

select name from account where id = 1;  

我们来分析一下select 2这个事务此时的read-view. 此时未提交的事务是200,最大的已经创建的事务id是300. 根据判断规则,此时的版本链事务id是200, 200在活跃事务数组中,所以该条记录lilei4不可见。继续向上寻找,版本链lilei3, 200依然不可见。继续向上查询,版本链lilei2, 100这个记录,事务id为100,100小于最小活跃数组中的最小元素,所以该条记录lilei2可见。 所以此次查询结果是lilei2.

此时数据库中的落地数据也是lilei2. 我们结合之前的知识,事务第一次查询的数据一定是数据库中真实的数据,这和我们版本链判断规则中的判断刚好也符合。

我们现在在全局的角度再来分析一下select 1,select 1第一次查询的结果是lilei300. 后面即使有其他事务修改并提交了数据,但是select 1根据版本链查询规则查询出来的结果依然是第一次查询的结果lilei300. 这就是MVCC在可重复读这个隔离级别下所体现的作用。

四、实战分析(读已提交隔离级别)

读已提交和可重复读的区别是,读已提交是每一次查询的时候都会生成read-view。 而可重复读是只在第一次查询的时候生成read-view。 这也是为什么可重复读的本质。

在这里插入图片描述
针对第16行,我们select 1的查询结果是多少呢?我们来分析一下:

首先该次select 1查询会生成一个新的read-view: [200], 300. 而现在最新的版本链记录id是:
在这里插入图片描述
最新的版本链记录id是200,,根据比对规则,200在活跃事务数组中,则该条lilei4数据不可见。

继续向上查询,lilei300的数据,事务id也是200, 依然不可见。

继续向上查询,知道查询到lilei2,事务id为100。 100小于活跃数组中的最小事务id200,则该条记录可见。即查询结果为lilei2.

我们再从宏观的角度分析一下,现在的隔离级别是读已提交,那么此次查询自然是要读取到数据库中最新提交那一次数据。通过表格,我们发现上一次commit是在13行,commit的数据正好是lilei2.

这样我们从宏观和微观的角度上都解释了读已提交这个隔离级别的查询机制。

五、Innodb引擎SQL执行的BufferPool缓存机制

Mysql中的增删改查都是基于Buffer Pool中的数据来进行的。可能磁盘上的数据还没有被更新到最新,但是buffer pool中的就是最新的数据。
在这里插入图片描述
当我们将数据更新到了Buffer Pool,还没有来得及写入到磁盘上的时候,如果数据库服务挂了,会怎么样?

InnoDB存储引擎后台有一个IO线程以page为单位、随机将buffer pool中的记录写入到磁盘中(刷盘)。
当我们将图上的第7步做完后,java程序认为事务已经提交,数据也已经成功修改了,但是mysql服务还没来得及将更新写入到磁盘上却挂了。此时磁盘上还是老数据。更新岂不是丢失了?

其实不是这样的,因为我们修改的那条数据已经落实到redolog中去了,当数据库服务再次重启的时候,可以使用redolog中的数据来恢复数据,从而保证我们的更新不丢失。这是一种比较好的思想,我们可以考虑使用到我们自己的项目中。

注意

redolog:是用来恢复buffer pool中的数据的。当数据库对数据做修改的时候,需要把数据页从磁盘读到buffer pool中,然后在buffer pool中进行修改,那么这个时候buffer pool中的数据页就与磁盘上的数据页内容不一致,称buffer pool的数据页为dirty page 脏数据,如果这个时候发生非正常的DB服务重启,那么这些数据还没在内存,并没有同步到磁盘文件中(注意,同步到磁盘文件是个随机IO),也就是会发生数据丢失,如果这个时候,能够在有一个文件,当buffer pool 中的data page变更结束后,把相应修改记录记录到这个文件(注意,记录日志是顺序IO),那么当DB服务发生crash的情况,恢复DB的时候,也可以根据这个文件的记录内容,重新应用到磁盘文件,数据保持一致。

undolog:undo日志用于存放数据修改被修改前的值,联想版本链,就是undolog记录了每个事务对应的数据;

binlog:是server层的,所有存储引擎都有binlog。而undolog和redolog是InnoDB存储引擎特有的。

假设有A、B两个数据,值分别为1,2,开始一个事务,事务的操作内容为:把1修改为3,2修改为4,那么实际的记录如下(简化):

A.事务开始.
B.记录A=1到undo log.
C.修改A=3.
D.记录A=3到redo log.
E.记录B=2到undo log.
F.修改B=4.
G.记录B=4到redo log.
H.将redo log写入磁盘。
I.事务提交

为什么Mysql不能直接更新磁盘上的数据而且设置这么一套复杂的机制来执行SQL了?

1、因为来一个请求就直接对磁盘文件进行随机读写,然后更新磁盘文件里的数据性能可能相当差。

2、因为磁盘随机读写的性能是非常差的,所以直接更新磁盘文件是不能让数据库抗住很高并发的。

3、Mysql这套机制看起来复杂,但它可以保证每个更新请求都是更新内存BufferPool,然后顺序写日志文件,同时还能保证各种异常情况下的数据一致性。

4、更新内存的性能是极高的,然后顺序写磁盘上的日志文件的性能也是非常高的,要远高于随机读写磁盘文件。正是通过这套机制,才能让我们的MySQL数据库在较高配置的机器上每秒可以抗下几干的读写请求。

5、随机读写磁盘效率(随机IO)是非常低的,而写redolog的时候是顺序读写(顺序IO)磁盘的,效率是比较高的。Kafka也是顺序IO的,几乎不会删除数据,它连续写磁盘,使用偏移量连续读磁盘,效率是非常高的。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值