MySQL优化实战学习笔记(三)
7从增删改查回顾BufferPool
7.1 回顾BufferPool
发生增删改操作请求
- 事物开启
- 加载缓存数据
- 在undo日志写旧数据
- 更新内存数据
- 写redo日志数据
- redo数据写回磁盘
- 写binlog日志数据
- binlog数据写回磁盘
- 在redo文件中写入binlog文件与文件的位置
- 在redo文件中写入commit标记
- 刷新本地磁盘
- 关闭事务
7.2 配置BufferPool大小
BufferPool本质就是内存中的一片具有特定数据结构的空间。
BufferPool默认128MB,实际上有点小,完全可以根据自己的机器配置调整内存大小。
例如,16核心32GB的配置,可以给BufferPool分配2G内存。
[server]
innodb_buffer_pool_size = 2147483648
7.3 数据页
数据库的核心数据模型:表+字段+行
但是实际上,在MySQL中,数据是以数据页的形式存放的。
数据页,可以理解为很多行数据打印在了一页上。
所以当我们更新BufferPool时,本质上是先找到相应的数据页,然后再去页上找到要进行操作的行。
在默认的情况下,磁盘中存放的数据页的大小是16KB。而BufferPool的数据页大小也是16KB
7. 4描述信息
针对在BufferPool中的每一个缓存页(数据页),都会有一个对应的描述信息页。
描述信息页主要介绍了如下信息:数据页所属的表空间、数据页的编号、数据页在BUfferPool中的地址等等。
而且在BUfferPool中,描述信息永远放在队列的开头,而数据页永远在描述信息的后面。
描述信息页的大小基本上是数据页的5%,也就是差不多800个字节。因此当你设置了128MB的BufferPool容量后,实际用作BufferPool的内存空间应该为130MB左右,因为还要用额外的空间存放描述信息。
8 描述文件中的数据结构
8.1 free链表
针对BufferPool中的描述数据,mysql采用了free链表的数据结构来描述和储存他们。free链表本质是一个双向链表数据结构。
用自己的话描述一下。在BufferPool中,描述信息是一组一组的内存空间,他们的大小固定,数量固定,这是在BufferPool初始化的时候就根据设置而确定的。
在实际的操作过程中,我们必须明确哪一些描述数据是空的,哪一些没有。因此,mysql采用free链表来描述这些个缓存信息。
注意,实际上,并非说free链表是独立于描述信息的,类似于osCache中的页表。而是整个描述信息内存区域,采用free链表这种数据结构来描述的。
可以抽象的理解为,在没有free链表之前,BufferPool中的描述信息部分,就是一块一块没有互相关联的数据块。
有了free链表之后,这些描述信息的内存块就分别变成了链表之中的数据域,同时还有左右两个指针域,分别指向下一个内存块和上一个内存块的地址。
当我们要拿出一个空的内存块的时候,我们直接按照双向链表的数据结构的操作,对这一个内存块进行释放就可以。
除此以外,在free链表中还额外新建了一个基础节点。这个节点中分别存储了整个描述内存空间的起始描述节点和最后一个描述节点,并且在基础节点中还有一个计数器count来记录当前free链表中的节点个数。
8.2 哈希表
我们知道,在进行增删改的时候,都是优先在内存中的BufferPool中进行操作的。而,整个mysql都是采用数据页的形式来保存数据的。
如果如果我们要修改的这个数据所在的数据页已经在BufferPool中,那么我们就可以直接在缓存中进行操作。如果不在的话,我们需要加载相应的数据页到我们的BufferPool之中。
因此一个问题就产生了,我们如何知道这个数据页有没有被加载到BufferPool之中呢?
答案就是构建一个哈希表,类似于操作系统中的Cache的快表和页表。这个哈希表的数据结构中,会用表空间号+数据页号作为key,将缓存页的地址作为value。
当我们要使用某个页的时候,就可以自动拼接一个地址:表空间号+数据页号,把这个地址作为key。
如果这个key所对应的value不为空,则该页已经在BufferPool之中了,如果value为空,则说明这个数据页不在BufferPool之中。
8.3 flush链表
当我们完成了在BufferPool中的操作时,数据变脏了,我们需要把数据写回磁盘。
这时候出现了一个问题,并不是所有的数据都被修改过,比如只是为了查询而把数据页加载到BufferPool之中的话,就没有必要把数据写回磁盘,因为数据还没有被修改过。
因此我们额外构建一个flush链表,当一个缓存页被修改过,就自动的把这个缓存页添加到flush链表之中。
注意,一个节点不可能既在flush链表之中,又在free链表之中的
8.4 伪代码
DescriptionDataBlock{
// 这个是缓存页01的数据块
block_id = BBB;
// 这个缓存页已经被更新过了,自然不可能在Free链表之中,因此前后指针都是null
free_pre = null;
free_next = null;
// 即将被刷新的前后两个数据块分别是AAA和CCC
flush_pre = AAA;
flush_next = CCC
}
9 淘汰缓存页与替换算法
当我们的BufferPool中的free链表中没有空闲的缓存页时,我们必须按照某种算法,把缓存页写回磁盘。
9.1 LRU链表
替换算法的核心思想是:LRU。Least Recently Used
简而言之,根据缓存页使用的频率,构建一个LRU链表。链表的顶端就是使用次数最多的页,尾端就是使用次数最少的页。
当然这里的lru链表是双向的,且同样有一个基础节点记录lru链表的头尾与个数。
当没有空闲的缓存页时,直接把lru链表的尾端节点刷入磁盘并且释放。
9.1 简单的LRU链表的危险之预读
mysql有预读机制。当我们在磁盘上加载一个数据页的时候,可能也把这个数据页连带的相邻的其他数据页也一起加载到BufferPool之中去。但是实际上,如果只有一个缓存页被访问了,预读加载的其他数据页没有被读取,则预读的缓存页都可能在LRU的前面。
核心问题就是一个连带机制问题,预读和要读取的数据页一起被加载进缓存,并且位于LRU链表前段,而实际上预读的页很有可能不被读取,他们才应该被删除,而不是那些被预读页挤到后面去的,实际上曾经被读取过的数据页。
9.1.1 Mysql的预读机制
可以看到这些个所谓的问题与Mysql的预读机制相关。下面来介绍一下什么情况会触发Mysql的预读机制。
- innodb_read_ahead_threshold 默认值56
- 代表如果顺序的访问一个区里的多个数据页,访问的数据页超过了阈值,就会把下一个相邻区中的所有数据页加载到缓存中去
- innodb_random_read_ahead 默认OFF
- 代表如果对一个区里的13个连续的数据页都在访问,就会直接把这个区中的所有数据页都加载到缓存中去
9.2 简单的LRU链表的危险之全局扫描
select * from XXX
执行全局扫描,由于没有加where所以会直接把整个磁盘中的所有数据页一口气的加载到缓存中去。
这样子会一下子把一些经常访问的数据页全部挤到LRU链表的尾部,这显然是不太合理的。