开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情
前言
最近听说有一个新的缓存工具Caffeine,看起来和Guava缓存差不多,先来看看它是怎么使用的吧。
创建Cache
因为Caffeine缓存的很多操作需要使用到线程池,所以我们先创建一个自定义的线程池供Caffeine使用。如果不给自定义线程池的话,Caffeine会使用forkjoin线程池,为了维护和调试方便,建议还是使用自定义的线程池:
/**
* 自定义线程池
*/
private static final Executor CAFFEINE_EXECUTOR = new ThreadPoolExecutor(5, 10, 600, TimeUnit.SECONDS, newLinkedBlockingQueue<>(100), new ThreadFactory() {
private final AtomicInteger atomicInteger = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, "custom_caffeine_cache_handler_thread" + atomicInteger.incrementAndGet());
t.setDaemon(true);
return t;
}
});
Caffeine Cache总共有四种创建方式:
-
简单的:
private static final Cache<String, Object> SIMPLE_CACHE = Caffeine.newBuilder() // 建议使用自定义线程池,否则会用forkjoin线程池 .executor(CAFFEINE_EXECUTOR) .build(); -
异步缓存:
private static final AsyncCache<String, Object> ASYNC_CACHE = Caffeine.newBuilder() .executor(CAFFEINE_EXECUTOR) .buildAsync();异步与同步的区别就在于,异步操作的所有返回值都是
CompletableFuture,说明异步操作都是交给线程池处理的;比如getIfPresent()方法:CompletableFuture<Object> future = ASYNC_CACHE.getIfPresent("key"); -
带CacheLoader的缓存
private static final LoadingCache<String, Object> CACHE_LOADER = Caffeine.newBuilder() .executor(CAFFEINE_EXECUTOR) .build(new CacheLoader<>() { @Override public @Nullable Object load(String key) throws Exception { // 从数据源读取数据 System.out.println("从数据源读取数据"); return "value for key"; } });通过实现自定义的
CacheLoader,在调用get()方法时,如果缓存不存在或者缓存过期,那么会调用load()方法重新载入缓存;该load()方法是线程安全的,多线程竞争会保证只有一个线程调用,不会产生线程安全问题;另外需要补充的就是,我们还可以重写loadAll()方法,该方法在调用getAll()时会触发,但是大多数情况下,不建议调用getAll(); -
异步带CacheLoader的缓存
private static final AsyncLoadingCache<String, Object> ASYNC_CACHE_LOADER = Caffeine.newBuilder() .executor(CAFFEINE_EXECUTOR) .buildAsync(new CacheLoader<>() { @Override public @Nullable Object load(String key) throws Exception { // 从数据源读取数据 System.out.println("从数据源读取数据"); return "value for key"; } });唯一的不同就是创建出来的
AsyncLoadingCache对象调用的各种方法返回值都是CompletableFuture,说明该操作都是交给了线程池来做;
获取缓存
不带CacheLoader的缓存只有两种常用的获取缓存的方法,所有的getAll()方法不会介绍,并且不建议使用,它会影响你的应用程序的性能:
// 没有缓存返回null,主要要判断是否为null
Object value = SIMPLE_CACHE.getIfPresent("key");
// 如果没有缓存,会将后面function返回的结果存入缓存(允许为null),并返回
Object value = SIMPLE_CACHE.get("key", k -> "value for key");
这个方法特别有用,它可以在缓存过期时,自动从指定的数据源获取数据填充,并且能够保证线程安全,这可比自己写代码好用多了。但是我们不可能每次get()方法调用都传function进去,所以我们才需要更好用的带CacheLoader的缓存;
下面再来补充带CacheLoader的缓存:
// 找不到缓存自动调用load()载入数据
Object value = CACHE_LOADER.get("key");
带有CacheLoader的缓存多了一个get()方法,相当于有一个公共的load()方法避免了每次获取缓存都要传function。
缓存过期
我们可以使用三种方式设置缓存的过期时间:
expireAfterAccess():距离上一次访问缓存后多久过期;
expireAfterWrite():距离上一次更新缓存后多久过期;
另外我们肯定还想针对特定的key做定制化过期策略:
expireAfter(new Expiry<String, Object>() {
@Override
public long expireAfterCreate(String s, Object value, long currentTime) {
// currentTime为该缓存被创建的时间,单位:纳秒
// 返回值是有效期,单位:纳秒
System.out.println("---expireAfterCreate start----");
System.out.println(System.nanoTime());
System.out.println(currentTime);
System.out.println("---expireAfterCreate end----");
return 1234567891;
}
@Override
public long expireAfterUpdate(String s, Object value, long currentTime, @NonNegative long currentDuration) {
// currentTime为更新前的系统时间,单位:纳秒
// currentDuration到期时间与当前时间的差值Math.max(1L, variableTime - now),单位:纳秒
System.out.println("---expireAfterUpdate start----");
System.out.println(System.nanoTime());
System.out.println(currentTime);
System.out.println(currentDuration);
System.out.println("---expireAfterUpdate end----");
return 0;
}
@Override
public long expireAfterRead(String s, Object value, long currentTime, @NonNegative long currentDuration) {
// currentTime为读取前的系统时间,单位:纳秒
// currentDuration到期时间与当前时间的差值Math.max(1L, variableTime - now),单位:纳秒
System.out.println("---expireAfterRead start----");
System.out.println(System.nanoTime());
System.out.println(currentTime);
System.out.println(currentDuration);
System.out.println("---expireAfterRead end----");
return 0;
}
}
实现自定义的Expiry可以让你针对特定的key和value制定过期策略;
缓存个数设置
initialCapacity():设置缓存池的初始数量;
maximumSize():设置缓存池的最大数量;
通过以上设置,可以控制缓存池的大小,避免缓存过大导致应用程序出现OOM的情况;
缓存权重(容量)
当缓存容量达到上限时,需要按照一定的策略淘汰不常用的数据,此时就可以通过权重设置来淘汰权重比较低的数据。
weigher(new Weigher<String, Object>() {
@Override
public @NonNegative int weigh(String key, Object value) {
// 设置每一个key的权重
return 0;
}
})
weigher()方法可以帮助开发人员设置每一个key的权重,实现灵活的权重定制化;
maximumWeight():设置最大的容量;
当所有key的权重加起来超出maximumWeight值时,会触发淘汰策略;
缓存刷新
refreshAfterWrite():设置缓存在写入后多久再进行刷新;所谓的刷新是值在缓存到期后第一次被访问时,不会阻塞访问线程,而是直接返回旧值,并通过线程池实现异步载入新数据;
它与expireAfterWrite()区别在于过期后的数据被再次访问时被阻塞直至加载到新的数据,而refreshAfterWrite()是直接返回旧值;
监听缓存被移除
evictionListener():移除操作与监听器处于一个线程中,同步触发监听器;
removalListener():异步触发监听器;
removalListener((key, value, removalCause) -> {
System.out.println("ThreadName:" + Thread.currentThread().getName());
System.out.println(key + ":" + value + "被淘汰了!");
})
RemovalCause枚举参数会告知该缓存是因为什么原因被清除的:
EXPLICIT:手动调用invalidate或remove等方法;
REPLACED:调用put等方法进行修改,说明有新的缓存进来导致被挤出去了;
COLLECTED:设置了key或value的引用方式;
EXPIRED:设置了过期时间导致被清除;
SIZE:超出设置的最大容量导致被清除;
开启统计功能
recordStats():开启缓存的统计功能;
开启该功能后,可以通过以下代码查看相关统计数据:
CacheStats cacheStats = CACHE_LOADER.stats();
System.out.println("载入数据的总纳秒数: " + cacheStats.totalLoadTime());
System.out.println("平均载入数据时间 totalLoadTime / (loadSuccessCount + loadFailureCount): " + cacheStats.averageLoadPenalty());
System.out.println("缓存淘汰的数量: " + cacheStats.evictionCount());
System.out.println("缓存淘汰的数量(权重): " + cacheStats.evictionWeight());
System.out.println("缓存命中的总数: " + cacheStats.hitCount());
System.out.println("缓存命中率: " + cacheStats.hitRate());
System.out.println("载入缓存次数: " + cacheStats.loadCount());
System.out.println("载入缓存成功次数: " + cacheStats.loadSuccessCount());
System.out.println("载入缓存失败次数: " + cacheStats.loadFailureCount());
System.out.println("载入缓存失败比率: " + cacheStats.loadFailureRate());
System.out.println("没有命中缓存次数: " + cacheStats.missCount());
System.out.println("没有命中缓存的比率: " + cacheStats.missRate());
System.out.println("请求总次数: " + cacheStats.requestCount());