Caffeine(一)基本使用

1,093 阅读5分钟

Caffeine简介

缓存(Cache)是一种空间换时间的手段,通过存储已经访问过的数据,使得下次进行数据访问时起到加速的效果,可以将缓存理解为具有存储、手动移除、自动移除策略的Map。

Caffeine是一个Java高性能的本地缓存,使用Window TinyLfu回收策略,相比Guava缓存有更好的缓存命中率。Caffeine具有提供了以下功能:

  • automatic loading of entries into the cache, optionally asynchronously(自动将条目加载到缓存中,也可选异步加载数据)
  • size-based eviction when a maximum is exceeded based on frequency and recency(基于频率和最近访问,可将基于数量设为移除策略)
  • time-based expiration of entries, measured since last access or last write(根据最近访问和修改时间,可将基于时间设为移除策略)
  • asynchronously refresh when the first stale request for an entry occurs(支持过期条目异步刷新)
  • keys automatically wrapped in weak references(key被包装成弱引用)
  • values automatically wrapped in weak or soft references(value被包装成弱引用或者软引用)
  • writes propagated to an external resource(缓存数据写入外部资源)
  • notification of evicted (or otherwise removed) entries(数据移除提醒)
  • accumulation of cache access statistics(累计缓存使用统计)

Caffeine内部结构有三部分:

  1. Cache部分:Cache内部包含着一个ConcurrentHashMap,是存放数据地地方,而ConcurrentHashMap是一个并发安全的容器,Caffeine可以看成一个被强化的ConcurrentHashMap
  2. Scheduler部分:定期清空数据的一个机制
  3. Executor部分:指定运行异步任务时要使用线程池

Caffeine使用

一. 缓存加载类型

1. 同步手动加载

  • 该方式不需要指定加载方式,需要手动使用put()方法加载数据。
  • 使用invalidate()方法,可以手动移除数据
  • 使用getIfPresent()方法,如果key的值存在,则获取缓存中的值;如果不存在,则返回NULL
  • 使用get(key, k->value),如果key的值存在,则获取缓存中的值;如果不存在,则将value值存入缓存,并返回value
  • 多线程情况下,使用get(key, k->value)时,如果有另一个线程同时调用本方法竞争,则后一线程会被阻塞,直到前一个线程更新缓存完成;而若另一线程调用getIfPresent()方法,则会立即返回NULL,不会阻塞
public static void main(String[] args){
    Cache<String, String> cache = Caffeine.newBuilder().build();
    cache.put("1", "1");
    System.out.println(cache.getIfPresent("1")); // 1
    cache.invalidate("1");
    System.out.println(cache.get("1", k -> "一")); // 一
    System.out.println(cache.get("2", k -> "2")); // 2
    System.out.println(cache.getIfPresent("2")); // 2
}

2. 同步自动加载

  • LoadingCache是一种自动加载得缓存,使用该缓存需要指定CacheLoader,并且实现其中得load()方法
  • 若调用get()方法,缓存不存在时,则会自动调用CacheLoader.load()方法加载最新值。
  • 若调用getIfPresent()方法,缓存不存在时,直接返回null
  • 多线程情况下,当两个线程同时调用get(),则后一线程将被阻塞,直至前一线程更新缓存完成
public static void main(String[] args){
    LoadingCache<String, String> cache = Caffeine.newBuilder().build(new CacheLoader<String, String>() {
        @Override
        public @Nullable String load(@NonNull String key) throws Exception {
            return key+"_value";
        }
    });

    System.out.println(cache.getIfPresent("1")); // null
    System.out.println(cache.get("2")); //2_value
}

3. 异步手动加载

  • AsynCache的响应结果为CompletableFuture,可以通过get()获取结果
  • 多线程情况下,当两个线程同时调用get(key, k -> value),则会返回同一个CompletableFuture对象。由于返回结果本身不进行阻塞,可以根据业务设计自行选择阻塞等待或者非阻塞。
public static void main(String[] args) throws Exception{
    AsyncCache<String, String> cache = Caffeine.newBuilder().buildAsync();

    CompletableFuture<String> completableFuture = cache.get("1", k -> "1");
    System.out.println(completableFuture.get());
}

4. 异步自动加载

  • AsyncLoadingCache是Loading Cache和Async Cache的功能组合,其支持异步的方式对缓存进行自动加载
