前言
对于innoDB存储引擎来说,数据是存储在磁盘上,而执行引擎想要操作数据,必须先将磁盘的数据加载到内存中才能操作。当数据从磁盘中取出后,缓存内存中,下次查询同样的数据的时候,直接从内存中读取,这样大大提高了查询性能。
InnoDB结构图
MySQL InnoDB Architecture的体系结构图在这里:Mysql5.7版本官方InnoDB结构图,虽然是英文的,但是解释的最明白的往往是官方文档。
内存结构(In-Memory Structures)主要是针对的是数据及其操作,主要分为:
- Buffer Pool: 缓冲池,数据缓冲池里面不直接存放数据而是存放的Page页,将数据存放在了Page页中,在缓冲池Page页是通过链表形式来存放的。
- Change Buffer: 写缓冲区,正常情况下修改数据是先修改的缓冲池中Page的数据,但是缓冲池肯定不是所有的数据,而修改数据没有对应的Page数据的时候并不会直接把数据加载到缓冲池中去,而是放在了写缓冲区中记录,等到数据被读取的时候再把数据合并到缓冲池中。
- Adaptive Hash Index: 自适应Hash索引,InnoDB存储引擎会根据Page页的访问频率和模式建立对应的Hash索引,这个索引是根据查询情况自动建立的,称为自适应Hash索引。
- Log Buffer: 日志缓冲区,主要用来保存写入磁盘的(Redo/Undo)日志文件,日志缓冲区会定期刷新到磁盘log文件中,这样不用每次日志都进行磁盘IO操作,提高效率。
磁盘结构(On-Disk Structures)主要针对的是表和表空间,主要分为以下结构:
- Tablespaces: 表空间,对于表空间大家应该都不陌生,用来存储表结构和数据的。表空间又被分为系统表空间、独立表空间、通用表空间、临时表空间等多种类型。
- InnoDB Data Dictionary: 数据字典,InnoDB数据字典由内部系统表组成,这些表包含用于查找表、索引和表字段等对象的元数据。
- Doublewrite Buffer: 双写缓冲区,我们知道数据修改先修改的Page页后又刷到磁盘的,在刷到磁盘前这些数据会先存放在双写缓存区中,双写缓存区是用来保障数据写入磁盘时候出现问题的备份。
- Redo Logs: 重做日志,记录了所有缓冲池修改的数据,修改数据的时候先写日志,后修改的缓冲区,假设修改写入操作的时候数据库崩溃了或停电了,等下次启动通过重做日志来保持数据的正确性。
Buffer Pool(缓冲池)
先看波关于Buffer Pool的概念:
Buffer Pool是MySQL服务在启动的时候向操作系统申请的一片连续地址的内存空间,其本质就是一片内存,默认大小是 128M,可以在启动服务的时候,通过 innodb_buffer_pool 这个参数设置buffer pool的大小,单位是字节(B),最小值是5MB。
那么Buffer Pool这段内存地址到底有什么,可以确定的就是肯定有16KB数据页,这里叫缓冲页。除此之外还有,索引页,undo 页,插入缓存、自适应哈希索引、锁信息
内部组成
因为buffer pool被划分为某干个数据页,其数据页大小和表空间使用的页大小一致,为了更好的管理buffer pool中的缓冲页,innoDB为每个缓冲页都创建了一个控制信息。
这些控制信息主要包括该缓冲页的【表空间编号、页号、缓冲页在buffer pool中的地址、链表节点信息】,存储这些控制信息控制块。
缓冲页和控制块是一一对应的,其中控制块在buffer pool前面,而缓冲页在buffer后面。
什么是碎片?
当剩余空间不够一对控制块和缓冲页的大小时,这样的空间称为碎片
怎么查看MySQL实例的Buffer Pool信息呢?
show variables like '%innodb_buffer_pool_size%'; 查看buffer pool的size
show global status like '%innodb_buffer_pool%'; 查看相关参数,详细的参数代表的意思,大家自己去搜搜。
管理Buffer Pool
Buffer Pool 中的页有三种状态:
接下来我们分别看看三种链表是如何进行管理的。
Free链表
初始化完的buffer pool时所有的页都是空闲页,所有空闲的缓冲页对应的控制块信息作为一个节点放到Free链表中。
要注意Free链表是一个个控制块,而控制块的信息中有缓存页的地址信息。
在有了free链表之后,当需要加载磁盘中的页到buffer pool中时,就去free链表中取一个空闲页所对应的控制块信息,根据控制块信息中的表空间号、页号找到buffer pool里对应的缓冲页,再将数据加载到该缓冲页中,随后删掉free链表该控制块信息对应的节点。
如何在buffer pool中快速查找缓冲页(数据页)呢?
这里就可以对缓冲页进行Hash处理,用表空间号、页号做为Key,缓冲页的控制块就是value**维护一个Hash表,**根据表空间号、页号做为Key去查找有没有对应的缓冲信息,如果没有就需要去free 链表中取一个空闲的缓冲页控制快信息,随后将磁盘中的数据加载到该缓冲页位置。
Flush链表
修改了buffer pool中缓冲页的数据,那么该页和磁盘就不一致了,这样的页就称为【脏页】,它不是立马刷入到磁盘中,而是由后台线程将脏页写入到磁盘。
Flush链表就是为了能知道哪些是脏页而设计的,它跟Free链表结构图相似,区别在于控制块指向的是脏页地址。
LRU链表
对于频繁访问的数据和很少访问的数据我们对与它的期望是不一样的,很少访问的数据希望在某个时机淘汰掉,避免占用buffer pool的空间,因为缓冲空间大小是有限的。
MySQL设计了根据LRU算法设计了LRU链表来维护和淘汰缓冲页。
LRU 算法简单来说,如果用链表来实现,将最近命中(加载)的数据页移在头部,未使用的向后偏移,直至移除链表。这样的淘汰算法就叫做 LRU 算法,但是简单的LRU算法会带来两个问题:预读失效、Buffer Pool污染
预读机制和预读失效
预读机制:当数据页从磁盘加载到 Buffer Pool 中时,会把相邻的数据页也提前加载到 Buffer Pool 中,这样做的好处就是减少未来可能的磁盘IO。
预读失效:当预读机制提前加载的数据页一直未被访问,这就是失效
好,那么结合简单的LRU算法来看,可能预读页呗加载到LRU链表头部,当Buffer Pool空间不够时,会把经常访问的位于LRU链表的尾部数据页给淘汰清理掉,这样缓冲就失效了。
改进的LRU 算法
Buffer Pool的LRU算法中InnoDB 将LRU链表按照5:3的比例分成了young区域和old区域。链表头部的5/8是young区(被高频访问数据),链表尾部的3/8区域是old区域(低频访问数据),箭头朝下的是未被访问的数据,朝上的是被访问的数据。
这样做的目的是,在预读的时候或访问不存在的缓冲页时,先加入到 old 区域的头部,当页被真正访问的时候,才将页插入 young 区域的头部。
我们来看下图:
多Buffer实例
我们已经默认情况下**innodb_buffer_pool_size是128M, **此时的innodb_buffer_pool_instances的大小也就是实例是1个。因为innodb_buffer_pool_size 小于1G时,设置innodb_buffer_pool_instances是无效的,都会是1。
当一个buffer pool在多线程访问的时候,各个链表都会加锁处理,这样一来,多线程访问时,性能就会降低。
可以通过innodb_buffer_pool_instances参数来设置实例的个数。每个buffer pool实例的大小计算公式:**innodb_buffer_pool_size / innodb_buffer_pool_instances,**每个实例都有其对应的链表管理,互不干扰。
修改Buffer Pool大小
如何修改运行中MySQL的Buffer Pool的大小?
MySQL 5.7.5之前:是不允许在运行时调整buffer pool大小的,只能在服务器启动之前,通过innodb_buffer_pool_size大小来调整。
**MySQL 5.7.5之后:**是以chunk为单位来修改Buffer Pool的大小,比如innodb_buffer_pool_chunk_size默认大小是128M,调整Buffer Pool大小就以chunk为单位来增加或减少Buffer Pool大小。
我们应该要有这么一个概念就是:一个Buffer Pool可能有多个buffer pool实例,而每个实例由多个chunk组成,一个chunk是一块连续的内存空间,一个chunk默认大小是128M。
如下图,这样理解是不是就很清晰啦?
总结
磁盘太慢, Buffer Pool本质上是向操作系统申请一块连续的内存空间作为缓冲区。
缓冲区由控制块和缓冲页组成,两者是一一对应的关系,而碎片是指不足以填充一组控制块和缓冲页的内存空间。
Buffer Pool使用Free List链表管理空闲页、LRU List链表管理正常页、Flush List链表管理脏页(被修改的页),而脏页并不是立即刷新的,而是先加入Flush List,而后再刷到磁盘中。
通过改进LRU算法,LRU链表分为young区域和old区域,预读的缓冲页,先也放到old区域的head头部。
可以根据Buffer Pool的大小来这是多实例,Buffer Pool小于1G,默认是1个实例。
MySQL 5.7.5之后可以在运行时修改 Buffer Pool大小,主要是通过chunk来调整。