底层数据结构
Guava:双向链表
Caffeine: ConcurrentHashMap + Count-Min Sketch(哈希表 + 二维数组)
相比 Guava,Caffeine 主要在底层数据结构和淘汰算法上进行优化
对比Guava
一、淘汰算法
-
Guava Cache: LRU变体
Guava Cache 维护了一个访问队列(通常是双向链表)和一个写队列,每当一个条目被访问(
get),它就会被移动到访问队列的头部(或尾部,取决于实现),当需要淘汰数据时,直接从队列的另一端(尾部)移除最久未被访问的条目。缺点:
-
无法应对突发流量:被高频访问的数据可能因为短时间的突发流量被淘汰掉
-
无法分辨频率:只记录最近使用,不记录频次
-
高开销:每次访问都需要修改链表(移动节点),高并发下需要加锁,带来性能损耗
-
-
Caffeine: W-TinyLFU
一种算法
-
解决频率统计问题
底层原理:维护一个
Count-Min Sketch的概率数据结构来近似统计访问频率。Count-Min Sketch是布隆过滤器的变体,使用一个小的二维数组和多个哈希函数统计概率-
写入:当一个条目被访问时,用多个哈希函数计算其在二维数组中的位置,并对这些位置的计数器进行增量操作。
-
读取:同样通过多个哈希函数找到多个计数器,取其中的最小值作为该条目的频率估计值
优势:用极小的、固定的空间,换取高精度估算所有条目的访问频率,内存效率高
-
-
应对突发流量
底层原理:W-TinyLFU 将缓存分为两个区域:
-
主缓存(~99%):使用 TinyLFU 算法,存放被证明是热点的长期数据
-
窗口缓存(~1%):一个很小的、采用简单的 LRU 算法的缓存
工作流程:
-
新条目先进入窗口缓存
-
如果该条目在窗口缓存中被再次访问,它就获得了“晋升”资格。
-
当主缓存需要淘汰数据时,会比较窗口缓存中“最可能被晋升”的条目和主缓存中“频率最低”的条目。
-
胜者(频率更高者)进入主缓存,败者被淘汰。
-
-
二、并发性能差异
-
Guava Cache:基于同步锁和队列
底层原理:为实现 LRU 淘汰机制,每次
get操作都需要加锁对链表进行修改,写操作以及定期维护(清理过期条目)同样需要加锁。缺点:大量的线程会阻塞在锁的获取上,导致性能急剧下降,性能开销与并发线程数呈线性关系
-
Caffeine:更细粒度的并发控制和异步操作
底层原理:
-
get操作无锁:基于ConcurrentHashMap的优化实现,get操作通常是无锁的(或使用更细粒度的锁),不会因为统计频率而阻塞。 -
时间轮算法管理过期:Caffeine 使用时间轮来管理条目过期。时间轮将时间分成多个间隔(tick)。每个过期时间相近的条目会被放入同一个时间格的链表中。
-
优势:清理过期条目时,无需遍历所有数据,只需处理当前时间格和即将到期的时间格即可,效率极高,且避免了全局锁。
-
-
异步操作:很多维护任务(如频率衰减、数据淘汰)被提交到独立的线程池中异步执行,不会阻塞主业务线程。
-
三、内存开销差异
-
Guava Cache:为每个条目维护链表节点
底层原理:为实现 LRU,每个缓存条目除了存储 key 和 value 外,还需要额外的内存来维护前后指针,以形成双向链表
缺点:每个条目都有固定的、额外的内存开销
-
Caffeine:固定的、极小的概率数据结构
底层原理:频率统计由全局共享的、固定大小的
Count-Min Sketch负责优势:占用内存极小且内存固定
453

被折叠的 条评论
为什么被折叠?