public static void main(String[] args) throws Exception{
    AsyncLoadingCache<String, String> cache = Caffeine.newBuilder()
            .buildAsync(new CacheLoader<String, String>() {
                @Override
                public String load(@NonNull String key) throws Exception {
                    return key+"_value";
                }
            });
    
    CompletableFuture<String> completableFuture = cache.get("1");
    System.out.println(completableFuture.get());
}

二. 驱逐数据

1. 驱逐策略

淘汰数据是根据驱逐策略来执行的,淘汰数据的过程是一个异步的过程。

驱逐策略主要是根据缓存数据的大小,数量以及储存的时间等判断缓存的数据是否要被移出缓存,几种常见的驱逐策略有:

  • 基于缓存数量的驱逐策略
// 当缓存的数据数量达到阈值时,淘汰数据
Cache<String, String> numCache = Caffeine.newBuilder().maximumSize(1000).build();
  • 基于缓存大小的驱逐策略
// 当缓存的数据大小达到阈值时,淘汰数据
Cache<String, String> weightCache = Caffeine.newBuilder().maximumWeight(1000).build();
  • 基于访问时间的驱逐策略
// 在最后一次访问或者写入后开始计时,在指定的时间后过期淘汰数据
Cache<String, String> afterWriteCache = Caffeine.newBuilder()
        .expireAfterWrite(1000, TimeUnit.MILLISECONDS).build();
  • 基于修改时间的驱逐策略
// 在最后一次写入缓存后开始计时,在指定的时间后过期淘汰数据
Cache<String, String> afterAccessCache = Caffeine.newBuilder()
        .expireAfterAccess(1000, TimeUnit.MILLISECONDS).build();

2. 移除监听

removalListener()可以监听数据驱逐事件,可以在该监听事件中输出数据淘汰的原因等等。

public static void main(String[] args) throws Exception{
    Cache<String, String> cache = Caffeine.newBuilder()
            .removalListener((String key, Object value, RemovalCause cause) ->
                    System.out.printf("Key %s was removed (%s)%n", key, cause))
            .maximumSize(1)
            .build();

    System.out.println(cache.get("1", k -> "1"));
    System.out.println(cache.get("2", k -> "2"));
    Thread.sleep(3000);
    
    // 1
    // 2
    // Key 1 was removed (SIZE)
}

3. 刷新机制

刷新机制可以在数据写入或者访问之后的某一段时间内异步刷新数据。使用refreshAfterWrite(),在key达到刷新时间之后的首次访问时,立即返回旧值,同时异步地对缓存值进行刷新,这使得调用方不至于因为缓存驱逐而被阻塞。

public static void main(String[] args) throws Exception{
    LoadingCache<String, String> cache = Caffeine.newBuilder()
            .refreshAfterWrite(1000, TimeUnit.MILLISECONDS)
            .maximumSize(1)
            .build(new CacheLoader<String, String>() {
                @Override
                public @Nullable String load(@NonNull String key) throws Exception {
                    long time = System.currentTimeMillis();
                    System.out.println(time);
                    return key + "_" + time;
                }
            });
    System.out.println(cache.get("1"));
    Thread.sleep(3000);
    System.out.println(cache.get("1"));
    Thread.sleep(3000);
    System.out.println(cache.get("1"));
    Thread.sleep(3000);
}
1649482046362
1_1649482046362
1649482049393
1_1649482046362
1649482052401
1_1649482049393

三. 统计

Caffeine可以使用recordStats()方法打开数据收集功能,并且Cache.stats()返回一个CacheStats,而CacheStats提供了以下统计方法:

  • hitRate(): 返回缓存命中率
  • evictionCount(): 缓存回收数量
  • averageLoadPenalty(): 加载新值的平均时间
public static void main(String[] args) throws Exception{
    Cache<String, String> cache = Caffeine.newBuilder()
            .maximumSize(2)
            .recordStats()
            .build();

    cache.put("1", "1");
    cache.put("2", "2");
    cache.getIfPresent("1");
    cache.get("3", k -> "3");
    cache.get("4", k -> "4");
    cache.getIfPresent("3");
    Thread.sleep(1000);
    CacheStats cacheStats = cache.stats();
    System.out.println(cacheStats.hitRate());
    System.out.println(cacheStats.hitCount());
    System.out.println(cacheStats.evictionCount());
    System.out.println(cacheStats.averageLoadPenalty());
}
0.5
2
2
2
6700.0