mysql的数据始终都是存放在磁盘中,频繁的读取、写入磁盘性能上肯定有所欠缺的,要知道磁盘I/O开销也是很大的。
怎么解决呢?加一个中间层就好。
buffer pool是innodb存储引擎设计的一个缓存池,来提升数据库的读写性能的。
buffer pool是mysql向操作系统申请的一片连续的内存空间,默认大小128M,也可以自己设置,配置参数innodb_buffer_pool_size。
buffer pool为一片连续的内存空间,然后按照页去划分,一页为16KB。
buffer pool里面不只是缓存数据页,还有一些索引页、插入缓存页(change buffer)、uodo log页等等。给我的感觉就是,我觉得哪些东西慢的,我就想办法把它缓存起来。
数据在缓存池的操作:
- 读取数据:首先先去buffer pool中的数据页里去找,如果找到直接返回。如果没有的情况下,就会去磁盘中查,查到数据之后,会将当前数据所在的当前页和相邻的数据页一起缓存到buffer pool中。
- 修改数据:要知道数据的修改是不会直接写磁盘的,也是先写入buff pool中。先写入redo log日志中(这个也是有buffer缓冲区的),然后对数据进行修改,并将当前数据所在的页标记为脏页,然后后台线程进行刷盘。
buffer pool中的数据页大小为16kb进行分成一页一页的。为了更好的管理,就设计出一个控制块,管理这些数据页。
- 控制块信息包含[缓存页的空间、页号、缓存地址、链表节点]等等
- 每一个控制块对应一个数据页
这里我们注意一下,这些数据页不一定是全部都占满的,会出现空闲页的情况,这个时候就要统一管理。 mysql使用的链表的形式,将这些空闲的页串联起来,术语叫(free链表),空闲链表。
free链表(空闲链表)
- 之前说过,数据页由控制块去管理。所以free链表只需对控制块做管理就行(你管你下属,我管你就行,不直接对你下属进行管理,不越级)。
- free list
- start:记录了链表的头节点地址
- end:链表的尾节点地址
- count:当前链表数量的信息
- free list node
- clt:控制块的地址信息,free list node --> 控制块地址 --> 空闲page地址
- pre:上一个节点的地址
- next:下一个节点的地址
- 如果有数据需要缓存到内存中的时候,就去free链表去取一个节点,然后就可以获取到对应的控制块和对应的缓存页信息,然后把缓存页的信息填上就好。然后删除free节点。
总之就是:数据页被控制块管理。出现的空闲页也是要管理起来,空闲页被free链表管理,free不直接管理空闲数据页,而是直接管理控制块。(就优点类似,我是一个大领导,我现在有事情需要找人处理,不能直接找基层员工,不能越级)
flush链表(脏页链表)
- 为什么会有脏页链表?要知道我们对数据进行修改的时候是不会直接操作磁盘的,而是先把它缓起来,被修改的数据对应的数据页就被称为脏页,然后由后台线程进行处理。
- 怎么管理这里脏页?这不就来了嘛,链表串起来统一管理。有点类似东西分类,方便直接拿。
- 其实和free list 的数据结构是一样的。
free list 和 flush list结构上是一样的,只是每个人管理的东西不一样,就是浪迹分类一样,你负责管理空闲页链表,我负责管理脏页的链表。
来看看缓存的命中率问题。程序开发中,内存相对来说还算是比较“昂贵”的,所以需要合理的使用和分配,无可厚非。我们在缓存数据的时候肯定是需要缓存经常使用的数据,为了提高效率。而不经常使用的数据有的时候偶尔一次的命中并加载到缓存当中,后续就很少使用,这个时候就会有点占用内存了,俗话说“占着茅坑不拉屎”。
mysql有自己的办法规避。
LRU算法
思路:这里还是使用的链表,LRU链表。链表头部存放的是一些常用数据(热数据),尾部节点存放最久没有使用的数据。当我们的内存空间不足的时候,使用末位淘汰制,这样可以腾出一部分的空间。
- 当我们访问数据的数据页在bffer pool中的时候,将当前页移动到链表头部
- 但我们访问数据的数据页不在buffer pool中的时候,将数据页直接写入了链表头部,然后淘汰掉尾部节点
LRU算法的本质就是,你需要读的数据就是热数据,需要放在前面。当内存不够的情况出现将会末位淘汰。
这里会出现一个问题,就是如果我们访问的数据不在buffer pool中的时候需要去磁盘中读取数据然后缓存在buffer pool中,这个时候会将需命中的数据的数据页和相邻的页一起缓存到buffer pool中,然后根据LRU算法,会被插入到LRU链表的头部中,如果这些加载到链表头部的数据页没有被缓存到,相当于没有意义,这个就出现了预读失效的问题。
还有一个问题,我们进行一个大范围查询的时候,会产生大量的数据。buffer pool的内存空间是有限的。这个时候就会出现一个问题,整个buffer pool中的数据页全部被替换。这个时候一些热数据就会被替换,当有新的查询需要查询这些热数据的时候,又要从磁盘里读取,产生磁盘I/O,导致性能下降,这个就是buffer pool污染。(就有点类似一家公司突然空降一个总裁,然后二话不说,工作交接也没有直接将原有的公司员工全部裁掉,换成自己的人来继续运营公司。然后发现总裁在裁员的时候没有让进行工作交接导致自己人在工作上出问题,导致很大损失,这个时候又得请老员工回来继续工作)。
看看Mysql如何解决这个问题:
依旧使用LRU算法,但是mysql进行了改进,为了解决预读失效和bffer pool污染的问题。
- LRU链表分为2个区域,一个是前半部分young区,一个是后半部分old区
- 占比可以通过innodb_old_blocks_pct来配置,默认为37。young:old(63:37)
这样看下来就比较明显知道mysql他想干嘛了。
- 我们在读取磁盘数据的时候mysql会将命中数据的页和相邻页的数据一起缓存到buffer pool中。
- 这个时候我们将缓存的数据页先放到old区的头部中
- 然后等到数据页真正被读取到的时候,再插入young区头部中
- 这个时候young区的尾部数据就变成old区头部,old区尾部数据将会被淘汰
- 但是呢还是没有办法解决buffer pool被污染的问题,因为只要在old区再被访问一次的情况下,就会进入young区,从而导致热数据被替换。这个时候就需要开始设置这个old和young的机制了。
- 一开始在old区,如果
后续的访问时间与第一次访问的时间在某个时间间隔内,那么该数据页就不会从old区移动到young区的头部 - 一开始在old区,如果
后续的访问时间与第一次访问的时间不在某个时间间隔内,那么该数据页移动到young区头部 - 配以进行配置
innodb_old_blocks_time,默认1000ms
其实就是满足被访问与old区停留时间超过1s两个条件,才会被移动到young区的头部。young区还做了一个优化,就是前1/4(25%)被访问的不会移动到链表的头部,3/4(75%)被访问才会。
我们之前有说过flush list脏页链表的数据是要写入磁盘并且保证内存中的数据和磁盘的数据一致才能保证mysql一致性。什么时候写?
因为mysql采用WAL策略,先写日志再写磁盘,这样就算mysql宕机我们重启的时候可以通过redo log日志实现崩溃恢复。
触发脏页的机制:
- redo log日志写满的情况下,会主动触发脏页刷写磁盘
- buffer pool空间不足的时候,需要将一部分的脏页套淘汰掉(腾空间),所以就需要先写入磁盘然后再淘汰脏页
- mysql 认为空闲时,后台线程会定期适量的将脏页刷写到磁盘
- mysql正常关闭i之前,会把所有的脏页写入磁盘
总结:buffer poll为了提高mysql的读写性能。数据缓存页由空闲页、脏页、干净页主组成。free list链表来管理空闲页,flush list管理脏页,LRU链表管理脏页和干净页。
Mysql对LRU算法优化,分为young区和old区,根据配置innodb_old_blocks_time配置时间,判断是否页被访问和在old区停留时间的判断是否要从old区移动到young区。