简介
本文介绍Java本地缓存工具Caffeine的用法。
常用的本地缓存方案有:
- JDK自带的(HashMap、ConcurrentHashMap等)
- Caffeine
- Guava Cache
- Encache
如果不需要超时时间,那么JDK的Map就够用了。
如果需要超时时间,就要使用第三方包了。推荐使用Caffeine,因为它性能很高。SpringBoot2.X及之后的版本使用的本地缓存就是Caffeine(旧版的SpringBoot用的是Guava Cache)。
快速上手
引入依赖
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.3</version>
<!-- 3.0.0及之后的版本不支持JDK8编译,需要更高版本的JDK才行 -->
</dependency>
使用
package com.knife.example.demo;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.TimeUnit;
public class Demo {
public static void main(String[] args) {
// 创建一个Caffeine缓存,指定最大缓存大小为100
Cache<String, Integer> cache = Caffeine.newBuilder()
// 最大100,超过则放弃老的
.maximumSize(100)
// 10秒过期(没有读写自动删除)
.expireAfterAccess(10, TimeUnit.SECONDS)
.build();
String key = "age";
// 写入数据
cache.put(key, 23);
// 读取数据
Integer value = cache.getIfPresent(key);
System.out.println(value);
// 删除
cache.invalidate(key);
// 读取数据
value = cache.getIfPresent(key);
System.out.println(value);
}
}
结果
23 null
详细用法
填充策略
填充策略是指key不存在时如何创建一个对象进行返回,主要分为下面四种:
手动(Manual)
package com.knife.example.demo;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
public class Demo {
public static void main(String[] args) {
Cache<String, Integer> cache = Caffeine.newBuilder().build();
String key = "age";
// 读取数据
Integer value = cache.get(key, k -> {
System.out.println("key:" + k);
return 18;
});
System.out.println(value);
}
}
自动(Loading)
package com.knife.example.demo;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
public class Demo {
public static void main(String[] args) {
//此时的类型是 LoadingCache 不是 Cache
LoadingCache<String, Integer> cache = Caffeine.newBuilder().build(key -> {
System.out.println("自动填充:" + key);
return 18;
});
String key = "age";
// 读取数据。key 不存在时会根据给定的CacheLoader自动装载进去
Integer value = cache.get(key);
System.out.println(value);
}
}
结果
自动填充:age 18
异步手动(Asynchronous Manual)
package com.knife.example.demo;
import com.github.benmanes.caffeine.cache.AsyncCache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public class Demo {
public static void main(String[] args) throws Exception {
//此时的类型是 LoadingCache 不是 Cache
AsyncCache<String, Integer> cache = Caffeine.newBuilder().buildAsync();
String key = "age";
//返回future对象, 调用其get方法会一直卡住直到得到返回,和多线程的submit一样
CompletableFuture<Integer> ageFuture = cache.get(key, name -> {
System.out.println("name:" + name);
return 18;
});
Integer age = ageFuture.get();
System.out.println("age:" + age);
}
}
结果
name:age age:18
异步自动(Asynchronously Loading)
package com.knife.example.demo;
import com.github.benmanes.caffeine.cache.AsyncLoadingCache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.CompletableFuture;
public class Demo {
public static void main(String[] args) throws Exception {
String key = "age";
AsyncLoadingCache<String, Integer> cache = Caffeine.newBuilder().buildAsync(name -> {
System.out.println("name:" + name);
return 18;
});
CompletableFuture<Integer> ageFuture = cache.get(key);
Integer age = ageFuture.get();
System.out.println(age);
}
}
结果
name:age 18
清除策略
有三种:基于大小、过期时间、引用类型。
1.基于大小
package com.knife.example.demo;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
public class Demo {
public static void main(String[] args) throws Exception {
Cache<String, Integer> cache = Caffeine.newBuilder()
.maximumSize(10)
.removalListener((key, value, cause) -> {
System.out.printf("key %s was removed(value:%s;cause:%s)\n",
key, value, cause);
})
.build();
for (int i = 0; i < 15; i++) {
cache.put("name" + i, i);
}
Thread.currentThread().join();
}
}
结果:
key name0 was removed(value:0;cause:SIZE) key name3 was removed(value:3;cause:SIZE) key name4 was removed(value:4;cause:SIZE) key name2 was removed(value:2;cause:SIZE) key name1 was removed(value:1;cause:SIZE)
也可以基于权重:
Cache<String, String> cache = Caffeine.newBuilder()
.maximumWeight(1000)
.weigher((String key, String value) -> value.length())
.build();
2.过期时间
expireAfterAccess():缓存访问(读或写)后,一定时间失效;
expireAfterWrite():缓存写入后,一定时间失效;
expireAfter():自定义缓存策略,满足多样化的过期时间要求。
package com.knife.example.demo;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.Expiry;
import lombok.NonNull;
import org.checkerframework.checker.index.qual.NonNegative;
import java.util.concurrent.TimeUnit;
public class Demo {
public static void main(String[] args) {
// 1.缓存访问后(读或者写),一定时间后失效
Cache<String, String> cache1 = Caffeine.newBuilder()
.expireAfterAccess(10L, TimeUnit.SECONDS)
.build();
// 2.缓存写入后,一定时间后失效
Cache<String, String> cache2 = Caffeine.newBuilder()
.expireAfterWrite(10L, TimeUnit.SECONDS)
.build();
// 3.自定义过期策略
Cache<String, String> cache3 = Caffeine.newBuilder()
.expireAfter(new Expiry<Object, Object>() {
@Override
public long expireAfterCreate(@NonNull Object o, @NonNull Object o2, long l) {
return 0;
}
@Override
public long expireAfterUpdate(@NonNull Object o, @NonNull Object o2, long l, @NonNegative long l1) {
return 0;
}
@Override
public long expireAfterRead(@NonNull Object o, @NonNull Object o2, long l, @NonNegative long l1) {
return 0;
}
})
.build();
}
}
3.引用类型
利用了Java中的引用类型,详见:这里
Caffeine.weakKeys():使用弱引用存储key,没有其他的强引用时,会被垃圾回收器回收;
Caffeine.weakValues():使用弱引用存储value,没有其他的强引用时,会被垃圾回收器回收;
Caffeine.softValues():使用软引用存储key,当没有其他的强引用时,内存不足的时候会被回收;
注:弱引用或软引用的值不能使用AsyncCache。
package com.knife.example.demo;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
public class Demo {
public static void main(String[] args) {
// 1.弱引用弱key方式,没有强引用时回收
Cache<String, String> cache1 = Caffeine.newBuilder()
.weakKeys()
.weakValues()
.build();
// 2.软引用,内存不足的时回收
Cache<String, String> cache2 = Caffeine.newBuilder()
.softValues()
.build();
}
}
手动删除
Caffeine可以手动删除缓存,无需等待上面提到的被动的一些删除策略,方法如下:
cacheOne.invalidateAll(); cacheOne.invalidate(Object o); cacheOne.invalidateAll(List);
监听移除事件
RemovalListener是缓存监听事件,key被移除时就会触发这个方法。RemovalListener可以获取到key、value和RemovalCause(删除的原因)。(RemovalListener中的操作是异步执行的)。
package com.knife.example.demo;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
public class Demo {
public static void main(String[] args) throws Exception {
Cache<String, Integer> cache = Caffeine.newBuilder()
.maximumSize(10)
.removalListener((key, value, cause) -> {
System.out.printf("key %s was removed(value:%s;cause:%s)\n",
key, value, cause);
})
.build();
for (int i = 0; i < 15; i++) {
cache.put("name" + i, i);
}
Thread.currentThread().join();
}
}
结果
key name0 was removed(value:0;cause:SIZE) key name3 was removed(value:3;cause:SIZE) key name4 was removed(value:4;cause:SIZE) key name2 was removed(value:2;cause:SIZE) key name1 was removed(value:1;cause:SIZE)
监听写入操作
缓存读写都会调用到CacheWriter。
CacheWriter也可以代替上面提到的RemovalListener,但CacheWriter是同步执行的,而且是原子操作(写入缓存完成前会阻塞后续更新缓存的操作,但读缓存不会阻塞。
package com.knife.example.demo;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.CacheWriter;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalCause;
import com.sun.istack.internal.Nullable;
import lombok.NonNull;
import java.util.concurrent.TimeUnit;
public class Demo {
public static void main(String[] args) {
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.printf("---write: key:%s,value:%s%n", key, value);
}
@Override
public void delete(@NonNull String key, @Nullable String value, @NonNull RemovalCause cause) {
// 从持久化或者次级缓存中删除
System.out.printf("---delete:key:%s,value:%s,cause:%s%n", key, value, cause);
}
})
.build();
cache.put("test1", "value1");
System.out.println(cache.asMap());
cache.invalidate("test1");
System.out.println(cache.asMap());
}
}
结果
---write: key:test1,value:value1
{test1=value1}
---delete:key:test1,value:value1,cause:EXPLICIT
{}
统计
package com.knife.example.demo;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.stats.CacheStats;
public class Demo {
public static void main(String[] args) {
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(1000)
.recordStats()
.build();
cache.put("Hello", "Caffeine");
for (int i = 0; i < 15; i++) {
// 命中15次
cache.getIfPresent("Hello");
}
for (int i = 0; i < 5; i++) {
// 未命中5次
cache.getIfPresent("a");
}
cache.get("b", key -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return key + "B";
});
CacheStats stats = cache.stats();
System.out.println(stats);
System.out.println("命中率:" + stats.hitRate());
System.out.println("未命中率:" + stats.missRate());
System.out.println("加载新值花费的平均时间:" + stats.averageLoadPenalty());
}
}
结果
CacheStats{hitCount=15, missCount=6, loadSuccessCount=1, loadFailureCount=0, totalLoadTime=1006854400, evictionCount=0, evictionWeight=0}
命中率:0.7142857142857143
未命中率:0.2857142857142857
加载新值花费的平均时间:1.0068544E9

请先 !