缓存又分进程内缓存和分布式缓存两种:分布式缓存如redis、memcached等,还有本地(进程内)缓存如ehcache、GuavaCache、Caffeine等
Caffeine是一个基于Java8开发的高性能Java缓存库,可提供接近最佳的命中率,其结构和 Guava Cache 基本一样,api也一样,基本上很容易就能替换。 Caffeine 实际上就是在 Guava Cache 的基础上,利用了一些 Java 8 的新特性,提高了某些场景下的性能效率。缓存与ConcurrentMap相似,但并不完全相同。最根本的区别是ConcurrentMap会保留添加到其中的所有元素,直到将其明确删除为止。
- 通过异步自动加载实体到缓存中
- 基于大小的回收策略
- 基于时间的回收策略
- 自动刷新
- key自动封装虚引用
- value自动封装弱引用或软引用
- 实体过期或被删除的通知
- 写入外部资源
- 统计累计访问缓存
在项目pom.xml中添加Caffeine的依赖
<!-- https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.8.6</version>
</dependency>
Caffeine提供了四种缓存添加策略:手动加载,自动加载,手动异步加载和自动异步加载。
Caffeine 有两种方式限制缓存大小。两种配置互斥,不能同时配置
Cache<String, String> cache = Caffeine.newBuilder()
// 设置超时时间为5s,超过该时间缓存过期
.expireAfterWrite(5, TimeUnit.SECONDS)
// 设置缓存最大条目数,超过条目则触发回收。
.maximumSize(1)
.build();
需要注意的是,实际实现上为了性能考虑,这个限制并不会很死板:
- 在缓存元素个数快要达到最大限制的时候,过期策略就开始执行了,所以在达到最大容量前也许某些不太可能再次访问的 Entry (Key-Value)就被过期掉了。
- 有时候因为过期 Entry 任务还没执行完,更多的 Entry 被放入缓存,导致缓存的 Entry 个数短暂超过了这个限制
示例:
/**
* 手动加载cache
*/
@Test
public void testManualLoadCache2() throws InterruptedException {
Cache<String, String> cache = Caffeine.newBuilder()
// 设置超时时间为5s,超过该时间缓存过期
.expireAfterWrite(5, TimeUnit.SECONDS)
// 设置缓存最大条目数,超过条目则触发回收。
.maximumSize(1)
.build();
// 查找一个缓存元素, 没有查找到的时候返回null
String value = cache.getIfPresent("test");
//执行结果--> null
System.out.println(value);
// 查找缓存,如果缓存不存在则生成缓存元素, 如果无法生成则返回null
value = cache.get("test", k -> "test-value");
//执行结果--> test-value
System.out.println(cache.getIfPresent("test"));
//执行结果--> test-value
System.out.println(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);
//查看缓存中的数据
//执行结果--> 1
System.out.println(dataObjectMap.size());
//执行结果--> {test9=test-value9}
System.out.println(dataObjectMap);
//设置10s的睡眠时间,使得超过过期时间
Thread.sleep(10000);
//执行结果--> null
System.out.println(cache.getIfPresent("test"));
}
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 是等效的。 同样的,为了性能考虑,这个限制也不会很死板。
示例:
@Test
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
类似,通过指定一个初始大小,减少扩容带来的性能损耗。这个值也不宜过大,浪费内存。
示例:
@DisplayName("测试LoadingCache")
@Test
public void testLoadingCache() {
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);
}
});
//第一次的时候会调用load方法
System.out.println(cache.get("test1"));
//第二次不会调用load方法
System.out.println(cache.get("test1"));
}
示例:
@Test
public void testAsyncCache() throws ExecutionException, InterruptedException {
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, 阻塞
}
测试结果:
ForkJoinPool.commonPool-worker-3
load cache ---test1
main
=========
value1
示例1:
@Test
public void testAsynchronouslyLoadingCache() throws ExecutionException, InterruptedException {
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");
// main
System.out.println(Thread.currentThread().getName());
System.out.println("=========");
// value1 阻塞
System.out.println(value.get());
}
测试结果
ForkJoinPool.commonPool-worker-3
load cache ---test1
main
=========
value1
示例2:
@Test
public void testAsynchronouslyLoadingCache2() throws ExecutionException, InterruptedException {
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();
}
// ForkJoinPool.commonPool-worker-3
System.out.println(Thread.currentThread().getName());
return MAP.get(key);
});
}
});
System.out.println(cache.get("test1").get()); // value1 阻塞
}
测试结果:
main
load cache
ForkJoinPool.commonPool-worker-3
value1
基于大小的我们前面已经讲到了。也就是通过设置maximumSize
来进行大小驱逐策略,还有设置maximumWeight
来设置权重驱逐策略
示例:
@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提供了三种定时驱逐策略
示例:
@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);
//执行结果-> value1
System.out.println(cache.getIfPresent("key1"));
Thread.sleep(1000);
//执行结果-> value1
System.out.println(cache.getIfPresent("key1"));
Thread.sleep(1000);
//执行结果-> null
System.out.println(cache.getIfPresent("key1"));
}
caffeine的缓存清除策略是惰性删除和定时删除,如果想使用缓存作为一个定时容器,缓存移除监听当作定时器的回调,可能会出现下面的问题:即使key已过期,若没有对该key再次访问,则该key将一直不会被删除,移除监听里的代码永远得不到执行。
示例:
@DisplayName("基于时间的过期策略,设置expireAfterAccess")
@Test
public void testManualLoadCache8() throws InterruptedException {
// 在最后一次读或者写入后开始计时,在指定的时间后过期。假如一直有请求访问该key,那么这个缓存将一直不会过期。
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfterAccess(3,TimeUnit.SECONDS)
.build();
cache.put("key1","value1");
Thread.sleep(1000);
//执行结果-> value1
System.out.println(cache.getIfPresent("key1"));
Thread.sleep(1000);
//执行结果-> value1
System.out.println(cache.getIfPresent("key1"));
Thread.sleep(1000);
//执行结果-> value1
System.out.println(cache.getIfPresent("key1"));
Thread.sleep(3001);
//执行结果-> null
System.out.println(cache.getIfPresent("key1"));
}
示例:
@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()
在保存key的时候将会进行弱引用。这允许在GC的过程中,当key没有被任何强引用指向的时候去将缓存元素回收。由于GC只依赖于引用相等性。这导致在这个情况下,缓存将会通过引用相等(==)而不是对象相等equals()
去进行key之间的比较。
示例:
/**
* 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()
在保存value的时候将会使用弱引用。这允许在GC的过程中,当value没有被任何强引用指向的时候去将缓存元素回收。由于GC只依赖于引用相等性。这导致在这个情况下,缓存将会通过引用相等(==)而不是对象相等equals()
去进行value之间的比较。
示例:
/**
* 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()
在保存value的时候将会使用软引用。为了相应内存的需要,在GC过程中被软引用的对象将会被通过LRU算法回收。由于使用软引用可能会影响整体性能,我们还是建议通过使用基于缓存容量的驱逐策略代替软引用的使用。同样的,使用softValues()
将会通过引用相等(==)而不是对象相等equals()
去进行value之间的比较。
示例:
@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());
}
@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
则会回收掉缓存
示例:
@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}
}
示例:
@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}
}
示例:
@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()); //{}
}
示例:
@Test
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]
}
@Test
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]
}
@Test
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,将对于缓存的更新,作用于其他存储,例如数据库。
示例:
@Test
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():
新值被载入的平均耗时
示例:
@Test
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}
*/
}
@Test
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}
*/
}
@Test
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
*/
}
@Test
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
*/
}
@Test
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
*/
}
那么,如果我的项目之前用的是GuavaCache,如何以尽可能低的成本迁移到Caffeine上来呢?嘿嘿,Caffeine已经想到了这一点,它提供了一个适配器,让你用Guava的接口操作它的缓存。代码片段如下所示:
// Guava's LoadingCache interface
LoadingCache<Key, Graph> graphs = CaffeinatedGuava.build(
Caffeine.newBuilder().maximumSize(10_000),
new CacheLoader<Key, Graph>() { // Guava's CacheLoader
@Override public Graph load(Key key) throws Exception {
return createExpensiveGraph(key);
}
});
关于淘汰算法的经典文章:Design Of A Modern Cache