高性能 Java 缓存框架 — Caffeine Cache

Caffeine Cache 是替代 Guava Cache 的新一代缓存技术,在性能方面,Caffeine 已经全面超越 Guava。

Caffeine github 官方地址

常见缓存淘汰算法

淘汰算法是缓存技术的灵魂,常见淘汰算法有以下几种:

FIFO:先进先出,先进入先淘汰,命中率低;
LRU:最近最少使用,把每次访问到的数据放到队尾,淘汰的时候从队首开始淘汰,没有考虑访问频率问题,并发高时可能淘汰掉真正的热点数据;
LFU:最近最少频率使用,记录缓存中每个数据的使用频率,优先淘汰使用频率低的。如果热点数据有时效性,会出现以前的某个热点数据由于之前命中频率过高,导致一直无法淘汰,占用缓存空间的问题。

以上三种淘汰算法各有各的缺点,实现成本也一个比一个高。命中率最好的 LFU 有两大弱点:

  1. 需要给每个记录维护一个频率信息,每次访问都有更新,挤占内存空间且降低性能
  2. 如果访问模式随时间有变,LFU 的频率信息无法随之变化,早起频繁访问但是后期不再访问的数据会一直挤占缓存空间

对应大多数应用来说,访问模式都是随时间变化的,LFU 并不适合,所以 Guava 采用的是 LRU 算法。但是 LRU 算法要实现和 LFU 算法同样的 命中率就需要更大的缓存空间。

W-TinyLFU

Caffeine Cache 为了避免上面几种算法的弱点采用了 W-TinyLFU 淘汰算法。 W-TinyLFU 是由前 google 工程师提出的一种现代缓存技术淘汰算法,他结合了 LRU 和 LFU 的优势,避免了他们的缺点。

为了应对额外记录频率信息会占用空间的问题,W-TinyLFU 采用了一种叫 Count-Min Sketch 的数据结构来记录频率信息, 从而大大缩小了记录频率信息所需的内存空间。

Count-Min Sketch 是一个概率数据结构(有点像布隆过滤器的一个变种),实际就是一个二维数组。 每一行就是一个 Sketch,每个 Sketch 对应一个 hash 函数。 每当一个 k-v 被放入缓存时,每一行的 hash 函数会将这个 key 映射成本行的一个 index,然后把对应 index 位置的数字加 1。

为了解决访问模式随时间变化的问题,W-TinyLFU 采用了一种 reset 操作来衰减旧数据的频率值,简单来说就是 当缓存中的数据频率数达到某个数值之后就将所有 Sketch 上的每一个频率数据都除以 2,这样就达到了衰减频率数据的目的。

使用 Caffeine Cache

  1. 依赖
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.8.6</version>
</dependency>
  1. 创建缓存
Cache<String, Object> cache = Caffeine.newBuilder()
    .initialCapacity(100)//初始大小
    .maximumSize(1000)//最大数量
    .expireAfterWrite(1800, TimeUnit.SECONDS)//过期时间
    .build();

创建参数说明:

initialCapacity: 初始的缓存空间大小
maximumSize: 缓存的最大数量
maximumWeight: 缓存的最大权重
expireAfterAccess: 最后一次读或写操作后经过指定时间过期
expireAfterWrite: 最后一次写操作后经过指定时间过期
refreshAfterWrite: 创建缓存或者最近一次更新缓存后经过指定时间间隔,刷新缓存
weakKeys: 打开key的弱引用
weakValues:打开value的弱引用
softValues:打开value的软引用
recordStats:开发统计功能
  1. 填充策略

3.1 手动填充

//填充
cache.put("hello",value);
//获取
cache.get("hello");
//手动过期
cache.invalidate("hello");

3.2 同步填充

LoadingCache<String, Object> cache = Caffeine.newBuilder()
    .initialCapacity(100)//初始大小
    .maximumSize(1000)//最大数量
    .expireAfterWrite(1800, TimeUnit.SECONDS)//过期时间
    .build(key->getDate(key));

3.3 异步填充

AsyncCache<String, Object> cache = Caffeine.newBuilder()
    .initialCapacity(100)//初始大小
    .maximumSize(1000)//最大数量
    .expireAfterWrite(1800, TimeUnit.SECONDS)//过期时间
    .buildAsync(key->getDateAsync(key).get());

4 Caffeine 的回收策略

Caffeine提供了三种回收策略

4.1 基于大小的回收策略

maximumSize:

maximumWeight:

**注意:maximumSize 和 maximumWeight 不能同时设置**

4.2 基于时间的回收策略

expireAfterAccess:在最后一次访问或者写入后开始计时,在指定的时间后过期。

expireAfterWrite:在最后一次写入缓存后开始计时,在指定的时间后过期。

expireAfter:自定义到期策略

注意:这三种到期策略也只能选其中一个

4.3 基于引用的

weakKeys:使用弱引用存储key。如果没有其他地方对该key有强引用,那么该缓存就会被垃圾回收器回收。

weakValues:使用弱引用存储value。如果没有其他地方对该value有强引用,那么该缓存就会被垃圾回收器回收。

softValues:使用软引用存储value。当内存满了过后,软引用的对象以将使用最近最少使用(least-recently-used ) 的方式进行垃圾回收。由于使用软引用是需要等到内存满了才进行回收,所以我们通常建议给缓存配置一个使用内存的最大值。

Java 中四种引用类型

引用类型 被垃圾回收的时间 用途 生成时间
强引用 Strong Reference 从来不会 对象的一般状态 JVM 停止运行时
软引用 soft Reference 在内存不足时 对象缓存 在内存不足时
弱引用 weak Reference 在垃圾回收时 对象缓存 gc 运行后
虚引用 Phantom Reference 从来不会 可以用虚引用来跟踪对象被垃圾回收器回收的活动,当一个虚引用关联的对象被垃圾收集器回收之前会收到一条系统通知 JVM 停止运行时

注意:AsyncLoadingCache 不支持弱引用和软引用,weakValues 和 softValues 不能同时使用

5 移除事件监听

LoadingCache<String, Object> cache = Caffeine.newBuilder()
    .initialCapacity(100)//初始大小
    .maximumSize(1000)//最大数量
    .expireAfterWrite(1800, TimeUnit.SECONDS)//过期时间
    .removalListener((key, value, cause) -> log.info("key:" + key + ", value:" + value + ", 删除原因:" + cause.toString()))
    .build(key->getDate(key));

6 写入外部存储

多级缓存的情况下,这个方法还是很实用的。

LoadingCache<String, Object> cache2 = Caffeine.newBuilder()
    .writer(new CacheWriter<String, Object>() {
        @Override public void write(String key, Object value) {
            // 写入到外部存储
        }
        @Override public void delete(String key, Object value, RemovalCause cause) {
            // 删除外部存储
        }
    })
    .build(key -> function(key));

7 统计

Cache<String, Object> cache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .recordStats()
    .build();

通过 Cache.stats() 返回一个CacheStats。CacheStats提供以下统计方法:

hitRate(): 返回缓存命中率
evictionCount(): 缓存回收数量
averageLoadPenalty(): 加载新值的平均时间

总结

Caffeine Cache 可以说是站在巨人(Guava Cache)的肩膀上发展起来的新一代缓存框架,他有着比 Guava Cache 更好的性能。 他的 api 跟 Guava 高度相似,使用过 Guava 的同学可以很顺利的切换到 Caffeine Cache。


原文:高性能 Java 缓存框架 —— Caffeine Cache | kun's blog