高性能java缓存库-Caffeine

Caffeine简介

Caffeine基于java8的高性能,接近最优的缓存库。Caffeine提供的内存缓存使用参考Google guava的API。Caffeine是基于Google guava和 ConcurrentLinkedHashMap的设计经验上改进的成果。

Caffeine可以通过建造者模式灵活的组合以下特性:

  1. 通过异步自动加载实体到缓存中
  2. 基于大小的回收策略
  3. 基于时间的回收策略
  4. 自动刷新
  5. key自动封装虚引用
  6. value自动封装弱引用或软引用
  7. 实体过期或被删除的通知
  8. 写入外部资源
  9. 统计累计访问缓存

加载策略

Caffeine提供了3种加载策略:手动加载,同步加载,异步加载

手动加载

1
2
3
4
5
6
7
8
9
10
11
12
Cache<Key, Graph> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(10_000)
.build();
// 检索一个entry,如果没有则为null
Graph graph = cache.getIfPresent(key);
// 检索一个entry,如果entry为null,则通过key创建一个entry并加入缓存
graph = cache.get(key, k -> createExpensiveGraph(key));
// 插入或更新一个实体
cache.put(key, graph);
// 移除一个实体
cache.invalidate(key);

同步加载

构造Cache时候,build方法传入一个CacheLoader实现类。实现load方法,通过key加载value。

1
2
3
4
5
6
7
8
LoadingCache<Key, Graph> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(key -> createExpensiveGraph(key));
//如果缓存种没有对应的value,通过createExpensiveGraph方法加载
Graph graph = cache.get(key);

Map<Key, Graph> graphs = cache.getAll(keys);

异步加载

1
2
3
4
5
6
AsyncLoadingCache<Key, Graph> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.buildAsync((key, executor) -> createExpensiveGraphAsync(key, executor));
CompletableFuture<Graph> graph = cache.get(key);
CompletableFuture<Map<Key, Graph>> graphs = cache.getAll(keys);

AsyncLoadingCache 是 LoadingCache 的变体, 可以异步计算实体在一个线程池(Executor)上并且返回 CompletableFuture.

回收策略

Caffeine提供了3种回收策略:基于大小回收,基于时间回收,基于引用回收

基于大小回收

1
2
3
4
5
6
7
8
9
10
// 基于实体数量淘汰实体
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.maximumSize(10_000)
.build(key -> createExpensiveGraph(key));

// 通过权重来计算,每个实体都有不同的权重,总权重到达最高时淘汰实体。
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.maximumWeight(10_000)
.weigher((Key key, Graph graph) -> graph.vertices().size())
.build(key -> createExpensiveGraph(key));

到达最大大小时淘汰最近最少使用的实体

基于时间回收

  1. 实体被访问之后,在实体被读或被写后的一段时间后过期

    1
    2
    3
    LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .expireAfterAccess(5, TimeUnit.MINUTES)
    .build(key -> createExpensiveGraph(key));
  2. 基于写之后,在实体被写入后的一段时间后过期

    1
    2
    3
    LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .build(key -> createExpensiveGraph(key));
  3. 自定义策略Expiry,可以自定义在实体被读,被更新,被创建后的时间过期。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .expireAfter(new Expiry<Key, Graph>() {
    public long expireAfterCreate(Key key, Graph graph, long currentTime) {
    // Use wall clock time, rather than nanotime, if from an external resource
    long seconds = graph.creationDate().plusHours(5)
    .minus(System.currentTimeMillis(), MILLIS)
    .toEpochSecond();
    return TimeUnit.SECONDS.toNanos(seconds);
    }
    public long expireAfterUpdate(Key key, Graph graph,
    long currentTime, long currentDuration) {
    return currentDuration;
    }
    public long expireAfterRead(Key key, Graph graph,
    long currentTime, long currentDuration) {
    return currentDuration;
    }
    })
    .build(key -> createExpensiveGraph(key));

基于引用回收

java种有四种引用:强引用,软引用,弱引用和虚引用,caffeine可以将值封装成弱引用或软引用。
软引用:如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。
弱引用:弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存

1
2
3
4
5
6
7
8
9
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.weakKeys()
.weakValues()
.build(key -> createExpensiveGraph(key));


LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.softValues()
.build(key -> createExpensiveGraph(key));

自动刷新

1
2
3
4
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.maximumSize(10_000)
.refreshAfterWrite(1, TimeUnit.MINUTES)
.build(key -> createExpensiveGraph(key));

在写后的持续时间过后,调用createExpensiveGraph刷新

移除通知

1
2
3
4
Cache<Key, Graph> graphs = Caffeine.newBuilder()
.removalListener((Key key, Graph graph, RemovalCause cause) ->
System.out.printf("Key %s was removed (%s)%n", key, cause))
.build();

通过removalListener添加实体移除监听器

写到外部存储

通过CacheWriter 可以将缓存回写的外部存储中。

1
2
3
4
5
6
7
8
9
10
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.writer(new CacheWriter<Key, Graph>() {
@Override public void write(Key key, Graph graph) {
// 写入到外部存储或二级缓存
}
@Override public void delete(Key key, Graph graph, RemovalCause cause) {
// 删除外部存储或者二级缓存
}
})
.build(key -> createExpensiveGraph(key));

使用场景

  1. 缓存同步数据库
  2. 多级缓存同步

注意,CacheWriter不能与弱键或AsyncLoadingCache一起使用

统计缓存使用情况

1
2
3
4
Cache<Key, Graph> graphs = Caffeine.newBuilder()
.maximumSize(10_000)
.recordStats()
.build();

通过使用Caffeine.recordStats(), 可以转化成一个统计的集合. 通过 Cache.stats() 返回一个CacheStats。CacheStats提供以下统计方法

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