缓存框架Caffeine探究
- Java高性能本地缓存框架Caffeine
- 依赖
- 缓存加载
- 手动加载
- CleanUp方法
- 自动加载
- 手动异步加载
- 自动异步加载
- 过期策略
- 基于大小
- 基于时间
- 基于引用
- Caffeine.weakKeys()
- Caffeine.weakValues()
- Caffeine.softValues()
- 刷新机制
- 缓存移除
- invalidate(Object key)方法
- invalidateAll(Iterable<?> keys)方法
- invalidateAll()方法
- 移除监听器(RemovalListener)---监听元素移除事件
- Writer
- 统计
- 参考文献
- SpringCache继承Caffeine
- 添加依赖
- 添加配置
- 注解使用
- 参考文献
Java高性能本地缓存框架Caffeine
- 缓存又分进程内缓存和分布式缓存两种:分布式缓存如redis、memcached等,还有本地(进程内)缓存如ehcache、GuavaCache、Caffeine等
- Caffeine是一个基于Java8开发的提供了近乎最佳命中率的高性能的缓存库。
- Caffeine 是一个基于Java 8的高性能本地缓存框架,其结构和 Guava Cache 基本一样,api也一样,基本上很容易就能替换。 Caffeine 实际上就是在 Guava Cache 的基础上,利用了一些 Java 8 的新特性,提高了某些场景下的性能效率。
官方介绍Caffeine是基于JDK8的高性能本地缓存库,提供了几乎完美的命中率。它有点类似JDK中的ConcurrentMap,实际上,Caffeine中的LocalCache接口就是实现了JDK中的ConcurrentMap接口,但两者并不完全一样。最根本的区别就是,ConcurrentMap保存所有添加的元素,除非显示删除之(比如调用remove方法)。而本地缓存一般会配置自动剔除策略,为了保护应用程序,限制内存占用情况,防止内存溢出
Caffeine提供了灵活的构造方法,从而创建可以满足如下特性的本地缓存:
- 自动把数据加载到本地缓存中,并且可以配置异步;
- 基于数量剔除策略;
- 基于失效时间剔除策略,这个时间是从最后一次访问或者写入算起;
- 异步刷新;
- Key会被包装成Weak引用;
- Value会被包装成Weak或者Soft引用,从而能被GC掉,而不至于内存泄漏;
- 数据剔除提醒;
- 写入广播机制;
- 缓存访问可以统计;
参考文档
依赖
代码语言:javascript复制 <dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.8.4</version>
</dependency>
缓存加载
- Caffeine提供了四种缓存添加策略:手动加载,自动加载,手动异步加载和自动异步加载。
手动加载
Caffeine 有两种方式限制缓存大小。两种配置互斥,不能同时配置
- 创建一个限制容量 Cache
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.SECONDS) // 设置超时时间为5s / 写入后隔段时间过期
.maximumSize(1)// 设置缓存最大条目数,超过条目则触发回收。
.build();
需要注意的是,实际实现上为了性能考虑,这个限制并不会很死板:
- 在缓存元素个数快要达到最大限制的时候,过期策略就开始执行了,所以在达到最大容量前也许某些不太可能再次访问的 Entry (Key-Value)就被过期掉了
- 有时候因为过期 Entry 任务还没执行完,更多的 Entry 被放入缓存,导致缓存的 Entry 个数短暂超过了这个限制
示例:
代码语言:javascript复制package dhy.com;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class Main
{
public static void main(String[] args) throws InterruptedException {
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.SECONDS) // 设置超时时间为5s / 写入后隔段时间过期
.maximumSize(1)// 设置缓存最大条目数,超过条目则触发回收。
.build();
// 查找一个缓存元素, 没有查找到的时候返回null
String value = cache.getIfPresent("test");
System.out.println(value);//-->null
// 查找缓存,如果缓存不存在则生成缓存元素, 如果无法生成则返回null
value = cache.get("test", k -> "test-value");
System.out.println(cache.getIfPresent("test"));//-->test-value
System.out.println(value);//-->test-value
// 加入一些缓存数据
List<String> list = new ArrayList<>();
for (int i = 2; i < 10; i ) {
list.add("test" i);
}
for (int i = 2; i < 10; i ) {
// 添加或者更新一个缓存元素
cache.put("test" i, "test-value" i);
}
// 执行缓存回收
// 缓存的删除策略使用的是惰性删除和定时删除,但是我也可以自己调用cache.cleanUp()方法手动触发一次回收操作。cache.cleanUp()是一个同步方法。
cache.cleanUp();
//根据key list去获取一个map的缓存
Map<String, String> dataObjectMap
= cache.getAllPresent(list);
//查看缓存中的数据
System.out.println(dataObjectMap.size()); //--> 1
System.out.println(dataObjectMap); //--> {test9=test-value9}
Thread.sleep(5000); //设置10s的睡眠时间,使得超过过期时间
System.out.println(cache.getIfPresent("test"));//-->null
}
}
- 创建一个自定义权重限制容量的 Cache
package dhy.com;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.Weigher;
import org.checkerframework.checker.index.qual.NonNegative;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.List;
public class Main
{
public static void main(String[] args) throws InterruptedException
{
Cache<String, List<Object>> stringListCache = Caffeine.newBuilder()
//最大weight值,当所有entry的weight和快达到这个限制的时候会发生缓存过期,剔除一些缓存
.maximumWeight(1)
//每个 Entry 的 weight 值
.weigher(new Weigher<String, List<Object>>() {
@Override
public @NonNegative int weigh(@NonNull String key, @NonNull List<Object> value) {
return value.size();
}
})
.build();
}
}
上面我们的 value 是一个 list,以 list 的大小作为 Entry 的大小。当把 Weigher 实现为只返回1,maximumWeight 其实和 maximumSize 是等效的。 同样的,为了性能考虑,这个限制也不会很死板
示例:
代码语言:javascript复制package dhy.com;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.Weigher;
import org.checkerframework.checker.index.qual.NonNegative;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class Main
{
public static void main(String[] args) throws InterruptedException
{
Cache<String, List<Object>> stringListCache = Caffeine.newBuilder()
//最大weight值,当所有entry的weight和快达到这个限制的时候会发生缓存过期,剔除一些缓存
.maximumWeight(1)
//每个 Entry 的 weight 值
.weigher(new Weigher<String, List<Object>>() {
@Override
public @NonNegative int weigh(@NonNull String key, @NonNull List<Object> value) {
return value.size();
}
})
.build();
}
public void testManualLoadCache4()
{
Cache<String, List<Object>> stringListCache = Caffeine.newBuilder()
//最大weight值,当所有entry的weight和快达到这个限制的时候会发生缓存过期,剔除一些缓存
.maximumWeight(1)
//每个 Entry 的 weight 值
.weigher(new Weigher<String, List<Object>>() {
@Override
public @NonNegative int weigh(@NonNull String key, @NonNull List<Object> value) {
return value.size();
}
})
.build();
stringListCache.put("test1", Collections.singletonList("test-value1"));
stringListCache.put("test2", Arrays.asList("test-value2","test-value2"));
stringListCache.cleanUp();
Map<String, List<Object>> dataObjectMap = stringListCache.getAllPresent(Arrays.asList("test1","test2"));
System.out.println(dataObjectMap.size()); // --> 1
System.out.println(dataObjectMap); // --> {test1=[test-value1]}
}
}
- 指定初始化大小
Cache<String, Object> cache = Caffeine.newBuilder()
//指定初始大小
.initialCapacity(1000)
.build();
和HashMap类似,通过指定一个初始大小,减少扩容带来的性能损耗。这个值也不宜过大,浪费内存。
CleanUp方法
默认情况下,在一个值失效之后,Caffeine没有自动或者立刻执行清理、驱逐值。相反,它在写操作之后或者读操作(写操作很少的情况)之后执行少量的维护工作。如果你的缓存是高吞吐量的,然后你不需要担心执行清理失效键值对的缓存清理工作;如果你的缓存很少读写,你可能希望有一个额外的线程去做这个清理的工作,那么可以调用Cache.cleanUp()方法。
CleanUp
一个Scheduler可能被用来开启对失效键值对的迅速删除。调度中使用批次来减少执行次数。调度会尽最大努力,但是不确保一个键值对失效时被删除。对于JAVA9 ,使用Scheduler.systemScheduler(),提供一个专门的、系统范围的调度线程。
您可以提供一个调度器用来快速删除过期条目。通过调度,会批量执行过期事件,以在短时间内最大程度的减少执行次数。调度会尽力删除过期条目,但不能对何时删除做任何保证。Java 9 的用户可能更喜欢使用Scheduler.systemScheduler()来利用专用的系统范围的调度线程。
在java9 中,可以使用Cleaner开启对基于引用键值对(weakKeys、weakValues、softValues)的迅速删除。简单使用Cleaner注册key或者value,然后调用一个Runnable,比如可以设置为Cache.cleanUp(),从而触发维护。
在Java 9 中,可以使用Cleaner来快速删除基于引用的条目(如果使用了弱引用key,弱引用value或软引用value)。只需要将key或value注册到Cleaner,其处理逻辑会调用Cache.cleanUp触发维护程序。
自动加载
- 创建LoadingCache
示例:
代码语言:javascript复制 LoadingCache<String, String> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(new CacheLoader<String, String>() {
@Override
public @Nullable String load(@NonNull String key) throws Exception {
//默认的数据加载实现,当调用get取值的时候,如果key没有对应的值,就调用这个方法进行加载
System.out.println("load data --- " key);
//模拟从数据库中获取数据
return MAP.get(key);
}
});
System.out.println(cache.get("test1")); //第一次的时候会调用load方法
System.out.println(cache.get("test1")); //第二次不会调用load方法
手动异步加载
创建AsyncCache
示例:
代码语言:javascript复制 AsyncCache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(10_000)
.buildAsync();
// 查找缓存元素,如果不存在,则异步生成
CompletableFuture<String> value = cache.get("test1", k -> {
//异步加载
System.out.println(Thread.currentThread().getName()); // ForkJoinPool.commonPool-worker-3
System.out.println("load cache ---" k);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return MAP.get(k);
});
System.out.println(Thread.currentThread().getName()); //main
System.out.println("=========");
System.out.println(value.get()); //value1, 阻塞
测试结果:
代码语言:javascript复制ForkJoinPool.commonPool-worker-3
load cache ---test1
main
=========
value1
自动异步加载
- 创建AsyncLoadingCache
示例1:
代码语言:javascript复制 AsyncLoadingCache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(10_000)
//异步的封装一段同步操作来生成缓存元素
.buildAsync(new CacheLoader<String, String>() {
@Override
public @Nullable String load(@NonNull String key) throws Exception {
System.out.println(Thread.currentThread().getName()); // ForkJoinPool.commonPool-worker-3
System.out.println("load cache ---" key);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return MAP.get(key);
}
});
CompletableFuture<String> value = cache.get("test1");
System.out.println(Thread.currentThread().getName()); //main
System.out.println("=========");
System.out.println(value.get()); //value1 阻塞
测试结果
代码语言:javascript复制ForkJoinPool.commonPool-worker-3
load cache ---test1
main
=========
value1
示例2:
代码语言:javascript复制 AsyncLoadingCache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(10_000)
//构建一个异步缓存元素操作并返回一个future
.buildAsync(new AsyncCacheLoader<String, String>() {
@Override
public @NonNull CompletableFuture<String> asyncLoad(@NonNull String key, @NonNull Executor executor) {
System.out.println(Thread.currentThread().getName()); //main
return CompletableFuture.supplyAsync(() -> {
System.out.println("load cache");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()); // ForkJoinPool.commonPool-worker-3
return MAP.get(key);
});
}
});
测试结果:
代码语言:javascript复制main
load cache
ForkJoinPool.commonPool-worker-3
value1
过期策略
本地缓存的过期机制是非常重要的,因为本地缓存中的数据并不像业务数据那样需要保证不丢失。本地缓存的数据一般都会要求保证命中率的前提下,尽可能的占用更少的内存,并可在极端情况下,可以被GC掉。
Caffeine的过期机制都是在构造Cache的时候申明,主要有如下几种:
- expireAfterWrite:表示自从最后一次写入后多久就会过期;
- expireAfterAccess:表示自从最后一次访问(写入或者读取)后多久就会过期;
- expireAfter:自定义过期策略
基于大小
基于大小的我们前面已经讲到了。也就是通过设置maximumSize
来进行大小驱逐策略,还有设置maximumWeight
来设置权重驱逐策略
示例:
代码语言:javascript复制 @Test
public void testManualLoadCache6() {
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(1)
.build();
cache.put("key1","value1");
cache.put("key2","value2");
System.out.println(cache.getIfPresent("key1"));
System.out.println(cache.getIfPresent("key2"));
cache.cleanUp();
System.out.println(cache.getIfPresent("key1"));
System.out.println(cache.getIfPresent("key2"));
}
基于时间
Caffeine提供了三种定时驱逐策略
- expireAfterWrite(long, TimeUnit)
在最后一次写入缓存后开始计时
,在指定的时间后过期。
示例:
代码语言:javascript复制 @DisplayName("基于时间的过期策略,设置expireAfterWrite")
@Test
public void testManualLoadCache7() throws InterruptedException {
//在最后一次写入缓存后开始计时,在指定的时间后过期。
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(3,TimeUnit.SECONDS)
.build();
cache.put("key1","value1");
Thread.sleep(1000);
System.out.println(cache.getIfPresent("key1")); // -> value1
Thread.sleep(1000);
System.out.println(cache.getIfPresent("key1")); // -> value1
Thread.sleep(1000);
System.out.println(cache.getIfPresent("key1")); // -> null
}
- expireAfterAccess(long, TimeUnit)
在最后一次读或者写入后开始计时
,在指定的时间后过期。假如一直有请求访问该key,那么这个缓存将一直不会过期
示例:
代码语言:javascript复制 // 在最后一次读或者写入后开始计时,在指定的时间后过期。假如一直有请求访问该key,那么这个缓存将一直不会过期。
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfterAccess(3,TimeUnit.SECONDS)
.build();
cache.put("key1","value1");
Thread.sleep(1000);
System.out.println(cache.getIfPresent("key1")); // -> value1
Thread.sleep(1000);
System.out.println(cache.getIfPresent("key1")); // -> value1
Thread.sleep(1000);
System.out.println(cache.getIfPresent("key1")); // -> value1
Thread.sleep(3001);
System.out.println(cache.getIfPresent("key1")); // -> null
- expireAfter(Expiry)
在expireAfter中需要自己实现Expiry接口,这个接口支持expireAfterCreate,expireAfterUpdate,以及expireAfterRead了之后多久过期。注意这个是和expireAfterAccess、expireAfterAccess是互斥的。这里和expireAfterAccess、expireAfterAccess不同的是,需要你告诉缓存框架,他应该在具体的某个时间过期,获取具体的过期时间。
示例:
代码语言:javascript复制 @DisplayName("基于时间的过期策略,设置expireAfterCreate")
@Test
public void testManualLoadCache9() throws InterruptedException {
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfter(new Expiry<String, String>() {
@Override
public long expireAfterCreate(@NonNull String key, @NonNull String value, long currentTime) {
return TimeUnit.SECONDS.toNanos(3);
}
@Override
public long expireAfterUpdate(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) {
return currentDuration;
}
@Override
public long expireAfterRead(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) {
return currentDuration;
}
})
.build();
cache.put("key1", "value1");
Thread.sleep(1000);
System.out.println(cache.getIfPresent("key1")); // -> value1
Thread.sleep(1000);
System.out.println(cache.getIfPresent("key1")); // -> value1
Thread.sleep(1000);
System.out.println(cache.getIfPresent("key1")); // -> null
}
@DisplayName("基于时间的过期策略,设置expireAfterUpdate")
@Test
public void testManualLoadCache10() throws InterruptedException {
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfter(new Expiry<String, String>() {
@Override
public long expireAfterCreate(@NonNull String key, @NonNull String value, long currentTime) {
return Long.MAX_VALUE;
}
@Override
public long expireAfterUpdate(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) {
return TimeUnit.SECONDS.toNanos(3);
}
@Override
public long expireAfterRead(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) {
return currentDuration;
}
})
.build();
cache.put("key1", "value1");
cache.put("key1", "value2");
Thread.sleep(1000);
System.out.println(cache.getIfPresent("key1")); // -> value2
Thread.sleep(1000);
System.out.println(cache.getIfPresent("key1")); // -> value2
Thread.sleep(1000);
System.out.println(cache.getIfPresent("key1")); // -> null
}
@DisplayName("基于时间的过期策略,设置expireAfterRead")
@Test
public void testManualLoadCache11() throws InterruptedException {
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfter(new Expiry<String, String>() {
@Override
public long expireAfterCreate(@NonNull String key, @NonNull String value, long currentTime) {
return Long.MAX_VALUE;
}
@Override
public long expireAfterUpdate(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) {
return currentDuration;
}
@Override
public long expireAfterRead(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) {
return TimeUnit.SECONDS.toNanos(3);
}
})
.build();
cache.put("key1", "value1");
Thread.sleep(1000);
System.out.println(cache.getIfPresent("key1")); // -> value1
Thread.sleep(1000);
System.out.println(cache.getIfPresent("key1")); // -> value1
Thread.sleep(1000);
System.out.println(cache.getIfPresent("key1")); // -> value1
Thread.sleep(3001);
System.out.println(cache.getIfPresent("key1")); // -> null
}
基于引用
Caffeine 允许你配置你的缓存去让GC去帮助清理缓存当中的元素,其中key支持弱引用,而value则支持弱引用和软引用。记住 AsyncCache不支持软引用和弱引用。
Caffeine.weakKeys()
Caffeine.weakKeys() 在保存key的时候将会进行弱引用。这允许在GC的过程中,当key没有被任何强引用指向的时候去将缓存元素回收。由于GC只依赖于引用相等性。这导致在这个情况下,缓存将会通过引用相等(==)而不是对象相等 equals()去进行key之间的比较
示例:
代码语言:javascript复制/**
* Caffeine.weakKeys() 在保存key的时候将会进行弱引用。
* 这允许在GC的过程中,当key没有被任何强引用指向的时候去将缓存元素回收。
* 由于GC只依赖于引用相等性。这导致在这个情况下,缓存将会通过引用相等(==)而不是对象相等 equals()去进行key之间的比较。
*/
@Test
public void testWeakKeys() {
Cache<String, String> cache = Caffeine.newBuilder()
.weakKeys()
.build();
cache.put(new String("test"), "value1");
System.out.println(cache.asMap());//{test=value1}
System.gc();
System.out.println(cache.asMap()); //value1
}
Caffeine.weakValues()
Caffeine.weakValues()在保存value的时候将会使用弱引用。这允许在GC的过程中,当value没有被任何强引用指向的时候去将缓存元素回收。由于GC只依赖于引用相等性。这导致在这个情况下,缓存将会通过引用相等(==)而不是对象相等 equals()去进行value之间的比较。
示例:
代码语言:javascript复制/**
* Caffeine.weakValues()在保存value的时候将会使用弱引用。
* 这允许在GC的过程中,当value没有被任何强引用指向的时候去将缓存元素回收。
* 由于GC只依赖于引用相等性。这导致在这个情况下,缓存将会通过引用相等(==)而不是对象相等 equals()去进行value之间的比较。
*/
@Test
public void testWeakValues() {
Cache<String, String> cache = Caffeine.newBuilder()
.weakKeys()
.weakValues()
.build();
cache.put("test1", new String("value"));
System.out.println(cache.asMap());//{test1=value}
System.out.println(cache.getIfPresent("test1")); //value
System.gc();
System.out.println(cache.getIfPresent("test1")); //null
System.out.println(cache.asMap());//{}
}
Caffeine.softValues()
Caffeine.softValues()在保存value的时候将会使用软引用。为了相应内存的需要,在GC过程中被软引用的对象将会被通过LRU算法回收。由于使用软引用可能会影响整体性能,我们还是建议通过使用基于缓存容量的驱逐策略代替软引用的使用。同样的,使用 softValues() 将会通过引用相等(==)而不是对象相等 equals()去进行value之间的比较。
示例:
代码语言:javascript复制@Test
public void testWithoutSoftValues() {
Cache<String, byte[]> cache = Caffeine.newBuilder()
.build();
cache.put("test1", new byte[1024 * 1024 * 1024]);
cache.put("test2", new byte[1024 * 1024 * 1024]);
cache.put("test3", new byte[1024 * 1024 * 1024]);
cache.put("test4", new byte[1024 * 1024 * 1024]);
//Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
System.out.println(cache.asMap());
}
代码语言:javascript复制 @Test
public void testSoftValues() {
Cache<String, byte[]> cache = Caffeine.newBuilder()
.softValues()
.build();
cache.put("test1", new byte[1024 * 1024 * 1024]);
cache.put("test2", new byte[1024 * 1024 * 1024]);
cache.put("test3", new byte[1024 * 1024 * 1024]);
cache.put("test4", new byte[1024 * 1024 * 1024]);
System.out.println(cache.asMap());//{test4=[B@5bf0d49}
}
如果不使用softValues的话,程序会报OutOfMemoryError,如果使用了softValues则会回收掉缓存
刷新机制
在构造Cache时通过refreshAfterWrite方法指定刷新周期,例如refreshAfterWrite(10, TimeUnit.SECONDS)表示10秒钟刷新一次:
代码语言:javascript复制.build(new CacheLoader<String, String>() {
@Override
public String load(String k) {
// 这里我们就可以从数据库或者其他地方查询最新的数据
return getValue(k);
}
});
需要注意的是,Caffeine的刷新机制是「被动」的。举个例子,假如我们申明了10秒刷新一次。我们在时间T访问并获取到值v1,在T 5秒的时候,数据库中这个值已经更新为v2。但是在T 12秒,即已经过了10秒我们通过Caffeine从本地缓存中获取到的「还是v1」
,并不是v2。在这个获取过程中,Caffeine发现时间已经过了10秒,然后会将v2加载到本地缓存中,下一次获取时才能拿到v2。即它的实现原理是在get方法中,调用afterRead的时候,调用refreshIfNeeded方法判断是否需要刷新数据。这就意味着,如果不读取本地缓存中的数据的话,无论刷新时间间隔是多少,本地缓存中的数据永远是旧的数据!
缓存移除
在构造Cache时可以通过removalListener方法申明剔除监听器,从而可以跟踪本地缓存中被剔除的数据历史信息。根据RemovalCause.java枚举值可知,剔除策略有如下5种:
- 「EXPLICIT」:调用方法(例如:cache.invalidate(key)、cache.invalidateAll)显示剔除数据;
- 「REPLACED」:不是真正被剔除,而是用户调用一些方法(例如:put(),putAll()等)改了之前的值;
- 「COLLECTED」:表示缓存中的Key或者Value被垃圾回收掉了;
- 「EXPIRED」: expireAfterWrite/expireAfterAccess约定时间内没有任何访问导致被剔除;
- 「SIZE」:超过maximumSize限制的元素个数被剔除的原因;
invalidate(Object key)方法
示例:
代码语言:javascript复制@DisplayName("测试移除cache,invalidate(key)方法")
@Test
public void testRemoveCache() {
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(50, TimeUnit.SECONDS)
.maximumSize(100)
.build();
cache.put("test1","value1");
cache.put("test2","value2");
cache.put("test3","value3");
cache.put("test4","value4");
System.out.println(cache.asMap()); //{test1=value1, test4=value4, test2=value2, test3=value3}
cache.invalidate("test1"); //移除指定key的Entry
System.out.println(cache.asMap()); //{test4=value4, test2=value2, test3=value3}
}
invalidateAll(Iterable<?> keys)方法
示例:
代码语言:javascript复制 @DisplayName("测试移除cache,invalidateAll(keys)方法")
@Test
public void testRemoveCache3() {
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(50, TimeUnit.SECONDS)
.maximumSize(100)
.build();
cache.put("test1","value1");
cache.put("test2","value2");
cache.put("test3","value3");
cache.put("test4","value4");
System.out.println(cache.asMap()); //{test1=value1, test4=value4, test2=value2, test3=value3}
cache.invalidateAll(Arrays.asList("test1","test2")); //批量移除指定key的Entry
System.out.println(cache.asMap()); //{test4=value4, test3=value3}
}
invalidateAll()方法
示例:
代码语言:javascript复制@DisplayName("测试移除cache,invalidateAll()方法")
@Test
public void testRemoveCache2() {
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(50, TimeUnit.SECONDS)
.maximumSize(100)
.build();
cache.put("test1","value1");
cache.put("test2","value2");
cache.put("test3","value3");
cache.put("test4","value4");
System.out.println(cache.asMap()); //{test1=value1, test4=value4, test2=value2, test3=value3}
cache.invalidateAll(); //移除所有的cache
System.out.println(cache.asMap()); //{}
}
移除监听器(RemovalListener)—监听元素移除事件
示例:
代码语言:javascript复制 public void testRemovalListener() {
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(50, TimeUnit.SECONDS)
.maximumSize(100)
.removalListener((RemovalListener<String, String>)
(key, value, cause) ->
System.out.println(Thread.currentThread().getName()
"--"
MessageFormat.format
("key:[{0}],value:[{1}],cause:[{2}]",key,value,cause)))
.build();
cache.put("test1", "value1");
cache.put("test2", "value2");
cache.put("test3", "value3");
cache.put("test4", "value4");
System.out.println(cache.asMap()); //{test1=value1, test4=value4, test2=value2, test3=value3}
cache.invalidate("test1"); //移除指定key的Entry
System.out.println(cache.asMap()); //{test4=value4, test2=value2, test3=value3}
//removalListener打印:ForkJoinPool.commonPool-worker-3--key:[test1],value:[value1],cause:[EXPLICIT]
}
public void testRemovalListener2() {
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(50, TimeUnit.SECONDS)
.maximumSize(1)
.removalListener((RemovalListener<String, String>) (key, value, cause) -> System.out.println(MessageFormat.format("key:[{0}],value:[{1}],cause:[{2}]",key,value,cause)))
.build();
cache.put("test1", "value1");
cache.put("test2", "value2");
System.out.println(cache.asMap()); //{test1=value1, test4=value4, test2=value2, test3=value3}
cache.cleanUp();
System.out.println(cache.asMap()); //{test4=value4, test2=value2, test3=value3}
//removalListener打印:key:[test1],value:[value1],cause:[SIZE]
}
public void testRemovalListener3() throws InterruptedException {
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.SECONDS)
.maximumSize(10)
.removalListener(
(RemovalListener<String, String>) (key, value, cause)
-> System.out.println(MessageFormat.format("key:[{0}],value:[{1}],cause:[{2}]",key,value,cause)))
.build();
cache.put("test1", "value1");
System.out.println(cache.asMap()); //{test1=value1}
Thread.sleep(1000);
cache.cleanUp();
System.out.println(cache.asMap()); //{}
//removalListener打印:key:[test1],value:[value1],cause:[EXPIRED]
}
Writer
我们还可以通过设置 Writer,将对于缓存的更新,作用于其他存储,例如数据库。
示例:
代码语言:javascript复制 public void testWriter() {
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(50, TimeUnit.SECONDS)
.maximumSize(100)
.writer(new CacheWriter<String, String>() {
@Override
public void write(@NonNull String key, @NonNull String value) {
// 持久化或者次级缓存
System.out.println(MessageFormat.format("key:[{0}],value:[{1}]", key, value));
}
@Override
public void delete(@NonNull String key, @Nullable String value, @NonNull RemovalCause cause) {
// 从持久化或者次级缓存中删除
System.out.println(MessageFormat.format("key:[{0}],value:[{1}],cause:[{2}]", key, value, cause));
}
})
.build();
cache.put("test1", "value1");
cache.put("test2", "value2");
System.out.println("===========");
System.out.println(cache.asMap());
cache.invalidate("test1");
System.out.println(cache.asMap());
cache.put("test2", "value222");
/**
* 打印结果:
* key:[test1],value:[value1]
* key:[test2],value:[value2]
* ===========
* {test1=value1, test2=value2}
* key:[test1],value:[value1],cause:[EXPLICIT]
* {test2=value2}
* key:[test2],value:[value222]
*/
}
统计
通过使用Caffeine.recordStats()
方法可以打开数据收集功能。Cache.stats()
方法将会返回一个CacheStats
对象,其将会含有一些统计指标,比如:
- hitRate(): 查询缓存的命中率
- evictionCount(): 被驱逐的缓存数量
- averageLoadPenalty(): 新值被载入的平均耗时
示例:
代码语言:javascript复制 public void testRecordStats() {
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(1)
//自定义数据采集器
.recordStats(() -> new StatsCounter() {
@Override
public void recordHits(@NonNegative int count) {
System.out.println("recordHits:" count);
}
@Override
public void recordMisses(@NonNegative int count) {
System.out.println("recordMisses:" count);
}
@Override
public void recordLoadSuccess(@NonNegative long loadTime) {
System.out.println("recordLoadSuccess:" loadTime);
}
@Override
public void recordLoadFailure(@NonNegative long loadTime) {
System.out.println("recordLoadFailure:" loadTime);
}
@Override
public void recordEviction() {
System.out.println("recordEviction...");
}
@Override
public @NonNull CacheStats snapshot() {
return null;
}
})
.build();
cache.put("test1", "value1");
cache.put("test2", "value2");
System.out.println(cache.asMap());
cache.getIfPresent("test1");
cache.getIfPresent("test3");
cache.cleanUp();
System.out.println(cache.asMap());
/**
* 打印结果:
* {test1=value1, test2=value2}
* recordHits:1
* recordMisses:1
* recordEviction...
* {test2=value2}
*/
}
public void testRecordStats2() {
LoadingCache<String, String> asyncCache = Caffeine.newBuilder()
.maximumSize(1)
//自定义数据采集器
.recordStats(() -> new StatsCounter() {
@Override
public void recordHits(@NonNegative int count) {
System.out.println("recordHits:" count);
}
@Override
public void recordMisses(@NonNegative int count) {
System.out.println("recordMisses:" count);
}
@Override
public void recordLoadSuccess(@NonNegative long loadTime) {
System.out.println("recordLoadSuccess:" loadTime);
}
@Override
public void recordLoadFailure(@NonNegative long loadTime) {
System.out.println("recordLoadFailure:" loadTime);
}
@Override
public void recordEviction() {
System.out.println("recordEviction...");
}
@Override
public @NonNull CacheStats snapshot() {
return null;
}
})
.build(new CacheLoader<String, String>() {
@Override
public @Nullable String load(@NonNull String key) throws Exception {
return MAP.get(key);
}
});
asyncCache.get("test1");
System.out.println(asyncCache.asMap());
/**
* 打印:
* recordMisses:1
* recordLoadSuccess:19800
* {test1=value1}
*/
}
public void testRecordStats3() {
LoadingCache<String, String> asyncCache = Caffeine.newBuilder()
.maximumSize(1)
//自定义数据采集器
.recordStats(() -> new StatsCounter() {
@Override
public void recordHits(@NonNegative int count) {
System.out.println("recordHits:" count);
}
@Override
public void recordMisses(@NonNegative int count) {
System.out.println("recordMisses:" count);
}
@Override
public void recordLoadSuccess(@NonNegative long loadTime) {
System.out.println("recordLoadSuccess:" loadTime);
}
@Override
public void recordLoadFailure(@NonNegative long loadTime) {
System.out.println("recordLoadFailure:" loadTime);
}
@Override
public void recordEviction() {
System.out.println("recordEviction...");
}
@Override
public @NonNull CacheStats snapshot() {
return null;
}
})
.build(new CacheLoader<String, String>() {
@Override
public @Nullable String load(@NonNull String key) throws Exception {
throw new RuntimeException("failed");
}
});
asyncCache.get("test1");
System.out.println(asyncCache.asMap());
/**
* 打印:
* recordMisses:1
* recordLoadFailure:41100
*/
}
public void testRecordStats4() {
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(1)
//打开数据采集
.recordStats()
.build();
cache.put("test1", "value1");
cache.put("test2", "value2");
System.out.println(cache.asMap());//{test1=value1, test2=value2}
cache.getIfPresent("test1");
cache.getIfPresent("test3");
cache.cleanUp();
System.out.println(cache.asMap());//{test2=value2}
System.out.println(cache.stats().hitRate());//查询缓存的命中率 0.5
System.out.println(cache.stats().hitCount());//命中次数 1
System.out.println(cache.stats().evictionCount());//被驱逐的缓存数量 1
System.out.println(cache.stats().averageLoadPenalty());//新值被载入的平均耗时
/**
* 打印结果:
* {test1=value1, test2=value2}
* {test2=value2}
* 0.5
* 1
* 1
* 0.0
*/
}
public void testRecordStats5() {
LoadingCache<String, String> asyncCache = Caffeine.newBuilder()
.maximumSize(1)
//打开数据采集
.recordStats()
.build(new CacheLoader<String, String>() {
@Override
public @Nullable String load(@NonNull String key) throws Exception {
return MAP.get(key);
}
});
asyncCache.get("test1");
asyncCache.get("test1");
System.out.println(asyncCache.asMap());//{test1=value1}
System.out.println(asyncCache.stats().hitRate());//查询缓存的命中率 0.5
System.out.println(asyncCache.stats().hitCount());//命中次数 1
System.out.println(asyncCache.stats().evictionCount());//被驱逐的缓存数量 0
System.out.println(asyncCache.stats().averageLoadPenalty());//新值被载入的平均耗时 21100.0
/**
* 打印:
* {test1=value1}
* 0.5
* 1
* 0
* 21100.0
*/
}
参考文献
Java高性能本地缓存框架Caffeine
干掉GuavaCache:Caffeine才是本地缓存的王
SpringCache继承Caffeine
添加依赖
代码语言:javascript复制 <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
添加配置
注意千万不要漏了@EnableCaching
代码语言:javascript复制@Configuration
@EnableCaching
public class CacheConfig extends CachingConfigurerSupport {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.initialCapacity(10)
.maximumSize(100)
.expireAfterAccess(10, TimeUnit.MINUTES));
cacheManager.setAllowNullValues(true);
return cacheManager;
}
@Bean
@Override
public KeyGenerator keyGenerator() {
return (target, method, params) -> {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(":");
sb.append(method.getName());
for (Object obj : params) {
sb.append(":").append(obj);
}
return String.valueOf(sb);
};
}
}
注解使用
需要在缓存的方法上加入注解@Cacheable(cacheNames = “xxxxxx”) ,因为加上了KeyGenerator 的配置所以,key是根据KeyGenerator 来生成的,这里可以不写。
其他的注解使用请参照Spring Cache,或者可以参考我的另一篇博文关于Spring Cache Redis的集成,使用到的注解都是类似的。
参考文献
Spring Boot 中快速集成Spring Cache和Caffeine做内存缓存 玩转Spring Cache — 整合进程缓存之王Caffeine Cache和Ehcache3.x【享学Spring】 Spring Cache 集成 Caffeine实现项目缓存