简介
本文介绍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
请先
!