在项目开发中,为提高系统性能,减小 IO 开销,本地缓存是必不可少的。最多见的本地缓存是 Guava 和 Caffeine,本篇文章将为你们介绍 Caffeine。html
Caffeine 是基于 Google Guava Cache 设计经验改进的结果,相较于 Guava 在性能和命中率上更具备效率,你能够认为其是 Guava Plus。git
毋庸置疑的,你应该尽快将你的本地缓存从 Guava 迁移至 Caffeine,本文将重点和 Guava 对比两者性能占据,给出本地缓存的最佳实践,以及迁移策略。github
2、PK Guava
2.1 功能

从功能上看,Guava 已经比较完善了,知足了绝大部分本地缓存的需求。Caffine 除了提供 Guava 已有的功能外,同时还加入了一些扩展功能。web
2.2 性能
Guava 中其读写操做夹杂着过时时间的处理,也就是你在一次 put 操做中有可能会作淘汰操做,因此其读写性能会受到必定影响。redis
Caffeine 在读写操做方面完爆 Guava,主要是由于 Caffeine 对这些事件的操做是异步的,将事件提交至队列(使用 Disruptor RingBuffer),而后会经过默认的 ForkJoinPool.commonPool(),或本身配置的线程池,进行取队列操做,而后再进行后续的淘汰、过时操做。算法
如下性能对比来自 Caffeine 官方提供数据:数据库
(1)在此基准测试中,从配置了最大大小的缓存中,8 个线程并发读:缓存
(2)在此基准测试中,从配置了最大大小的缓存中,6个线程并发读、2个线程并发写:微信

(3)在此基准测试中,从配置了最大大小的缓存中,8 个线程并发写:并发

2.3 命中率
缓存的淘汰策略是为了预测哪些数据在短时间内最可能被再次用到,从而提高缓存的命中率。Guava 使用 S-LRU 分段的最近最少未使用算法,Caffeine 采用了一种结合 LRU、LFU 优势的算法:W-TinyLFU,其特色是:高命中率、低内存占用。
2.3.1 LRU
Least Recently Used:若是数据最近被访问过,未来被访问的几率也更高。每次访问就把这个元素放到队列的头部,队列满了就淘汰队列尾部的数据,即淘汰最长时间没有被访问的。
须要维护每一个数据项的访问频率信息,每次访问都须要更新,这个开销是很是大的。
其缺点是,若是某一时刻大量数据到来,很容易将热点数据挤出缓存,留下来的极可能是只访问一次,从此不会再访问的或频率极低的数据。好比外卖中午时候访问量突增、微博爆出某明星糗事就是一个突发性热点事件。当事件结束后,可能没有啥访问量了,可是因为其极高的访问频率,致使其在将来很长一段时间内都不会被淘汰掉。
2.3.2 LFU
Least Frequently Used:若是数据最近被访问过,那么未来被访问的几率也更高。也就是淘汰必定时间内被访问次数最少的数据(时间局部性原理)。
须要用 Queue 来保存访问记录,能够用 LinkedHashMap 来简单实现一个基于 LRU 算法的缓存。
其优势是,避免了 LRU 的缺点,由于根据频率淘汰,不会出现大量进来的挤压掉老的,若是在数据的访问的模式不随时间变化时候,LFU 可以提供绝佳的命中率。
其缺点是,偶发性的、周期性的批量操做会致使LRU命中率急剧降低,缓存污染状况比较严重。
2.3.3 TinyLFU
TinyLFU 顾名思义,轻量级LFU,相比于 LFU 算法用更小的内存空间来记录访问频率。
TinyLFU 维护了近期访问记录的频率信息,不一样于传统的 LFU 维护整个生命周期的访问记录,因此他能够很好地应对突发性的热点事件(超过必定时间,这些记录再也不被维护)。这些访问记录会做为一个过滤器,当新加入的记录(New Item)访问频率高于将被淘汰的缓存记录(Cache Victim)时才会被替换。流程以下:

