[Mysql] 聊聊MVCC与Buffer Pool缓存机制

2023年 7月 24日 85.9k 0

Hi,大家好,我是抢老婆酸奶的小肥仔。

Mysql是我们日常中用的比较多的关系型数据库。今天我们就来聊一聊什么是MVCC与Buffer Pool缓存机制。

开聊!

1、MVCC

1.1 概念

MVCC:(Multi-Version Concurrency Control)多版本并发控制,主要解决事务的隔离性,对一行数据的读和写两个操作默认不通过加锁互斥来保证隔离性,避免频繁加锁互斥。

MVCC在读已提交和可重复读隔离级别都已实现

1.2 undo日志版本链与read view机制

1.2.1 undo日志版本链

undo日志:即记录Mysql执行的数据的历史,便于进行回滚。

undo日志版本链:指一行数据被多个事务进行修改后,在每个事务修改完成后,MySQL会保留修改前的数据undo回滚日志,并用两个隐藏字段trx_id和roll_pointer把这些undo日志串联起来形成一个历史记录版本链。如图:

注:trx_id:事务Id roll_pointer:指向上一条记录的指针。

1.2.2 read view

read view:一致性视图,在可重复读隔离级别,当事务开启后,第一次执行查询SQL时生成。在事务提交前,该视图不发生任何变化。(读已提交隔离级别,每执行查询SQL都生成新的read view)。

组成:未提交事务Id组成的数组(最小事务idmin_id)和已创建的最大事务Id(max_id)

例如:假设有三个事务,其事务id分别是100,200,300,都未提交,此时的readview为:read view:[100,200],300

版本链对比规则:

即将read view分作三部分:

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,意味着记录已被删除,则不返回数据。

2、Buffer Pool缓存机制

buffer pool:即以页的单位进行存储的mysql内存区域。默认大小:128M。是InnoDB存储引擎特有的。

Mysql的增删改查都是在Buffer pool上执行,之后会随机将buffer pool中的修改日志同步到磁盘中。

流程图:

步骤:1、将磁盘中的数据以所在页的形式,加载到buffer pool中

2、写入undo日志,保存当前数据的旧数据,便于进行事务回滚

3、更新buffer pool中的数据

4、写入redo日志,

5、准备提交事务,将redo日志写入磁盘,保证当事务提交后,数据没有写入磁盘时,能够恢复buffer pool数据

6、准备提交事务,将binlog日志写入磁盘

7、commit事务,标记到redo日志,保证redo日志与binlog日志一至

8、随机将buffer pool中的数据更新到磁盘中

2.1 怎样识别数据在哪个缓存页?

每个缓存页都对应一个描述数据块,该数据块包含数据页所属的表空间、数据页的编号,缓存页在Buffer Pool中的地址等。本身为一块数据,大小是缓存页大小的5%左右。因此,描述数据块在前,缓存页在后。如图:

2.2 Buffer Pool初始化与配置

1、Mysql启动时,根据配置参数innodb_buffer_pool_size的值来分配Buffer Pool内存大小

2、按缓存页默认大小16kb以及对应的描述数据块800字节左右大小,在Buffer Pool中划分一个个缓存页和描述数据块。

注:此时缓存页、描述数据块都为空

配置:

1、innodb_buffer_pool_size: 设置InnoDB Buffer Pool的总大小

2、innodb_buffer_pool_chunk_size: 当增加或减少innodb_buffer_pool_size时,操作以块(chunk)形式执行。块大小由innodb_buffer_pool_chunk_size配置选项定义。

配置规则:innodb_buffer_pool_size=innodb_buffer_pool_chunk_size * innodb_buffer_pool_instances*N(N>=1);

3、innodb_buffer_pool_instances: 设置InnoDB Buffer Pool实例个数,每个实例都有自己独立的list管理。innodb_buffer_pool_size必须是innodb_buffer_pool_chunk_size * innodb_buffer_pool_instances的整数倍,否则Buffer Pool会自动增大,调整为整数倍。

例如:size大小为9G,instances为16,chunk_size为128M,则Buffer Pool会自动调整为10G。

4、innodb_old_blocks_pct: 默认InnoDB Buffer Pool中点的位置,默认大小是37,最大100,即3/8的位置。

设置Buffer Pool大小及相关操作:

set global innodb_buffer_size=4050310212;   //设置size大小

SHOW STATUS WHERE Variable_name='InnoDB_buffer_pool_resize_status'  //查看Buffer Pool状态

SELECT @@innodb_buffer_pool_size/1024/1024;   //查看buffer pool大小,单位为G

show global status like 'Innodb_buffer_pool_pages_total';   //查询buffer pool总页数

show global status like 'Innodb_buffer_pool_pages_data'; //查询buffer pool包含数据的页数

语句执行后,需等待所有事务全部执行后才生效,而新连接和事务需等到设置生效后才能连接,否则一直处于等待状态

2.3 冷热数据分离

相对于传统LRU方法,Mysql采用了冷热数据分离的处理方案。

主要解决问题:

(1)预读失效:即相邻的两个页,只有其中一个页号被访问时,则Mysql会将相邻的另一个数据页也加载进Buffer Pool,此时就会淘汰LRU链表的最后一个缓存页,如果被淘汰的缓存页被经常访问,而预读的数据没有访问,因此会存在问题。

