对于MySQL数据库来说,最重要的存储引擎就是InnoDB。我们后端程序员几乎每天都会同MySQL打交道,在使用的同时也需要熟悉其底层原理,知其然知其所以然。其实IT技术犹如练武,底层原理犹如内功,只有内功扎实才能修炼上层的武功。为什么张无忌能在几个小时练成乾坤大挪移?因为他有九阳神功这门高深的内功。IT技术也是一样,只有了解各种中间件以及框架的原理,基础扎实才能飞得更高。
本篇文章将以最通俗易懂的语言,大白话的形式说明buffer pool的原理。同时也希望各位大佬给出建议,写这篇文章不仅仅是技术分享,更重要的是自我知识的巩固以及总结能力的提升。
对于innoDB存储引擎来说,数据是存储在磁盘上,而执行引擎想要操作数据,必须先将磁盘的数据加载到内存中才能操作。那么innoDB是 如何将磁盘中的数据加载到内存中?是不是用多少就加载多少?如果内存不够用了怎么办? 先带着这些问题,认真往下看
以下是摘自mysql官网的一个图,先不用深究细节,了解一个大概即可
是不是看的很懵?没关系,我整理了一个中文版
了解这些前置知识后,下面可以进入今天的主角 - buffer pool
buffer pool是MySQL服务在启动的时候向操作系统申请的一片连续地址的内存空间,其本质就是一片内存,默认大小是 128M,可以在启动服务的时候,通过 innodb_buffer_pool 这个参数设置buffer pool的大小,单位是字节(B),最小值是5MB,如果设置的小于5MB时会自动设置成5MB
buffer pool被划分为某干个数据页,其数据页大小和表空间使用的页大小一致,为了描述方便下文都称buffer pool中的数据页为 缓冲页。为了更好的管理buffer pool中的缓冲页,innoDB为每个缓冲页都创建了一个控制信息。这些控制信息主要包括该 缓冲页 所属的表空间编号、页号、缓冲页在buffer pool中的地址、链表节点信息等,除这些内容外,控制信息还包括一些内容,这里就不再一一列举。
每个缓冲页对应的控制信息所占内存空间是相同的,这个内存空间称为 控制块。缓冲页和控制块是一一对应的,其中控制块在buffer pool前面,而缓冲页在buffer后面,整个buffer pool其内存结构图如下
LRU不清楚的童鞋,请看这篇文章 https://blog.csdn.net/belongtocode/article/details/10298968
需要操作的页如果在buffer pool中,那么每次访问的时候直接将其对应的控制块异动到LRU链表的表头,如果操作的页不在buffer pool中,则先从磁盘加载数据到该缓冲页中,其对应的控制信息直接放到LRU链表的头部,如果没有空闲页,则淘汰LRU链表尾部对应的缓冲页。传统LRU链表如下图所示:
由于innoDB存储引擎每次都是按页读,即便每次只读一行记录,也会将该行所在的页全部加载到buffer pool中,而数据访问遵循“集中读写”原则。使用一些数据,大概率会使用附近的数据这就是 局部性原理【因此,将下次可能用到的页加载到Buffer Pool,顺序访问N个页后触发预读】
(1)线性预读:如果顺序访问同一个区的页,当大于等于innodb_read_ahead_threshold变量指定的值时,会触发线性预读,即会将下一个区的所有页加载到buffer中,Mysql5.4版本默认开启,默认值是56
(2)随机预读 :如果访问某一个区的连续页超过13,那么会触发随机预读,即将该区的所有页都加载到buffer pool中,MySQL 5.5之后的版本随机预读默认是关闭的,通过innodb_random_read_ahead变量设置为ON表示开启
预读是后台线程异步完成的,由参数 innodb_read_io_threads 控制数量。默认 4 个
由于InnoDB的预读机制,在有些情况下可能回出现预读的页并没有被使用;如果有非常多的使用频率低的页同时加载buffer pool中,这样使用频率高的页会被淘汰掉。基于这两个原因,InnoDB将传统LRU算法做了改进。
将LUR分为两个区域,一部分使用频率高的在链表的头部称为“young区”,一部分使用频率低的在链表的后部分称为“old区”。主要示意图如下:
所有新加载的页都先放到old区的Head,无论是预读的还是普通的读操作;新加入到old的数据,如果没有后续访问,会被逐渐从old区域淘汰,不会影响young区使用频率比较高的缓冲页。即使是使用了分区,那么还会出现一些问题,对于LRU链表的优化措施,还非常多的内容,由于篇幅限制,这里不一一赘述。那么一些列举了两个比较重要的优化点:
一个buffer pool在多线程访问的时候,各个链表都会加锁处理,这样一来,多线程访问时,性能就会降低,所以buffer pool是有多个实例的,每个实例都有其对应的链表管理。可以通过innodb_buffer_pool_instances参数来设置实例的个数。每个buffer pool实例的大小计算公式:innodb_buffer_pool_size / innodb_buffer_pool_instances
如果innodb_buffer_pool_size 小于1G时,设置innodb_buffer_pool_instances是无效的,都会是1
如果修改innobd中buffer pool的大小,就需要重新申请一片大的内存块,随后将老的buffer pool中的内容复制到新的中,这样的开销是非常大的,那么innoBD中肯定不是这么做的。
在MySQL 5.7.5之前,是不允许在运行时调整buffer pool大小的,只能在服务器启动之前,通过innodb_buffer_pool_size大小来调整
MySQL 5.7.5及以后的版本中,MySQL以chunk为单位来申请内存空间,即buffer pool由多个buffer pool实例组成,而一个实例由多个chunk组成,一个chunk就代表一片连续的内存空间
在运行的时候调整buffer pool大小,只需要以chunk为单位来增加或减少buffer pool大小,这样就不用每次复制了。chunk的大小是在MySQL服务启动时,通过innodb_buffer_pool_chunk_size来指定,默认大小是128M
innodb_buffer_pool_chunk_size的大小只能在服务器启动的时候指定,不能在运行时指定。
属于buffer pool中的一片内存。当InnoDB观察到某个索引的值频繁被使用,那么会创建自适应hash索引,可以提高查询速度,可以通过参数innodb_adaptive_hash_index来禁用或启动此特性,默认为开启
redo log buffer也属于buffer pool的一部分,redo log也不是每次写入都刷新到磁盘,也有一个专门的缓冲区redo log buffer(默认16M,通过innodb_log_buffer_size来设置)来记录,等待时机再刷新到磁盘。关于redo log后面会专门用一篇博客来讲解,这里先简单描述下。