缓存Caffeine之W-TinyLFU淘汰测录

news/2023/6/7 0:54:04

我们常见的缓存是基于内存的缓存,但是单机的内存是有限的,不能让缓存数据撑爆内存,所有需要缓存淘汰机制。https://mp.csdn.net/editor/html/115872837 中大概说明了LRU的缓存淘汰机制,以及基于LRU的著名实现guava cache。除了LRU淘汰策略外,其是常见的还有FIFO以及LFU,只是说目前用的最多的是LRU。

LRU

LRU记录了缓存中数据项的访问时间,在缓存数据量达到一定成都的时候,淘汰掉最远访问的那些数据项。

LRU的理论基础就是局部性原理,认为最近访问过的数据,在未来还会继续访问的概率是比较大的,所以当缓存满的时候,首先将那些很久没有访问过的数据项给淘汰出缓存。

优点:实现简单(链表+hash表),且命中率还是相对比较高的。所以LRU也是当前使用的最多的缓存淘汰算法。

更多更具体的介绍可参考:LRU算法及其优化策略——算法篇 - 掘金

缺点:对于偶发的批量操作支持不友好,偶发的批量操作会降低缓存命中率。这种批量操作会将非热点数据打量加载到内存,按照LRU可能就将真正的热点数据挤出缓存。

mysql的优化

LRU算法中,对批量支持不友好的本质原因其实还是LRU算法淘汰的依据仅仅是访问时间,缺少数据项的访问次数的统计,当需要淘汰的时候,只能根据访问时间来进行淘汰,所以就会导致偶尔的一次批量操作将访问频率低的数据项读入到缓存。所以解决这个问题的办法就是加入对数据项访问量的统计,在淘汰策略的时候,将访问频率也加入到决策因素中。

mysql将整个LRU分成了两段来避免这个问题,为每个数据项增加了一个统计时间窗口来解决这个问题(这个其本质就是增加了统计访问频率),如下图:

  1. 缓存淘汰的位置是从old读队尾开始淘汰的,即当缓存不够的时候,优先将old端队尾位置的元素剔除缓存

  1. 当数据读入的时候,是将该数据项放到了old的队头。 然后给这个数据项一个统计时间窗口,即innodb_old_blocks_time参数指定的时间段,只有在指定时间窗口内,这个数据项再次被访问到的时候,就会将这个数据项挪到yong端。

这样,热点数据都是放在了yong端,优先淘汰的数据都是在old端。偶发的批量操作读取的数据都在old,会优先于yong端数据被淘汰,从而避免批量操作将热点数据淘汰出缓存,降低命中率。

LFU

LFU为缓存中的每个数据项都维护了一个计数器,会统计每个缓存项的访问次数,当缓存使用量达到一定程度的时候,会优先淘汰那些访问次数比较小的数据项

LFU是基于这种思想进行设计:一定时期内被访问次数最少的数据项,在将来被访问到的几率也是最小的。具体可参考LFU算法及其优化策略——算法篇 - 掘金

优点:很明显,LFU是没有LRU偶发批量操作导致的将热点数据挤出缓存的问题。

但是LFU的缺点也很明显:

  1. 对于那种大热门而又迅速冷却的数据,比较难以淘汰出缓存。比如某明星出轨消息,当刚曝出时,浏览量特别高,可能段时间内就是上千万次的读取,但是第二天就没人关心这个信息了,那几乎就没什么读取量了,但是因为第一天太热门了,远远超过了其他数据项的读取次数

  1. 因为淘汰是基于数据项的访问次数的,那么对于新加入到缓存的数据,更容易被淘汰,除了偶发的批量操作导致的新加入到缓存的数据以外,更多的情况可能是新加入到缓存的数据被后续操作访问的可能性是比较大的。这种数据被提出缓存,就有可能造成很快又被加载进来的。

  1. 因为LFU需要为每个数据项维护访问频率的数据,一方面实现复杂度相对就较高了,而且会额外占用空间。另外一个问题就是,这个访问次数如果是有史以来的访问次数和,那其实根据这个访问次数实现的淘汰策略必定不是很理想,所以这个访问次数一定是最近一段时间的访问次数,这就有个问题了,最近一段时间是多长时间?以及到期的时候这些访问次数的清理其实都是比较麻烦的事情。

不管是FIFO、LRU还是LFU,解决了一定问题,但是还是会有其他的问题出现,归根接地,其原因无外乎是以为淘汰策略参考的因素单一:FIFO只是参考了进入队列时间、LRU只是参考了最近访问时间、LFU只是参考了访问次数。所以都会有些问题。

