Innodb的缓存池主要是解决热点数据的IO资源消耗问题。
没有缓存池时,每次数据请求,都通过IO将数据从磁盘读到内存里。对于频繁访问的数据,应该设计成少量次数或者一次读入,后续不更新此数据的话就避免IO的消耗,直接使用此数据。
那就给Mysql独立分配一块内存当做缓存池。这样从表空间加载的数据先加载到缓存池中,数据请求缓存池,如果数据不在缓存池中,再通过IO加载文件。
那么对于频繁访问和一次性访问的数据应该区别对待,我们希望热点数据能一直存在缓存池中,不会被一次性的数据替换。可以设计出LRU链表(Least Recently Used),越热点数据越靠近头结点保存,这样可以大大的提高缓存的命中率;这样确实可以有效的提高缓存命中,缓存大小有限意味着列表的长度也是固定的,如果一次性读入的数据量接近甚至大于链表容量时,一次性访问数据依然会替换掉热点数据给链表来了一次大换血,这样缓存池辛苦建立的优势付之一炬。
进一步优化,将LRU链表分为两段,一段专门放置热点数据(非常热、东京热的那种),另一段用于放置冷数据(比较冷、冷宫冷的那种)、一次性访问数据。这样既保住缓存命中率,又不会被一次性数据替换。
我对Mysql的缓存池设计原理大致如此,缓存池的细节肯定复杂的多。
- 首先缓存池大小可配置,默认256M,最小5M。
- 缓存池使用表空间号+页号作为key,缓存页作为value的hash表来进行管理
- 缓存池中被修改的页成为脏页,脏页会加入到flush链表里进行后台同步到磁盘。必要时也可能直接同步到磁盘
- LRU链表的冷热数据分段大小也可以设置,比如默认innodb_old_blocks_pct为37。冷数据区占总链表的37%
- 一页会保存很多数据,对同一页的不同数据多次访问都算是访问该页面。那全表扫描时,所有的页都有可能成为热数据,为了区别热数据还是冷数据,innodb_old_blocks_time设定了区别,默认1000ms。当对同一页的第一次与最后一次访问间隔时间小于此间隔,认定此页并不是热数据,会保存在冷数据区。类似select * from tableName结果应该保存在冷数据区,超过1000s之后又访问了其中的某一页,那这一页才会从冷数据区移动到热数据区。
- LRU链表对热数据段又做了优化,随着访问次数的叠加,越靠近头结点的数据应该是访问频率越高的数据,访问相对次数少的就靠近尾节点。但对于热数据区前1/4的数据页频繁的访问可能导致频繁的位置交换,性能上反而下降了,于是热数据区前1/4的数据页不随着访问次数进行排序
- 对于数据的访问,Mysql还提供了预读功能,异步加载之后的数据页
- 有时,缓存池不够用了,也有可能使用用户线程直接从LRU链表里将已修改的数据页同步到磁盘,这非常影响用户的性能吧
- 单一缓存池面临高并发场景比较吃力。于是缓存池实例也支持有多个,并且每个实例又可以有多个chunk结构,方便处理并发行为,动态扩展等