(2)缓存池污染:某个查询时扫描了大量数据或进行了全表扫描,会将缓存池中的数据全部替换。

实现逻辑:

因此Mysql采用了冷热数据分离,将LRU链表分成冷热两部分。如图:

冷数据区数据被加载到缓存后,1s (innodb_old_blocks_time参数值决定,单位ms) 内后再次被访问,在被移至热数据区域

2.4 Buffer Pool三种Page和链表

2.4.1 三种page

分别是:Free Page(空闲页)、Clean Page(干净页)、Dirty Page(脏页)

Free Page(空闲页):即缓存页中没有任何数据

Dirty Page(脏页):被修改的数据,即与磁盘中的数据不一致

Clean Page(干净页):没有被修改过的数据,且与磁盘中数据一致,只是做了查询操作

2.4.2 三种链表

LRU链表,Flush链表,Free链表

LRU链表: 最近最少使用链表,即将分为冷热数据区。

(1)冷数据区占整个LRU链表比例的3/8,由innodb_old_blocks_pct控制,默认值是37(3/8 * 100),值范围:5~95

(2)新页被读取到Buffer Pool时,会插入热数据区域后,冷数据区后的中间位置,即Mid Point

(3)频繁被访问的数据,会往热数据区域头部移动,预读的数据如果短时间内没有被访问,则很可能在下次访问前被移到冷数据区域尾部而被移除

(4)新数据的不断添加,会不断插入Mid Point,冷数据区域会被不断移至尾部,常被访问的数据移至热数据区域头部,那些没被访问的页会逐渐移至热数据区域而被移除。

注:只有一个被访问数据页处于热数据区域长度的1/4(大约值)之后,才会被移动到热数据区域链表的头部。这样做的目的是减少对LRU 链表的修改,因为LRU 链表的目标是保证经常被访问的数据页不会被驱逐出去。

Flush链表:

(1)保存的都是脏页,且保存的页存在于LRU链表

(2)按照oldest_modification排序,值大的在头部,值小的在尾部

(3)当页面被修改时,使用mini-transaction,对应的page进入Flush链表

(4)page第一次修改时进入Flush,其他次数修改不加入

(5)Page Cleaner线程执行flush操作时,从Flush链表开始scan,将脏页写入磁盘,突进检查点,减少recover时间。Flush链表中脏页刷回磁盘中,MySQL提供了一个后台线程执行。

Free链表:

(1)空闲页面,初始化的时候申请一定数量的页面,Mysql使用过程中,空闲页不断不断减少

(2)执行SQL时,数据加载到内存后,会判断Free链表页够不够,不够时执行flush LRU链表和Flush链表来释放空间。够用时,删除对应页面,LRU链表中添加。

2.5 LRU 链表和Flush链表的区别

  • LRU 链表 flush,由用户线程触发(MySQL 5.6.2之前);而Flush 链表 flush由MySQL数据库InnoDB存储引擎后台srv_master线程处理。(在MySQL 5.6.2之后,都被迁移到Page Cleaner线程中)。
  • LRU 链表 flush,其目的是为了写出LRU 链表尾部的脏页,释放足够的空闲页,当Buffer Pool满的时候,用户可以立即获得空闲页面,而不需要长时间等待;Flush 链表 flush,其目的是推进Checkpoint LSN,使得InnoDB系统崩溃之后能够快速的恢复。
  • LRU 链表 flush,其写出的脏页,需要从LRU链表中删除,移动到Free 链表。Flush List flush,不需要移动page在LRU链表中的位置。
  • LRU 链表 flush,每次flush的脏页数量较少,基本固定,只要释放一定的空闲页即可;Flush 链表 flush,根据当前系统的更新繁忙程度,动态调整一次flush的脏页数量,量很大。
  • 在Flush 链表上的页面一定在LRU 链表上,反之则不成立。
  • 2.6 触发刷脏页的条件

  • REDO日志快用满的时候。由于MySQL更新是先写REDO日志,后面再将数据Flush到磁盘,如果REDO日志对应脏数据还没有刷新到磁盘就被覆盖的话,万一发生Crash,数据就无法恢复了。此时会从Flush 链表里面选取脏页,进行Flush。
  • 为了保证MySQL中的空闲页面的数量,Page Cleaner线程会从LRU 链表尾部淘汰一部分页面作为空闲页。如果对应的页面是脏页的话,就需要先将页面Flush到磁盘。
  • MySQL中脏页太多的时候。innodb_max_dirty_pages_pct 表示的是Buffer Pool最大的脏页比例,默认值是75%,当脏页比例大于这个值时会强制进行刷脏页,保证系统有足够可用的Free Page。innodb_max_dirty_pages_pct_lwm参数控制的是脏页比例的低水位,当达到该参数设定的时候,会进行preflush,避免比例达到innodb_max_dirty_pages_pct 来强制Flush,对MySQL实例产生影响。
  • MySQL实例正常关闭的时候,也会触发MySQL把内存里面的脏页全部刷新到磁盘。
  • 相关文章

    Oracle如何使用授予和撤销权限的语法和示例
    Awesome Project: 探索 MatrixOrigin 云原生分布式数据库
    下载丨66页PDF,云和恩墨技术通讯(2024年7月刊)
    社区版oceanbase安装
    Oracle 导出CSV工具-sqluldr2
    ETL数据集成丨快速将MySQL数据迁移至Doris数据库

    发布评论