所以在实际生产中,一个缓存框架的淘汰策略,实际上很少会单独使用某一个指标来决定淘汰策略,比如mysql的缓存,虽然整体上是LRU,但是实际上也引入了访问次数的因素。所以往往都是将多个维度相结合,共同来决定应该淘汰缓存转给你的哪些数据,达到既不会将缓存撑爆,又能够维持一个比较搞的缓存命中率,使得缓存效果最大化。

W-TinyLFU

这个淘汰算法其实就是结合LRU和LFU算法的优缺点的一个缓存淘汰算法,说白了就是要保留LRU和LFU的这些优点(淘汰策略要结合最近访问时间和访问次数),并规避掉上述各自的问题。

参考:W-TinyLFU论文阅读与原理分析

TinyLFU

要为缓存中的每个数据项维护一个计数器,最基本的结构那就是一个HashMap,key=缓存项的key,value就是这个缓存项的访问计数。这个结构实现基本功能是没问题的,但是问题在于效率和空间上:key是数据项的key,会占据额外的空间的,另外每次访问缓存都需要修改对应数据项的计数器,这里就会有并发问题,且如果当hash有冲突,那么这个访问效率就会直线下降,导致缓存的访问不稳定。对于一个高效的缓存来说,这都是致命的问题。

所以就需要找到一个减少空间占用的方式:那就是压缩,但需要考虑怎么从压缩后的数据里获得对应key的频率。对于使用HashMap的场景中,如果需要空间考虑,那么可以参考存在性判断的思路:使用BitMap,但是BitMap的问题就是hash冲突,解决它的方式就是布隆过滤器,用多个hash函数来减少hash冲突,然后容忍小概率的hash冲突导致的不准确(这也是为啥说boomfilter对于不存在判断一定是准的,但是存在判断存在误差的原因)。

Count-Min Sketch也是采用了类似布隆过滤的方式来为数据项维护计数,和布隆过滤器的区别在于,布隆过滤器的值,只需要一个bit就足够用来表示是否存在在了,但是Count-Min Sketch,对应的值需要是一个计数值,就不能是一个bit了,采用4bit来进行计数。

Caffeine的实现中认为,一个数据项的访问频率到了15就算是很高了,就属于热点数据了,所以采用4bit来存储计数就已经够了。当计数值达到15的时候,就将技术值减半,这也起到了一个衰减的作用,达到计数器记录的是"最近"访问次数的效果,从而也解决了那种热极一时的数据不容易淘汰的问题。

为了减少hash冲突的问题,Caffeine使用了4个hash函数,为每个数据项保存了4个计数器,即一个数据项需要4*4=16bit来存储访问频率,而在使用淘汰的时候,取的是这4个计数值中的最小值来进行访问频率比较;只要一个计数器的值达到15,则中4个计数值都将减半。

Caffeine的TinyLFU的实现在FrequencySketch中,

获取一个数据项的技术统计频率,是取四个计数器中的最小值,这也是为啥叫TinyLFU的原因:

数据项的访问次数+1

Caffeine是用一个long数组来保存数据项的访问计数的,一个long有64位,可以充当16个计数器,但其实要通过位运算完全利用上这64,java提供的能力还有有点困难,所以Caffeine取了数据项key的hash值的后左移两位,然后加上hash四次,可以利用到16个中的13个,利用率挺高的,或许有更好的算法能将16个都利用到。

ps:FrequencySketch里大量使用了位运算,代码阅读其实很不直观,可以先了解原理,然后结合debug、注释看。

W-TinyLFU

Count-Min Sketch算法解决了LFU中频率统计的问题,但是对于新加入数据更容易被淘汰、热极一时的数据项更难被淘汰出去的问题还存在,而这些问题正式LRU不会存在的。所以W-TinyLFU就是在TinyLFU前面增加了一个LRU缓存。它将缓存分成了两大部分:Window cache和main cache,不同区域采用不同的淘汰策略

  • window cache:这部分是一个LRU缓存。当读入缓存的时候,都是先进入到window cache;当window cache满的时候,按照LRU淘汰出数据项,对于从window cache淘汰出的数据项,再根据TinyLFU来决策是否进入到main cache中。在Caffine的实现中,window cache默认是整个cache的1%,缓存数据是通过一个ConcurrentHashMap来实现,而window cache的LRU策略是通过一个双端队列:accessOrderEdenDeque来实现的。

  • main cache:进一步划分为两部分

  • protected部分:在Caffine的实现中,这部分默认占main cache的80%,其淘汰策略的实现是通过一个双端队列accessOrderProtectedDeque来实现的。这里面存放的其实是访问频率比较高的热数据,这是整个缓存的核心部分,是高缓存命中的保证。

  • probation部分:在Caffine的实现中,这部分默认占main cache的20%,其淘汰策略的实现是通过一个双端队列accessOrderProbationDeque来实现的。里面存放的其实就是访问频率比较少的冷数据,即即将别淘汰出缓存的数据。

