|
|
@ -1654,6 +1654,8 @@ MySQL 5.7 版本以后,支持设置多个刷脏页线程,提高脏页处理
|
|
|
|
|
|
|
|
|
|
|
|
`InnoDB `为了解决磁盘 `I/O` 频繁操作问题,`MySQL `需要申请一块内存空间,这块内存空间称为`Buffer Pool`。
|
|
|
|
`InnoDB `为了解决磁盘 `I/O` 频繁操作问题,`MySQL `需要申请一块内存空间,这块内存空间称为`Buffer Pool`。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
![Buffer-Pool-LRU链表优化](images/Database/Buffer-Pool-LRU链表优化.png)
|
|
|
|
|
|
|
|
|
|
|
|
#### 缓存页
|
|
|
|
#### 缓存页
|
|
|
|
|
|
|
|
|
|
|
|
`MySQL`数据是以页为单位,每页默认`16KB`,称为**数据页**。在`Buffer Pool`里面会划分出若干个**缓存页**与**数据页**对应。
|
|
|
|
`MySQL`数据是以页为单位,每页默认`16KB`,称为**数据页**。在`Buffer Pool`里面会划分出若干个**缓存页**与**数据页**对应。
|
|
|
@ -1664,7 +1666,7 @@ MySQL 5.7 版本以后,支持设置多个刷脏页线程,提高脏页处理
|
|
|
|
|
|
|
|
|
|
|
|
#### 描述数据
|
|
|
|
#### 描述数据
|
|
|
|
|
|
|
|
|
|
|
|
每个**缓存页**会有对应的一份**描述数据**(一一对应),里面存储了**缓存页的元数据信息**,包含一些所属表空间、数据页的编号、`Buffer Pool`中的地址等。可用于**缓存页**直接映射到对应的**数据页**。默认为`800Byte`每个描述数据。
|
|
|
|
每个**缓存页**会有对应的一份**描述数据**(一一对应),里面存储了**缓存页的元数据信息**,包含一些所属表空间、数据页的编号、`Buffer Pool`中的地址等。可用于**缓存页**直接映射到对应的**数据页**。每个描述数据默认为 `800Byte`。
|
|
|
|
|
|
|
|
|
|
|
|
![Buffer-Pool-描述数据](images/Database/Buffer-Pool-描述数据.png)
|
|
|
|
![Buffer-Pool-描述数据](images/Database/Buffer-Pool-描述数据.png)
|
|
|
|
|
|
|
|
|
|
|
@ -1674,41 +1676,10 @@ MySQL 5.7 版本以后,支持设置多个刷脏页线程,提高脏页处理
|
|
|
|
- 插入:直接写入缓存
|
|
|
|
- 插入:直接写入缓存
|
|
|
|
- 更新删除:缓存中存在直接更新,不存在加载数据页到缓存更新
|
|
|
|
- 更新删除:缓存中存在直接更新,不存在加载数据页到缓存更新
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
`MySQL`宕机数据不就全丢了吗?
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
`InnoDB`提供了`WAL`技术(Write-Ahead Logging),通过`redo log`让`MySQL`拥有了崩溃恢复能力。再配合空闲时,会有异步线程做缓存页刷盘,保证数据的持久性与完整性。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
![Buffer-Pool-Write-Ahead-Logging](images/Database/Buffer-Pool-Write-Ahead-Logging.png)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
直接更新数据的缓存页称为**脏页**,缓存页刷盘后称为**干净页**。
|
|
|
|
直接更新数据的缓存页称为**脏页**,缓存页刷盘后称为**干净页**。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#### Free链表
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
`MySQL`运行起来后,会不停的执行增删改查,需要从磁盘读取一个一个的数据页放入`Buffer Pool`对应的缓存页里,把数据缓存起来,以后就可以在内存里执行增删改查。但这个过程必然涉及一个问题,**哪些缓存页是空闲的**?
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
为了解决该问题,使用链表结构把空闲缓存页的**描述数据**放入链表(`Free`链表)中。针对`Free`链表要做如下设计:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
![Buffer-Pool-Free链表设计](images/Database/Buffer-Pool-Free链表设计.png)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- 新增`free`基础节点
|
|
|
|
|
|
|
|
- 描述数据添加`free`节点指针
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
最终呈现出来的,是由空闲缓存页的**描述数据**组成的`free`链表。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
![Buffer-Pool-Free链表组成](images/Database/Buffer-Pool-Free链表组成.png)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
有了`free`链表之后,我们只需要从`free`链表获取一个**描述数据**,就可以获取到对应的缓存页。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
![Buffer-Pool-Free链表-获取描述数据](images/Database/Buffer-Pool-Free链表-获取描述数据.png)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
往**描述数据**与**缓存页**写入数据后,就将该**描述数据**移出`free`链表。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#### 缓存页哈希表
|
|
|
|
#### 缓存页哈希表
|
|
|
|
|
|
|
|
|
|
|
|
查询数据时,如何在`Buffer Pool`里快速定位到对应的缓存页呢?难道需要一个**非空闲的描述数据**链表,再通过**表空间号+数据页编号**遍历查找吗?这样做也可以实现,但是效率不太高,时间复杂度是`O(N)`。
|
|
|
|
查询数据时,如何在`Buffer Pool`里快速定位到对应的缓存页呢?难道需要一个**非空闲的描述数据**链表,再通过**表空间号+数据页编号**遍历查找吗?这样做也可以实现,但是效率不太高,时间复杂度是`O(N)`。
|
|
|
@ -1727,77 +1698,64 @@ MySQL 5.7 版本以后,支持设置多个刷脏页线程,提高脏页处理
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#### Flush链表
|
|
|
|
#### Free链表
|
|
|
|
|
|
|
|
|
|
|
|
还记得之前有说过「**空闲时会有异步线程做缓存页刷盘,保证数据的持久性与完整性**」吗?
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
新问题来了,难道每次把`Buffer Pool`里所有的缓存页都刷入磁盘吗?当然不能这样做,磁盘`IO`开销太大了,应该把**脏页**刷入磁盘才对(更新过的缓存页)。可是我们怎么知道,那些缓存页是**脏页**?很简单,参照`free`链表,弄个`flush`链表出来就好了,只要缓存页被更新,就将它的**描述数据**加入`flush`链表。针对`flush`链表我们要做如下设计:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- 新增`flush`基础节点
|
|
|
|
|
|
|
|
- 描述数据添加`flush`节点指针
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
![Buffer-Pool-Flush链表](images/Database/Buffer-Pool-Flush链表.png)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
最终呈现出来的,是由更新过数据的缓存页**描述数据**组成的`flush`链表。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
![Buffer-Pool-Flush链表-缓存页](images/Database/Buffer-Pool-Flush链表-缓存页.png)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
后续异步线程都从`flush`链表刷缓存页,当`Buffer Pool`内存不足时,也会优先刷`flush`链表里的缓存页。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**Free链表可以帮助我们快速找到空闲的缓存页**。当执行增删改查时,需要从数据页加载数据,然后从free链表(双向链表)中找到空闲的缓存页。把数据页的表空间号和数据页号写入描述信息块,加载数据到缓存页后,会把缓存页对应的描述信息块从free链表中移除。`Free`链表设计:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- 新增**`free`基础节点**
|
|
|
|
|
|
|
|
- **描述数据**添加**`free`节点指针**
|
|
|
|
|
|
|
|
|
|
|
|
#### LRU链表
|
|
|
|
![Buffer-Pool-Free链表组成](images/Database/Buffer-Pool-Free链表组成.png)
|
|
|
|
|
|
|
|
|
|
|
|
目前看来`Buffer Pool`的功能已经比较完善了。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
![Buffer-Pool-LRU链表](images/Database/Buffer-Pool-LRU链表.png)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
但是仔细思考下,发现还有一个问题没处理。`MySQL`数据库随着系统的运行会不停的把磁盘上的数据页加载到空闲的缓存页里去,因此`free`链表中的空闲缓存页会越来越少,直到没有,最后磁盘的数据页无法加载。
|
|
|
|
#### Flush链表
|
|
|
|
|
|
|
|
|
|
|
|
![Buffer-Pool-LRU链表-无法加载](images/Database/Buffer-Pool-LRU链表-无法加载.png)
|
|
|
|
**Free链表可以帮助我们快速找到脏页的缓存页**。当空闲时会有异步线程做缓存页刷盘,保证数据的持久性与完整性。但不会每次把`Buffer Pool`里所有缓存页都刷入磁盘,因为磁盘`I/O`开销太大,应该把**脏页**刷入磁盘才对(更新过的缓存页)。可哪些缓存页是**脏页**?参照`free`链表设计`flush`链表,只要缓存页被更新,就将它的**描述数据**加入`flush`链表。`Flush`链表设计:
|
|
|
|
|
|
|
|
|
|
|
|
为了解决这个问题,我们需要淘汰缓存页,腾出空闲缓存页。可是我们要优先淘汰哪些缓存页?总不能一股脑直接全部淘汰吧?这里就要借鉴`LRU`算法思想,把最近最少使用的缓存页淘汰(命中率低),提供`LRU`链表出来。针对`LRU`链表我们要做如下设计:
|
|
|
|
- 新增**`flush`基础节点**
|
|
|
|
|
|
|
|
- **描述数据**添加**`flush`节点指针**
|
|
|
|
|
|
|
|
|
|
|
|
- 新增`LRU`基础节点
|
|
|
|
![Buffer-Pool-Flush链表-缓存页](images/Database/Buffer-Pool-Flush链表-缓存页.png)
|
|
|
|
- 描述数据添加`LRU`节点指针
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
![Buffer-Pool-LRU链表-结构](images/Database/Buffer-Pool-LRU链表-结构.png)
|
|
|
|
后续异步线程都从`flush`链表刷缓存页,当`Buffer Pool`内存不足时,也会优先刷`flush`链表里的缓存页。
|
|
|
|
|
|
|
|
|
|
|
|
实现思路也很简单,只要是查询或修改过缓存页,就把该缓存页的描述数据放入链表头部,也就说近期访问的数据一定在链表头部。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
![Buffer-Pool-LRU链表-节点](images/Database/Buffer-Pool-LRU链表-节点.png)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
当`free`链表为空的时候,直接淘汰`LRU`链表尾部缓存页即可。
|
|
|
|
#### LRU链表
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
`MySQL`数据库随着系统的运行会不停的把磁盘上的数据页加载到空闲的缓存页里去,因此`free`链表中的空闲缓存页会越来越少,直到没有,最后磁盘的数据页无法加载。为了解决该问题,需要淘汰缓存页,腾出空闲缓存页。这里借鉴`LRU`算法思想,把最近最少使用的缓存页淘汰(命中率低),提供`LRU`链表出来。`LRU`链表设计:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- 新增**`LRU`基础节点**
|
|
|
|
|
|
|
|
- **描述数据**添加**`LRU`节点指针**
|
|
|
|
|
|
|
|
|
|
|
|
#### LRU链表优化
|
|
|
|
![LRU链表设计思路](images/Database/LRU链表设计思路.png)
|
|
|
|
|
|
|
|
|
|
|
|
麻雀虽小五脏俱全,基本`Buffer Pool`里与缓存页相关的组件齐全了。
|
|
|
|
实现思路也很简单,只要是查询或修改过缓存页,就把该缓存页的描述数据放入链表头部,也就说近期访问的数据一定在链表头部。当`free`链表为空的时候,直接淘汰`LRU`链表尾部缓存页即可。
|
|
|
|
|
|
|
|
|
|
|
|
![Buffer-Pool-LRU链表优化](images/Database/Buffer-Pool-LRU链表优化.png)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
但是缓存页淘汰这里还有点问题,如果仅仅只是使用`LRU`链表的机制,有两个场景会让**热点数据**被淘汰。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- **预读机制**
|
|
|
|
**LRU链表优化**
|
|
|
|
|
|
|
|
|
|
|
|
InnoDB使用两种预读算法来提高I/O性能:线性预读(linear read-ahead)和随机预读(randomread-ahead)。
|
|
|
|
缓存页淘汰这里还有点问题,如果仅仅只是使用`LRU`链表的机制,有两个场景会让**热点数据**被淘汰:
|
|
|
|
|
|
|
|
|
|
|
|
- **全表扫描**
|
|
|
|
**① 预读机制**
|
|
|
|
|
|
|
|
|
|
|
|
预读机制是指`MySQL`加载数据页时,可能会把它相邻的数据页一并加载进来(局部性原理)。这样会带来一个问题,预读进来的数据页,其实我们没有访问,但是它却排在前面。
|
|
|
|
InnoDB使用两种预读算法来提高I/O性能:线性预读(linear read-ahead)和随机预读(randomread-ahead)。
|
|
|
|
|
|
|
|
|
|
|
|
![Buffer-Pool-LRU链表优化-全表扫描](images/Database/Buffer-Pool-LRU链表优化-全表扫描.png)
|
|
|
|
预读机制是指`MySQL`加载数据页时,可能会把它相邻的数据页一并加载进来(局部性原理)。这样会带来一个问题,预读进来的数据页,其实我们没有访问,但是它却排在前面。正常来说,淘汰缓存页时,应该把这个预读的淘汰,结果却把尾部的淘汰了,这是不合理的。
|
|
|
|
|
|
|
|
|
|
|
|
正常来说,淘汰缓存页时,应该把这个预读的淘汰,结果却把尾部的淘汰了,这是不合理的。我们接着来看第二个场景全表扫描,如果**表数据量大**,大量的数据页会把空闲缓存页用完。最终`LRU`链表前面都是全表扫描的数据,之前频繁访问的热点数据全部到队尾了,淘汰缓存页时就把**热点数据页**给淘汰了。
|
|
|
|
![Buffer-Pool-LRU链表优化-全表扫描](images/Database/Buffer-Pool-LRU链表优化-全表扫描.png)
|
|
|
|
|
|
|
|
|
|
|
|
![图片](images/Database/819dbbcd31605b3a692576932f25d325.png)
|
|
|
|
**② 全表扫描**
|
|
|
|
|
|
|
|
|
|
|
|
为了解决上述的问题,我们需要给`LRU`链表做冷热数据分离设计,把`LRU`链表按一定比例,分为冷热区域,热区域称为`young`区域,冷区域称为`old`区域。
|
|
|
|
如果**表数据量大**,大量的数据页会把空闲缓存页用完。最终`LRU`链表前面都是全表扫描的数据,之前频繁访问的热点数据全部到队尾了,淘汰缓存页时就把**热点数据页**给淘汰了。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
![图片](images/Database/819dbbcd31605b3a692576932f25d325.png)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**解决方案**
|
|
|
|
|
|
|
|
|
|
|
|
**以7:3为例,young区域70%,old区域30%**
|
|
|
|
为了解决上述的问题,我们需要给`LRU`链表做冷热数据分离设计,把`LRU`链表按一定比例,分为冷热区域,热区域称为`young`区域,冷区域称为`old`区域。**以7:3为例,young区域70%,old区域30%**
|
|
|
|
|
|
|
|
|
|
|
|
![图片](images/Database/0f2c2610773fb9fe304c374fc37af4ac.png)
|
|
|
|
![图片](images/Database/0f2c2610773fb9fe304c374fc37af4ac.png)
|
|
|
|
|
|
|
|
|
|
|
|