在上一篇文章《事务隔离级别与锁机制》中介绍了,读已提交和可重复度隔离级别都用到了MVCC机制,MySQL在可重复读
的事务隔离级别下,可以解决数据的不可重复读问题,就算其他事务对访问的数据进行了修改,也不影响当前事务两次读取的结果。而这个隔离性就是由**MVCC(Multi-Version Concurrency Control)**机制来保证的,对一行数据的读写操作不要进行加锁就可以保证隔离性,达到了一致性非锁定读
的效果。
注:MySQL在读已提交和可重复读两种事务隔离级别下都实现了MVCC机制
这篇文章就简单介绍一下MVCC机制是如何实现的,顺带简单介绍一下Buffer Pool,后面还有文章详细介绍。
一、MVCC
MVCC机制可以在不加锁的情况实现事务的隔离性,其基本原理就是不同的事务持可以看见不同版本的记录,而不同版本的记录就与undo log有着密切的关系,所以在介绍MVCC之前,需要先对undo log有个初步的了解,关于undo log的具体介绍,可以参考后面的文章。
1.1 undo log
undo log用于事务的回滚,可以利用undo log将数据恢复到修改之前的样子。
undo日志存放在数据库内部的一个特殊段(segment)中,这个段称为undo段,undo段位于共享表空间内(也可以配置存在undo表空间中)。
undo log是逻辑日志,并不是物理日志,因此只能将数据库逻辑的恢复到原来的样子,但数据结构和页在回滚之后可能大不相同。
注:事务在undo log segment分配页并写入undo log的过程中,同样需要写 redo log
除了回滚操作,undo的另一个作用就是MVCC,MVCC是通过undo log的版本链来实现的。关于undo 段的内容,可以参考后面介绍索引结构的文章。
1.2 undo log版本链
在MySQL的数据表中,对于每行数据而言,不仅记录了定义的字段值,还隐藏记录了两个字段:row_trx_id
和roll_pointer
,前者表示更新该行数据的事务ID,后者表示的是回滚指针。它指向的是该行数据上一个版本的undo log
在Innodb存储引擎中,undo log分为两类:
- insert undo log
- update undo log
insert undo log
是当前事务执行insert
时产生的,它只对当前事务可见,而对其他事务是不可见的,所以当前事务提交时,可以直接删除insert undo log。而update undo log
包含了update
和delete
操作产生的undo log。该undo log可能需要提供MVCC机制,也就是可能还有其他已经开启的事务需要访问这些修改的记录。因此,当前事务在提交时,不能直接删除undo log,需要将其放入到undo log版本链中。
1.2.1 undo log行结构
![](https://i-blog.csdnimg.cn/blog_migrate/1120fcdf95d3c98c0c7acb1d6905364d.png)
1.2.2 undo log版本链结构
以account
表为例,其表结构如下:
CREATE TABLE account(
id int NOT NULL AUTO_INCREMENT,
name VARCHAR(255) DEFAULT NULL,
balance INT DEFAULT NULL,
PRIMARY KEY(id)
)ENGINE=InnoDB DEFAULT CHARSET=utf8;
下图展示了一个undo log版本链的结构,这也是实现MVCC机制的基本要素:
![](https://i-blog.csdnimg.cn/blog_migrate/18adbbfcc6db3252b7ec983e53c8415b.png)
注:每条记录都对应一个undo log版本链,如果某条记录上的事务都提交了,那么会由后台线程去清理对应的undo log版本链。
1.3 read view(一致性试图)
有了undo log版本链之后,就可以做到每行记录可以有多个版本的记录,但对于一个事务来说,那些是可见的,哪些是不可见的,这就是read view的作用了。
在可重复读隔离级别当中,当事务开启,执行任何查询SQL时会生成当前事务的一致性试图read-view
(需要注意的是:read-view
并不是在事务开启时就生成,而在是执行第一次查询SQL时才生成),该视图在当前事务结束之前都不会变化。
该试图由执行查询时,所有未提交事务的事务id数组(数组中最小的id为min_trx_id
)和已创建的最大事务id(max_trx_id
)组成。
事务里面的任何SQL查询结果需要从对应版本链中最新的数据开始逐条与read-view
做对比,从而得到最终的快照结果。
注:如果是在读已提交的隔离级别下,每次执行查询SQL时,都会重新生成read-view
1.3.1 read-view描述
![](https://i-blog.csdnimg.cn/blog_migrate/48b152af67d86415fccd3de802174e24.png)
按照read-view
的描述,可以将事务分成三个范围,根据这三个范围,可以判断版本链的数据是否对当前事务可见。
虽然视图是由未提交事务的事务id数组组成,但从上面的图可以看出,黄色区域还包含了已提交事务,这是因为最大的事务ID对应的事务,可能是已经提交的事务。
1.3.2 版本链比对规则
undo log
版本链有了,read-view
一致性视图也有了,那么就可以判断哪些记录是可见的,哪些是不见的,对此有相应的判断规则。
当前事务已经创建了read-view
,在读取数据的时候,就需要根据版本链中的事务id进行判断,以此来决定undo log的数据是否可见,比对规则有如下几种:
1、如果undo记录的trx_id
位于read-view
的绿色部分(trx_id < min_trx_Id
),表示这个版本是已提交的事务生成的,所以数据可见。
2、如果undo记录的trx_id
落在粉红色区域(trx_id > max_trx_id
),表示当前版本是由将来启动的事务生成,是不可见的。(如果数据行的trx_id
就是当前事务的trx_id
,那么数据是可见的)
3、如果undo记录的trx_id
落在黄色区域(min_trx_id =< trx_id =< max_trx_id
),那就有两种情况:
- 如果
trx_id
位于未提交事务ID数组
中,表示这个版本是由还未提交的事务生成的,因此,不可见。(如果trx_id
就是当前事务的id,则可见) - 如果
trx_id
不在未提交事务ID数组
,说明这个版本是由已提交的事务生成,也就是trx_id
为一致性视图中最大的事务ID,且该事务已经提交了,因此可见。
1.4 read-view比对流程
read-view
在可重复读和读已提交两种隔离级别下,其实现是有些不同的,下面分别就这两个隔离级别,细看一下它们的比对流程。
1.4.1 可重复读隔离级别
分别开启了三个事务,然后在不同阶段执行三次相同的查询:
如表格所示,在查询前,开启了三个事务,其中trx_id
为300的事务在查询前就提交了,于是,可以根据查询依次展示undo log的版本链,每次查询对应的undo log版本链都有所不同。
第一次查询:
第二次查询:
第三次查询:
这三次查询的比对流程如下:
因为是在可重复读隔离级别
,所以第一次查询时生成了read-view
后就不会再变化,第一次查询时,生成的一致性视图为[100,200],300
,其中数组中为未提交事务,300为当前最大事务。
1、第一次查询时,undo 链只有一条记录,且记1录的trx_id
为当前落在数组之外,所以,数据可以读
2、第二次查询时,最新记录的trx_id
为100,在未提交事务ID数组
里面,数据不可见;于是继续向下遍历,然后事务id为300的记录可见
3、第三次查询时,与第二次查询一样,前两次比对的记录的事务id都在未提交事务id数组
中,数据均不可见,所以,最后读取到的还是事务id为300的记录数据
1.4.2 读已提交隔离级别
开启了三个事务,然后在不同阶段执行三次相同的查询,在读已提交隔离级别
中,每次查询都会生成新的read-view
:
三次查询是的undo log版本链与上面可重复读隔离级别的undo log版本链是一致的。查询的比对流程如下:
第一次查询和第二次查询时,由于没有新的事务提交,所有read-view
没有发生变化,读取的数据也都是事务id为300的那一条数据
而在第三次查询时,前面事务100提交了,于是第三次查询的read-view
就变成了[200],300
,根据比对规则,在对undo链上第二条事务id为100的记录进行比对时,发现它比未提交事务id数组
中所有的事务id都小,说明这是一个已提交的事务,所以可见。于是该版本的数据就是查询的数据。
注:正是因为读已提交隔离级别每次查询都要生成新的一致性试图,才导致不可重复读问题
二、Buffer Pool
在InnoDB存储引擎中,一条更新的SQL语句,其详细的执行流程如下:
在Innodb
存储引擎中,MySQL对于数据的操作并不是直接写磁盘的。而是通过BufferPool
的缓存机制来实现。
以下面的SQL为例:
update account set name = 'lihehe' where id =1
数据的更新操作分为以下几步:
1、加载id=1
的记录所在页的整页数据到BufferPool
中
2、将旧值写入到undo log中,便于回滚
3、SQL执行器按照条件更新缓存中的数据
4、执行器将redo log写入到redo log buffer
中
5、准备提交事务,redo log写入到磁盘中
6、准备提交事务,binlog 写入到磁盘(用来数据恢复)
7、binlog写入完成后,写入commmit标记到redo log中。提交事务完成,该标记为了保证事务提交后redo与binlog的数据一致性
8、最后由后台线程将BufferPool
中的数据以页为单位随机写入到磁盘
注:这里只是简单的介绍一下Buffer Pool,后面还会有大量文件,详细介绍这些步骤的操作。