Caffeine的整体实现在BoundedLocalCache中(源码分析的可参考http://events.jianshu.io/p/77e90f79f075,Caffeine的使用可参考https://www.chinacion.cn/article/5629.html,缓存的概要介绍可参考https://my.oschina.net/u/4072299/blog/3019442)

从整个实现上可以发现,在思路上和mysql对lru的优化都是打通小异的,都是将整个LRU缓存进行分区,然后在实际执行淘汰的时候加上数据项的访问次数。

  • 对于mysql的优化,对于偶发的批量操作数据全都在old端,当整体缓存不够的时候会被优先淘汰,真正的热数据在yong端,yong端不够用了,就会按照LRU淘汰到old,old的数据如果再次被频繁访问又会被放到yong端,从而达更高的缓存命中。

  • caffeine其实是类似的,新加载进来的数据首先会进入到window cache,包括偶发的批量操作的数据项,而真正将数据项淘汰出缓存是从probation,所以新加入的数据项,只会等到window cache满的时候,才会去比较频率,而这个时候新加入的数据项可能访问次数又变高了,在跟probation淘汰出来的数据项比较频率的时候,最终淘汰掉的是probation中的数据项,即window cache出来的数据项访问频率并不高,也不一定会被淘汰,因为有个访问次数小于5,是随机淘汰的机制。所以window cache就解决了LFU新加入数据项访问频率低容易被淘汰的问题。

tinyLFU解决了数据项频率统计占用空间的问题,而且计数器满自动筛检一半的机制又避免了热极一时的数据项不容易被淘汰的问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.exyb.cn/news/show-4558831.html

如若内容造成侵权/违法违规/事实不符,请联系郑州代理记账网进行投诉反馈,一经查实,立即删除!

相关文章

java:从淘宝获取优惠券的、带推广链接的、带淘口令的商品信息

必要准备 阿里妈妈账号注册推广管理商品推广推广位管理 淘宝开发API 实现步骤 完成阿里妈妈账号注册、及网站或无线APP备案审核通过(获取App Key、App Secret): 阿里妈妈账号注册 推广管理 进入淘宝联盟—>推广管理—>推广资源管理—>媒体管理—>网站…

JVM-内存模型详解

JVM 把内存分为若干个不同的区域,这些区域有些是线程私有的,有些则是线程共享的,Java 内存区域也叫做运行时数据区,它的具体划分如下: 虚拟机栈 Java 虚拟机栈是线程私有的数据区,Java 虚拟机栈的生命周期…

第二篇 我的第一个开发程序

我的第一个程序开发,这次是一个控制台的程序,教给大家做一个简易的计算器 话不多说,直接上代码:底部附教程视频,第一次开发的朋友可以看看教程视频,没有安装开发工具的朋友可以下载一个开发工具&#xff0…

业务总把工程师当成资源或者工具,这该悲哀吗?

点击上方“蓝色字体”,选择“设为星标”做积极向上的前端人!7 月,玉伯在他的一次演讲里这么说,曾几何时,他觉得资源是个贬义词。因为有业务的同学在和他讨论项目排期的时候,总是提到资源这词,潜…

TypeScript 研发规约落地实践

本文分享的内容是笔者在淘宝店铺迁移到 TypeScript,以及落地相关研发规约的经验。全文从 TypeScript 研发侧规范、 TypeScript 工程侧规范、TypeScript Compiler、 TypeScript 的竞争者和工具链、总结五个方面展开。在开始前,我们先做一个简单的铺垫。淘…

基于cocos2dx的2D手游美术资源制作技术选型(1)--UI、纹理格式、动画制作 - 宏波.王

一、在屏幕尺寸和分辨率变化不一的情况下,UI如何做机型适配? UI是应用的门户,相对来说IOS分辨率较为简单,但考虑到retina与非retina,iphone与ipad, ipad mini,则IOS的分辨率也要支持4种,而Android机型是典型的碎片化&a…