文章目录
介绍
Caffeine是基于JDK1.8版本的高性能本地缓存库,它是Guava的增强版,与ConcurrentLinkedHashMap相似,支持并发,并且可以在O(1)的时间复杂度内查找、写入元素。
性能比对
使用方式
一、Population(缓存类型)
1.Cache
private static void manual() {
// 构建caffeine的缓存对象,并指定在写入后的10分钟内有效,且最大允许写入的条目数为10000
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(10_000)
.build();
String key = "hello";
// 查找某个缓存元素,若找不到则返回null
String str = cache.getIfPresent(key);
System.out.println("cache.getIfPresent(key) ---> " + str);
// 查找某个缓存元素,若找不到则调用函数生成,如无法生成则返回null
str = cache.get(key, k -> create(key));
System.out.println("cache.get(key, k -> create(key)) ---> " + str);
// 添加或者更新一个缓存元素
cache.put(key, str);
System.out.println("cache.put(key, str) ---> " + cache.getIfPresent(key));
// 移除一个缓存元素
cache.invalidate(key);
System.out.println("cache.invalidate(key) ---> " + cache.getIfPresent(key));
}
private static String create(Object key) {
return key + " world";
}
输出结果:
cache.getIfPresent(key) ---> null
cache.get(key, k -> create(key)) ---> hello world
cache.put(key, str) ---> hello world
cache.invalidate(key) ---> null
2.Loading
LoadingCache是附加在CacheLoader之上构建的缓存对象。
可以使用getAll方法执行批量查找,默认情况下,getAll()方法会单独调用CacheLoader.load()方法来加载每个不在缓存中的Key,必要情况下可以重写CacheLoader.loadAll()方法来弥补其缺陷。
public static void loading() {
LoadingCache<String, String> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(key -> create(key)); // 当调用get或者getAll时,若找不到缓存元素,则会统一调用create(key)生成
String key = "hello";
String str = cache.get(key);
System.out.println("cache.get(key) ---> " + str);
List<String> keys = Lists.newArrayList("a", "b", "c", "d", "e");
// 批量查找缓存元素,如果缓存不存在则生成缓存元素
Map<String, String> maps = cache.getAll(keys);
System.out.println("cache.getAll(keys) ---> " + maps);
}
private static String create(Object key) {
return key + " world";
}
输出结果
cache.get(key) ---> hello world
cache.getAll(keys) ---> {a=a world, b=b world, c=c world, d=d world, e=e world}
3.Asynchronous (Manual)
AsyncCache就是Cache的异步实现方式,提供了通过Executor生成缓存元素并返回CompletableFuture的能力。
synchronous()提供了在缓存计算完成前的阻塞能力,AsyncCache默认使用ForkJoinPool.commonPool()线程池,你也可以通过重写Caffeine.executor(executor)来实现自己的线程池。
private static void asynchronous() {
AsyncCache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(10_000)
.buildAsync();
String key = "Hello";
// 查找某个缓存元素,若找不到则返回null
CompletableFuture<String> value = cache.getIfPresent(key);
// 查找某个缓存元素,若不存在则异步调用create方法生成
value = cache.get(key, k -> create(key));
// 添加或者更新一个缓存元素
cache.put(key, value);
// 移除一个缓存元素
cache.synchronous().invalidate(key);
}
4.Asynchronously Loading
AsyncLoadingCache就是LoadingCache的异步形式
private static void asynchronouslyLoading() {
AsyncLoadingCache<String, String> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
// 异步构建一个同步的调用方法create(key)
.buildAsync(key -> create(key));
// 也可以使用下面的方式来异步构建缓存,并返回一个future
// .buildAsync((key, executor) -> createAsync(key, executor));
String key = "Hello";
// 查找某个缓存元素,若找不到则会异步生成。
CompletableFuture<String> value = cache.get(key);
List<String> keys = Lists.newArrayList("a", "b", "c", "d", "e");
// 批量查找某些缓存元素,若找不到则会异步生成。
CompletableFuture<Map<String, String>> graphs = cache.getAll(keys);
}
二、Eviction(驱除策略)
1.Size-based(基于容量)
private static void sizeBased() {
// 基于缓存内元素的个数,尝试回收最近或者未经常使用的元素
LoadingCache<String, String> values = Caffeine.newBuilder()
.maximumSize(10_000)
.build(key -> create(key));
// 也可以基于缓存元素的权重,进行驱除
LoadingCache<String, String> graphs = Caffeine.newBuilder()
.maximumWeight(10_000)
.weigher((String key, String value) -> value.length())
.build(key -> create(key));
}
2.Time-based(基于时间)
private static void timeBased() {
// 自上一次写入或者读取缓存开始,在经过指定时间之后过期。
LoadingCache<String, String> fixedAccess = Caffeine.newBuilder()
.expireAfterAccess(5, TimeUnit.MINUTES)
.build(key -> create(key));
// 自缓存生成后,经过指定时间或者一次替换值之后过期。
LoadingCache<String, String> fixedWrite = Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.build(key -> create(key));
// 自定义缓存过期策略,可以在创建时,写入后、读取时。
LoadingCache<String, String> varying = Caffeine.newBuilder()
.expireAfter(new Expiry<String, String>() {
public long expireAfterCreate(String key, String value, long currentTime) {
return currentTime;
}
public long expireAfterUpdate(String key, String value,
long currentTime, long currentDuration) {
return currentDuration;
}
public long expireAfterRead(String key, String value,
long currentTime, long currentDuration) {
return currentDuration;
}
})
.build(key -> create(key));
}
3.Reference-based(基于引用)
private static void referenceBased() {
// 当缓存key和value都不存在强引用关系时,进行驱逐
LoadingCache<String, String> weak = Caffeine.newBuilder()
.weakKeys()
.weakValues()
.build(key -> create(key));
// 当发生GC时进行驱逐
LoadingCache<String, String> soft = Caffeine.newBuilder()
.softValues()
.build(key -> create(key));
}
三、Explicit Removals(显示删除)
1.直接删除
// 直接删除
cache.invalidate(key)
// 批量删除
cache.invalidateAll(keys)
// 删除所有
cache.invalidateAll()
2.可监听删除
监听缓存元素被删除或者被驱除事件
private static void removalsListeners() {
Cache<String, String> cache = Caffeine.newBuilder()
// 注意:evictionListener是3.X版本中新特性,2.X版本中没有
.evictionListener((String key, String value, RemovalCause cause) ->
System.out.printf("Key %s was removed (%s)%n", key, cause))
.removalListener((String key, String value, RemovalCause cause) ->
System.out.printf("Key %s was removed (%s)%n", key, cause))
.build();
cache.put("Hello", "Caffeine");
System.out.println(cache.getIfPresent("Hello"));
cache.invalidate("Hello");
try {
// 监听是异步执行的
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
四、Refresh(刷新机制)
refresh只有在LoadingCache或者AsyncLoadingCache时才能使用,与驱逐不同之处,在刷新的时候,如果访问元素仍然可以返回,但返回的是旧值。
static StringBuffer stringBuffer = new StringBuffer(">");
private static void refresh() {
LoadingCache<String, String> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.refreshAfterWrite(1, TimeUnit.SECONDS)
.build(key -> stringBuffer.append(key).toString());
for (int i = 0; i < 5; i++) {
// 这里需要注意,前两次输出都是:“>*”
// 理论上第二次输出应是:“>**”,这是因为refreshAfterWrite刷新实际上指的是在x秒、并且是第二次访问之后才开始刷新
System.out.println(cache.get("*"));
try {
Thread.sleep(1200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出结果
>*
>*
>**
>***
>****
五、Statistics(统计)
caffeine提供了完善的缓存统计能力,提供了metrics-caffeine类库,可以直接入Prometheus
private static void statistics() {
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.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=1014390300, evictionCount=0, evictionWeight=0}
命中率:0.7142857142857143
未命中率:0.2857142857142857
加载新值花费的平均时间:1.0143903E9
hitRate:命中率
hitCount:命中次数
missRate:未命中率
missCount:未命中次数
loadSuccessCount:成功加载新值的次数
loadFailureCount:失败加载新值的次数
totalLoadTime:总计加载的耗时
averageLoadPenalty:加载新值花费的平均时间
evictionCount:驱逐的条目数
evictionWeight:按权重驱除的次数
Caffeine在SpringBoot中的使用
配置类
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalCause;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
@Configuration
public class CacheConfig {
@Bean
public Cache<String, Object> caffeineCache() {
return Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(1500, TimeUnit.MILLISECONDS)
.removalListener((String key, Object value, RemovalCause cause) ->
removalListener(key, value, cause))
.recordStats()
.build();
}
private void removalListener(String key, Object value, RemovalCause cause) {
System.out.printf("Key: %s , Value: %s , was removed (%s)%n", key, value, cause);
System.out.println();
}
}
测试类
@RunWith(SpringRunner.class)
@SpringBootTest
public class CaffeineCacheTestTests {
@Resource
CacheConfig cacheConfig;
@Test
public void test() throws InterruptedException {
Cache<String, Object> caffeineCache = cacheConfig.caffeineCache();
new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
while(true){
Thread.sleep(1000);
CacheStats cacheStats = caffeineCache.stats();
System.out.println("命中率:" + cacheStats.hitRate());
System.out.println("未命中率:" + cacheStats.missRate());
System.out.println();
}
}
}).start();
for (int i = 1; i <= 10; i++) {
Object cacheKey = caffeineCache.getIfPresent("hello");
if (Objects.isNull(cacheKey)) {
caffeineCache.put("hello", "caffeine");
}
Thread.sleep(((i % 2) + 1) * 1000);
}
}
}
测试结果
命中率:0.0
未命中率:1.0
Key: hello , Value: caffeine , was removed (EXPIRED)
命中率:0.0
未命中率:1.0
命中率:0.0
未命中率:1.0
命中率:0.3333333333333333
未命中率:0.6666666666666666
Key: hello , Value: caffeine , was removed (EXPIRED)
命中率:0.25
未命中率:0.75
命中率:0.4
未命中率:0.6
命中率:0.4
未命中率:0.6
Key: hello , Value: caffeine , was removed (EXPIRED)
命中率:0.3333333333333333
未命中率:0.6666666666666666
命中率:0.42857142857142855
未命中率:0.5714285714285714
命中率:0.42857142857142855
未命中率:0.5714285714285714
Key: hello , Value: caffeine , was removed (EXPIRED)
命中率:0.375
未命中率:0.625
命中率:0.4444444444444444
未命中率:0.5555555555555556
命中率:0.4444444444444444
未命中率:0.5555555555555556
Key: hello , Value: caffeine , was removed (EXPIRED)
命中率:0.4
未命中率:0.6