缓存池管理
1.简介¶
缓存池是内存中的一块区域,用于存放读取自磁盘的page。
在数据库中,缓存池可以看成由固定大小的frame组成的数组,其中frame用于存储页。
缓存池提供的是类似于虚拟内存的机制,为数据库提供了内存中可以放下所有数据的假象。因此,他需要提供类似虚拟内存的封装。如在获取一个不存在于内存中的page时,如果缓存池已满,则需要根据策略选择牺牲页写回到磁盘中,然后再从磁盘中复制数据到缓存池中,最后返回结果。
2.元数据¶
缓存池中常需要维护一些元数据,用于标识页的信息。包括但不限于:
- dirty flag:表示该页是否有被修改过
- pin:表示该页不允许被写回到磁盘中
- reference counter:该页正在被引用的线程的次数,当线程在获取页的时候会去将该值加一,使用完后将该值减一。显然当该值大于零时,不应该让该页写回磁盘中。
3.优化手段¶
3.1.多缓存池¶
DBMS应该维护多个缓存池,其中每个缓存池是用于不同的场景的。
一个显然的原因是,若只维护一个缓存池,由于一个缓存池只能同时使用一个牺牲策略,那么对于正在进行不同workload任务的线程来说,它们采用的牺牲策略可能不都是最优的。
其次,由于数据库常常是支持并发的,那么为了保护数据结构,在对数据结构操作时就需要进行latch的获取。显然,减少线程之间的竞争也能加快运行速率。
3.2.Pre-fetching¶
对于顺序扫描或者其他可以提前预知需要什么页的查询来说,可以提前将其获取到缓存池中。
3.3.Scan Sharing¶
对于可能拥有交集的查询来说,重复利用另一个线程已经获取到缓存池中的页,比自己再去从磁盘里获取的效率会高。
3.4.Buffer Pool Bypass¶
对于顺序扫描来说,一个缓存池常常是放不下所有所需的页的,此时就需要选择牺牲页写回到磁盘中。但是缓存池中的数据可能是热点数据,经过扫描之后就都被写回到磁盘中了,还要再重新获取。
解决方法是另起一个暂时的缓存池,只用于该查询,而不去干扰原有的缓存池。
4.操作系统页缓存¶
许多关于磁盘的读写操作是基于操作系统的API,而操作系统自身会维护一个系统级别的缓存,称之为os page cache。
由于这会造成缓存冗余,即os的缓存中有一份,数据库的缓存中也有一份,因此常用的手段是使用直接内存I/O,让os不缓存相关数据。
5.牺牲策略¶
5.1.LRU¶
思想是将最久之前使用的页去除。
方式是维护一个队列,每当使用页的时候,就将该页提到队头,那么队尾就是最久之前使用的页。
缺点是偶尔的顺序操作会污染缓存池,从而丢失热点数据。
5.2.LRU-K¶
维护两个队列,一个是历史队列,一个是缓存队列。
历史队列保存最近访问的页面及其访问次数,当访问次数到达k次时,放入缓存队列。
缓存队列则根据一定的策略如LRU,LFU,FIFO等进行维护。
命中率要比LRU要高,但是因为需要维护一个历史队列,因此内存消耗会比LRU多。
实际应用中LRU-2是综合各种因素后最优的选择,LRU-3或者更大的K值命中率会高,但适应性差,需要大量的数据访问才能将历史访问记录清除掉。
5.3.LFU¶
思想是将最少使用的页去除,每个页维护一个访问次数,每次访问该页时就增加这个访问次数,牺牲时将最小访问次数的页去除。
缺点是对于交替出现的数据命中率低。
5.4.Clock¶
一种近似牺牲策略。
方式是将页组织成一个环形区域,并给每个页维护一个引用位。只要该页被访问过,该位就会被置为1。
需要选出牺牲页的时候,就对该环形区域进行扫描。若扫到的页的引用位为0,则表示该页最近没有被引用,可以去除。为了保证死循环,若扫到的页的引用位为1,则置引用位为0。
5.5.Dirty Pages¶
由于写回一个脏页是需要修改磁盘数据的,这拥有I/O成本,而一个非脏页由于数据没有变化,因此不需要写回磁盘,直接从缓存池中去除即可。