尽管维护的是近期的访问记录,但仍然是很是昂贵的,TinyLFU 经过 Count-Min Sketch 算法来记录频率信息,它占用空间小且误报率低,关于 Count-Min Sketch 算法能够参考论文:pproximating Data with the Count-Min Data Structure
2.3.4 W-TinyLFU
W-TinyLFU 是 Caffeine 提出的一种全新算法,它能够解决频率统计不许确以及访问频率衰减的问题。这个方法让咱们从空间、效率、以及适配举证的长宽引发的哈希碰撞的错误率上作均衡。
下图是一个运行了 ERP 应用的数据库服务中各类算法的命中率,实验数据来源于 ARC 算法做者,更多场景的性能测试参见官网:

W-TinyLFU 算法是对 TinyLFU算法的优化,可以很好地解决一些稀疏的突发访问元素。在一些数目不多但突发访问量很大的场景下,TinyLFU将没法保存这类元素,由于它们没法在短期内积累到足够高的频率,从而被过滤器过滤掉。W-TinyLFU 将新记录暂时放入 Window Cache 里面,只有经过 TinLFU 考察才能进入 Main Cache。大体流程以下图:

3、最佳实践
3.1 实践1
配置方式:设置 maxSize
、refreshAfterWrite
,不设置 expireAfterWrite
存在问题:get 缓存间隔超过 refreshAfterWrite
后,触发缓存异步刷新,此时会获取缓存中的旧值
适用场景:
-
缓存数据量大,限制缓存占用的内存容量 -
缓存值会变,须要刷新缓存 -
能够接受任什么时候间缓存中存在旧数据

设置 maxSize
、refreshAfterWrite
,不设置 expireAfterWrite
3.2 实践2
配置方式:设置 maxSize
、expireAfterWrite
,不设置 refreshAfterWrite
存在问题:get 缓存间隔超过 expireAfterWrite
后,针对该 key,获取到锁的线程会同步执行 load,其余未得到锁的线程会阻塞等待,获取锁线程执行延时过长会致使其余线程阻塞时间过长
适用场景:
-
缓存数据量大,限制缓存占用的内存容量 -
缓存值会变,须要刷新缓存 -
不能够接受缓存中存在旧数据 -
同步加载数据延迟小(使用 redis 等)

设置 maxSize
、expireAfterWrite
,不设置refreshAfterWrite
3.3 实践3
配置方式:设置 maxSize
,不设置 refreshAfterWrite
、expireAfterWrite
,定时任务异步刷新数据
存在问题:须要手动定时任务异步刷新缓存
适用场景:
-
缓存数据量大,限制缓存占用的内存容量 -
缓存值会变,须要刷新缓存 -
不能够接受缓存中存在旧数据 -
同步加载数据延迟可能会很大

设置 maxSize,不设置 refreshAfterWrite
、expireAfterWrite
,定时任务异步刷新数据
3.4 实践4
配置方式:设置 maxSize
、refreshAfterWrite
、expireAfterWrite
,refreshAfterWrite
< expireAfterWrite
存在问题:
-
get 缓存间隔在 refreshAfterWrite
和expireAfterWrite
之间,触发缓存异步刷新,此时会获取缓存中的旧值 -
get 缓存间隔大于 expireAfterWrite
,针对该 key,获取到锁的线程会同步执行 load,其余未得到锁的线程会阻塞等待,获取锁线程执行延时过长会致使其余线程阻塞时间过长
适用场景:
-
缓存数据量大,限制缓存占用的内存容量 -
缓存值会变,须要刷新缓存 -
能够接受有限时间缓存中存在旧数据 -
同步加载数据延迟小(使用 redis 等)

设置 maxSize
、refreshAfterWrite
、expireAfterWrite
4、迁移指南
4.1 切换至 Caffeine
在 pom 文件中引入 Caffeine 依赖:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
Caffeine 兼容 Guava API,从 Guava 切换到 Caffeine,仅须要把 CacheBuilder.newBuilder()
改为 Caffeine.newBuilder()
便可。
4.2 Get Exception
须要注意的是,在使用 Guava 的 get()
方法时,当缓存的 load()
方法返回 null
时,会抛出 ExecutionException
。切换到 Caffeine 后,get()
方法不会抛出异常,但容许返回为 null
。
Guava 还提供了一个getUnchecked()
方法,它不须要咱们显示的去捕捉异常,可是一旦 load()
方法返回 null
时,就会抛出 UncheckedExecutionException
。切换到 Caffeine 后,再也不提供 getUnchecked()
方法,所以须要作好判空处理。
本文分享自微信公众号 - 浪尖聊大数据(bigdatatip)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。