数据存在磁盘了,总不能次次和磁盘交互吧,所以innoDB有一个缓冲池(Buffer Pool),有了缓冲池后,读写就优先在缓冲池了。读先在缓冲池读,没有再去磁盘加载进缓冲池;写也是先写缓冲池,然后等后台线程去刷写磁盘。线程池默认128M,也可以自己设置。
我们知道数据基本读写单位是页,在缓冲区里也一样,也是按照页来分配的,叫缓存页(包括数据页、索引页、undo页等)。为了更好地管理这些页,MySQL设计了一个控制块来存储页的地址、页号等等,控制块也是占空间的,最后可能会有碎片空间,也就是控制块和缓存页中间的灰色区域。
如何管理缓冲池?
空闲页
用Free链表指向空闲的缓存页的控制块,之后有人要写就根据这个链表来找位置写,然后从链表中移除它。
脏页
如何修改sql数据?思路是不要改一个就写一次磁盘。用Flush链表记录脏页(被修改过的记录所在页),等后台线程来将脏页写入磁盘。
那什么时候刷写磁盘呢?万一还没来得及刷写MySQL就宕机了那不寄了
其实MySQL是用了一种Write Ahead Log 策略,即先写日志,再写入磁盘,通过 redo log 日志(mysql持久性的保证,undo log是原子性的保证)让 MySQL 拥有了崩溃恢复能力。
下面几种情况会触发脏页的刷新:
- 当 redo log 日志满了的情况下,会主动触发脏页刷新到磁盘;
- Buffer Pool 空间不足时,需要将一部分数据页淘汰掉,如果淘汰的是脏页,需要先将脏页同步到磁盘;
- MySQL 认为空闲时,后台线程会定期将适量的脏页刷入到磁盘;
- MySQL 正常关闭之前,会把所有的脏页刷入到磁盘;
提高缓存命中率
我们知道缓冲池的大小有限,所以肯定数据有出有进,因此我们要尽可能提升常用的数据在缓存中的 时间。因此设计了LRU链表(Least Recently Used),也就是说新用到的缓存页放到表头,表尾自动移除一个页,但涉及了两个问题需要解决。
预读失效
程序局部性,即加载一个地方的数据,会一次性连带加载其附近的数据,本质上是好的,因为很大概率相近位置的数据会在相近时刻被用到。
但假设没用到呢?也就是说我白干了,这就是预读失效。
这就寄了,不用的数据被预读到链表头部,反而先前用过的数据(可能还会再用的)被挤到尾部甚至移除了。
MySQL的思路是划分young和old区域
新加入的页20放到old里
假设页20又被访问了,才会放到young里
Buffer Pool污染
假设一条sql语句会导致大量数据页被加载进来,就很可能把buffer空间顶满了,之前的数据(很可能是大量的热数据)都会被顶出去,young区间也不例外。
怎么解决?
其实这些被大量读入的数据页很可能就会被用到一次,那只访问一次时候不让它进入young区域而是记录下访问页的时间,假设在某个时间范围内重复读取,那就不算是热数据(一页里多个数据行被反复用到,之后就没被用到了),那数据页还是在old里;假设在某个时间范围外都被重复读取了,那就证明有多条语句对这部分数据有需求,那就是热数据,这才会被从old移动到young里。时间默认是1s。
实际上MySQL还对young做了个优化,为防止young区域频繁出现头部节点变更,young里前1/4的页被访问了不会移动到头部,后3/4才会。