【MySQL】深入理解MVCC与BufferPoll缓存机制
【MySQL】深入理解MVCC与BufferPoll缓存机制
(1)从磁盘中加载 id=1 的整页数据到Buffer Pool
(2)从Buffer Pool将老数据写入undo日志文件中
(7)从 binlog 向 redo日志 写入 commit标记,提交事务完成
(8)将 Buffer pool 中待修改数据以页为单位,通过IO线程随机写入磁盘
为什么MySQL不能直接更新磁盘上的数据而且设置这么一套复杂的机制来执行SQL了?
【MySQL】深入理解MVCC与BufferPoll缓存机制
MVCC多版本并发控制机制
首先说一下什么是MVCC。
其英文全称就是 Multi-Version Concurrency Control(多版本并发控制机制)
MySQL在可重复读隔离级别下如何保证事务较高的隔离性?在上一篇文章中提到过!
同样的SQL查询语句在一个事务里多次执行查询结果相同,就算其它事务对数据有修改也不会影响当前事务SQL语句的查询结果。
MySQL是如何做到这一点的?这个隔离性主要是依靠MVCC机制来保证的!!!在读已提交和可重复读这两个隔离级别下,它们的隔离性都是通过MVCC机制来实现的!
而串行化也可以保证隔离性(高),但是是通过加锁互斥来实现的!有时候连select,也要加上读锁!性能是相当的低,所以99%是不会去使用它的!
我们最常用的还是可重复读这个隔离级别!
MVCC机制是通过 undo日志 和 read view机制来实现的,所以我们先来看看这两个家伙。
undo日志版本链与read view机制详解
undo日志版本链
undo日志版本链是指一行数据被多个事务依次修改过后,在每个事务修改完后,MySQL会保留修改前的数据undo回滚日志,并且用两个隐藏字段 trx_id 和 roll_pointer 把这些undo日志串联起来形成一个历史记录版本链。
通过我在图片上的标注,相信大家应该可以看明白 undo日志版本链 的大致流程。
trx_id :事务ID
roll_pointer: 指针
需要注意的是这个事务ID:
begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个修改(insert、update、delete)操作InnoDB表的语句,事务才真正启动,才会向MySQL申请事务id!
而在事务中的select语句是不会向MySQL申请事务id的!
MySQL内部是严格按照事务的启动顺序来分配事务id的。
read view机制
在可重复读隔离级别,当事务开启,执行任何 查询SQL 时会生成当前事务的一致性视图read-view。
该视图在本事务结束之前都不会变化!!!(如果是读已提交隔离级别在每次执行查询SQL时都会重新生成)。
这个视图由执行查询时所有未提交事务id数组(数组里最小的id为min_id)和已创建的最大事务id(max_id)组成。
事务里的任何SQL查询结果需要从对应版本链里的最新数据开始逐条跟read-view做比对从而得到最终的快照结果。
版本链比对规则(重点!!!)
(1)如果 row 的 trx_id 落在绿色部分( trx_id
(2)如果 row 的 trx_id 落在红色部分( trx_id>max_id ),表示这个版本是由将来启动的事务生成的,是不可见的(若 row 的 trx_id 就是当前自己的事务是可见的);
(3)如果 row 的 trx_id 落在黄色部分(min_id
a. 若 row 的 trx_id 在视图数组中,表示这个版本是由还没提交的事务生成的,不可见(若 row 的 trx_id 就是当前自己的事务是可见的);
b. 若 row 的 trx_id 不在视图数组中,表示这个版本是已经提交了的事务生成的,可见。
注:
我们用下来的例子,里面的min_id为100,里面的max_id为300.
案例说明
我们现在已一张account表为例,来讲述这个比较过程!
不妨假设原先的account表中id=1是,name的值为lilei
当前案例中,有3个事务分别是Transaction 100、Transaction 200、Transaction 300 。
在第四列,第四个事务(从左往右数)中,执行了一条select语句,所以会生成一个read-view
当执行到这一个 select语句 的时候,Transaction 300已经commit(提交了),而Transaction 100、Transaction 200还没有提交。所以,read-view中最大的事务ID为300,所有未提交事务id数组为[100,200]
这个最大的事务ID300,不在【100,200】这个活跃数组中,所以按照上面的规则来说是可见的,所以我们在第四个事务中执行select语句,得到的结果是name = 'lilei300'
undo版本链如下所示
接下来我们继续执行SQL
由于read-view在可重复读这个隔离级别中,一旦生成就不会变(除非在本事务中执行了update之类的语句)
所以readview[100,200],300
Transaction 100里面的update操作,由于它的事务ID为100,在这个数组【100,200】中,所以对于第四个事务的select还是不可见的!
undo版本链如下所示
接下来我们继续执行SQL
undo版本链如下所示
逻辑也和上面的几个步骤一样,readview仍然是不会变的。
Transaction 200的事务ID为200,也一样会落在黄色的位置,200在视图数组【100,200】中,所以不可见,一次往下找最后还是会找到Transaction 300!!!
此时,我们数据库中实际存放的name为 lilei2(Transaction 200还未commit!),而第四个事务所能查询到的name仍然为lilei300。
如果此时还有一个事务我们称其为第五个事务,具体看下图:
显然,这里生成的readview为[200],300 ,select出的结果是lilei2。
trx_id = 100的事务,是已提交事务(绿色),按照上面的规则,是可见的,所以我们select得到的结果是lilei2!
MVCC总结(undo + readview)
MVCC机制的实现就是通过read-view机制与undo版本链比对机制,使得不同的事务会根据数据版本链对比规则读取同一条数据在版本链上的不同版本数据。
Innodb引擎SQL执行的BufferPool缓存机制
关于SQL底层执行原理,我们在之前的文章里面有提到,但是之前主要是提了Server层的各个组件,真正重要的还是InnoDB存储引擎底层的实现机制!
InnoDB引擎执行流程
我们就以上面的这一张图为例
(1)从磁盘中加载 id=1 的整页数据到Buffer Pool
存储引擎为InnoDB的表,且用的又是B+树的索引,进行数据读写(IO)的时候,都是以“页(16KB)”为单位的。
(2)从Buffer Pool将老数据写入undo日志文件中
之前id=1的记录,name为"zhuge",这边执行的update语句,将其更新为"zhuge666"。id=1 的整页数据加载到Buffer Pool后,会将老数据(zhuge)写入undo日志文件中,便于回滚。
(3)更新Buffer pool中的数据
不会更新磁盘中的数据,这一步只是更新内存数据!
(4)向Redo Log Buffer写入redo日志
将变更数据”zhuge666“写入内存(Redo Log Buffer),有可能时批量写入!
(5)准备提交事务,redo日志写入磁盘
注意 redo日志 是InnoDB特有的!
(6)准备提交事务,binlog 日志写入磁盘
(7)从 binlog 向 redo日志 写入 commit标记,提交事务完成
commit标记主要是为了保证事务提交后,binlog与redo日志的数据一致性!
如果在提交完事务后,Buffer pool中的数据还未写回磁盘,此时MySQL宕机了怎么办?
这个时候redo日志就派上用场了!
在commit之前,数据变更已经写入了redo日志(磁盘),所以在下次MySQL恢复重启的时候,会先把redo日志中的数据加载到Buffer Pool中!
正是因为InnoDB中有一个 Buffer pool 缓存池,才会需要redo日志来恢复它,以保证数据的可靠性、一致性!
(8)将 Buffer pool 中待修改数据以页为单位,通过IO线程随机写入磁盘
这个写入磁盘是通过IO线程来完成的,但是什么时候写入是随机的,是MySQL刷盘策略所决定的!
为什么MySQL不能直接更新磁盘上的数据而且设置这么一套复杂的机制来执行SQL了?
因为来一个请求就直接对磁盘文件进行随机读写,然后更新磁盘文件里的数据性能可能相当差。
因为磁盘随机读写的性能是非常差的,所以直接更新磁盘文件是不能让数据库抗住很高并发的。
MySQL这套机制看起来复杂,但它可以保证每个更新请求都是更新内存BufferPool,然后顺序写日志文件,同时还能保证各种异常情况下的数据一致性。
更新内存的性能是极高的,然后顺序写磁盘上的日志文件的性能也是非常高的,要远高于随机读写磁盘文件。
正是通过这套机制,才能让我们的MySQL数据库在较高配置的机器上每秒可以抗下几干甚至上万的读写请求!!!