Caffeine本地缓存学习
0.依赖坐标
<!-- https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.6</version>
</dependency>
1.介绍
Caffeine是一个基于Java8开发的提供了近乎最佳命中率的高性能的缓存库。
缓存和ConcurrentMap有点相似,但还是有所区别。最根本的区别是ConcurrentMap将会持有所有加入到缓存当中的元素,直到它们被从缓存当中手动移除。但是,Caffeine的缓存Cache
通常会被配置成自动驱逐缓存中元素,以限制其内存占用。在某些场景下,LoadingCache
和AsyncLoadingCache
因为其自动加载缓存的能力将会变得非常实用。
Caffeine提供了灵活的构造器去创建一个拥有下列特性的缓存:
- 自动加载元素到缓存当中,异步加载的方式也可供选择
- 当达到最大容量的时候可以使用基于就近度和频率的算法进行基于容量的驱逐
- 将根据缓存中的元素上一次访问或者被修改的时间进行基于过期时间的驱逐
- 当向缓存中一个已经过时的元素进行访问的时候将会进行异步刷新
- key将自动被弱引用所封装
- value将自动被弱引用或者软引用所封装
- 驱逐(或移除)缓存中的元素时将会进行通知
- 写入传播到一个外部数据源当中
- 持续计算缓存的访问统计指标
为了提高集成度,扩展模块提供了JSR-107 JCache和Guava适配器。JSR-107规范了基于Java 6的API,在牺牲了功能和性能的代价下使代码更加规范。Guava的Cache是Caffeine的原型库并且Caffeine提供了适配器以供简单的迁移策略。
2.常用方法
V getIfPresent(K key) :如果缓存中 key 存在,则获取 value,否则返回 null。
void put( K key, V value):存入一对数据 <key, value>。
Map<K, V> getAllPresent(Iterable<?> var1) :参数是一个迭代器,表示可以批量查询缓存。 void putAll( Map<? extends K, ? extends V> var1); 批量存入缓存。 void invalidate(K var1):删除某个 key 对应的数据。 void invalidateAll(Iterable<?> var1):批量删除数据。
void invalidateAll():清空缓存。
long estimatedSize():返回缓存中数据的个数。执行此方法之前先执行cleanup()。
CacheStats stats():返回缓存当前的状态指标集。
ConcurrentMap<K, V> asMap():将缓存中所有的数据构成一个 map。
void cleanUp():会对缓存进行整体的清理,比如有一些数据过期了,但是并不会立马被清除,所以执行一次 cleanUp 方法,会对缓存进行一次检查,清除那些应该清除的数据。
V get( K var1, Function<? super K, ? extends V> var2):第一个参数是想要获取的 key,第二个参数是函数
3.缓存添加策略
Caffeine提供了四种缓存添加策略:手动加载,自动加载,手动异步加载和自动异步加载。
3.1.手动加载
Cache
接口提供了显式搜索查找、更新和移除缓存元素的能力。
缓存元素可以通过调用 cache.put(key, value)
方法被加入到缓存当中。如果缓存中指定的key已经存在对应的缓存元素的话,那么先前的缓存的元素将会被直接覆盖掉。因此,通过 cache.get(key, k -> value)
的方式将要缓存的元素通过原子计算的方式 插入到缓存中,以避免和其他写入进行竞争。值得注意的是,当缓存的元素无法生成或者在生成的过程中抛出异常而导致生成元素失败,cache.get
也许会返回 null
。
当然,也可以使用Cache.asMap()
所暴露出来的ConcurrentMap的方法对缓存进行操作。
示例:
/**
* 手动加载
*/
@Test
public void manualLoad(){
//初始化cache
Cache<Integer, String> cache = Caffeine.newBuilder()
//初始化缓存大小
.initialCapacity(10)
//最大缓存策略,超过此大小则会触发驱逐策略
.maximumSize(100)
//驱逐策略
.expireAfterWrite(10, TimeUnit.SECONDS)
.build();
//向缓存里面添加元素
cache.put(1,"1");
//查询指定缓存元素,没有则返回为null
String s = cache.getIfPresent(1);
System.out.println("s = " + s);
//查询指定缓存元素,可以指定默认的返回值,默认是null
String s1 = cache.get(2, integer -> null);
System.out.println("s1 = " + s1);
//移除指定缓存元素
cache.invalidate(1);
//查询指定缓存元素,没有则返回为null
s = cache.getIfPresent(1);
System.out.println("s = " + s);
}
控制台打印:
s = 1
s1 = null
s = null
3.2.自动加载
一个LoadingCache
是一个Cache
附加上 CacheLoader
能力之后的缓存实现。
通过 getAll
可以达到批量查找缓存的目的。 默认情况下,在getAll
方法中,将会对每个不存在对应缓存的key调用一次 CacheLoader.load
来生成缓存元素。 在批量检索比单个查找更有效率的场景下,你可以覆盖并开发CacheLoader.loadAll
方法来使你的缓存更有效率。
值得注意的是,你可以通过实现一个 CacheLoader.loadAll
并在其中为没有在参数中请求的key也生成对应的缓存元素。打个比方,如果对应某个key生成的缓存元素与包含这个key的一组集合剩余的key所对应的元素一致,那么在loadAll
中也可以同时加载剩下的key对应的元素到缓存当中。
示例:
/**
* 自动加载
*/
@Test
public void autoLoad(){
//get(key)为null时,则自动填充key+1
LoadingCache<Integer, String> cache = Caffeine.newBuilder()
.initialCapacity(10)
.maximumSize(100)
.expireAfterWrite(10, TimeUnit.SECONDS)
//设置默认生成缓存元素策略
.build(key -> "自动填充:"+(key+1));
//向缓存里面添加元素
cache.put(1,"1");
//查询指定元素,没有则返回默认的缓存元素
String s = cache.get(2);
System.out.println("s = " + s);
}
控制台打印:
s = 自动填充:3
3.3.手动异步加载
一个AsyncCache
是 Cache
的一个变体,AsyncCache
提供了在 Executor上生成缓存元素并返回 CompletableFuture的能力。这给出了在当前流行的响应式编程模型中利用缓存的能力。
synchronous()
方法给 Cache
提供了阻塞直到异步缓存生成完毕的能力。
当然,也可以使用 AsyncCache.asMap()
所暴露出来的ConcurrentMap的方法对缓存进行操作。
默认的线程池实现是 ForkJoinPool.commonPool() ,当然你也可以通过覆盖并实现 Caffeine.executor(Executor)
方法来自定义你的线程池选择。
示例:
/**
* 手动异步加载
*/
@Test
public void asyncLoad() throws ExecutionException, InterruptedException, TimeoutException {
AsyncCache<Integer, String> cache = Caffeine.newBuilder()
.initialCapacity(10)
.maximumSize(100)
.expireAfterWrite(10, TimeUnit.SECONDS)
.buildAsync();
// 查找一个缓存元素, 没有查找到的时候返回null
cache.put(1, CompletableFuture.completedFuture("1"));
CompletableFuture<String> present = cache.getIfPresent(1);
if (present != null) {
String s = present.get(1000,TimeUnit.SECONDS);
System.out.println("s = " + s);
}
// 同步移除一个缓存元素
cache.synchronous().invalidate(1);
CompletableFuture<String> ifPresent = cache.getIfPresent(1);
if (ifPresent != null) {
String s1 = ifPresent.get();
System.out.println("s1 = " + s1);
}else {
System.out.println("无值");
}
//查找缓存元素,如果不存在,则异步生成
CompletableFuture<String> future = cache.get(1, key -> "无值");
String s = future.get();
System.out.println(s);
}
控制台打印:
s = 1
无值
无值
3.4.自动异步加载
一个 AsyncLoadingCache
是一个 AsyncCache
加上 AsyncCacheLoader
能力的实现。
在需要同步的方式去生成缓存元素的时候,CacheLoader
是合适的选择。而在异步生成缓存的场景下, AsyncCacheLoader
则是更合适的选择并且它会返回一个 CompletableFuture。
通过 getAll
可以达到批量查找缓存的目的。 默认情况下,在getAll
方法中,将会对每个不存在对应缓存的key调用一次 AsyncCacheLoader.asyncLoad
来生成缓存元素。 在批量检索比单个查找更有效率的场景下,你可以覆盖并开发AsyncCacheLoader.asyncLoadAll
方法来使你的缓存更有效率。
值得注意的是,你可以通过实现一个 AsyncCacheLoader.asyncLoadAll
并在其中为没有在参数中请求的key也生成对应的缓存元素。打个比方,如果对应某个key生成的缓存元素与包含这个key的一组集合剩余的key所对应的元素一致,那么在asyncLoadAll
中也可以同时加载剩下的key对应的元素到缓存当中。
示例:
/**
* 自动异步加载
*/
@Test
public void autoAsyncLoad() throws ExecutionException, InterruptedException, TimeoutException {
AsyncLoadingCache<Integer, String> cache = Caffeine.newBuilder()
.initialCapacity(10)
.maximumSize(100)
.expireAfterWrite(10, TimeUnit.SECONDS)
.buildAsync(key -> "自动填充");
//向缓存里面添加元素
cache.put(1, CompletableFuture.completedFuture("1"));
// 查找一个缓存元素, 没有查找到的时候返回null
CompletableFuture<String> present = cache.getIfPresent(1);
if (present != null) {
String s = present.get(1000,TimeUnit.SECONDS);
System.out.println("s = " + s);
}
//同步删除
cache.synchronous().invalidate(1);
CompletableFuture<String> ifPresent = cache.getIfPresent(1);
if (ifPresent != null) {
String s1 = ifPresent.get();
System.out.println("s1 = " + s1);
}
//查询指定元素,没有则返回默认的缓存元素
CompletableFuture<String> future = cache.get(1);
String s = future.get();
System.out.println(s);
}
控制台打印:
s = 1
自动填充
4.缓存驱逐策略
Caffeine 提供了三种驱动策略,分别是基于容量,基于时间和基于引用三种类型。
4.1.基于容量
如果你的存量不想超过某个特定的大,那么记得用。Caffeine.maximumSize(long)
存量会尝试通过基本就在附近和频率的计算方法来驱动退出不会再被使用到的元素。
另一种情况,你的储藏可能中的元素可能存在于不同的“权力”–打个比方,你的储藏中的元素可能有不同的内存占用–你也许也需要借方方法来世界确定每个元素Caffeine.weigher(Weigher)
的权重并通过Caffeine.maximumWeight(long)
方法来界定存中元素的权重来现实上描述的场景。除掉“最大容量”所需要的注意事项,在权重驾逸的策略下,一个存储元素的权重计算正在其创建和更新中,此后其权力重值都是静态存在的,在两个元素之间进行权重的比较的时候,并不会根据权重的比较。
4.1.1.最大容量
当缓存容量达到最大容量时,会触发缓存驱逐策略,不是立即驱逐,有稍许延迟
示例:
/**
* 基于容量
*/
@Test
public void baseCapacity(){
LoadingCache<Integer, String> cache = Caffeine.newBuilder().maximumSize(5).build(key -> "无值");
for (int i = 0; i < 10; i++) {
cache.put(i,i+"");
}
long size = cache.estimatedSize();
System.out.println("size = " + size);
String s = cache.get(1);
System.out.println(s);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
for (int i = 0; i < 10; i++) {
s = cache.get(i);
System.out.println(s);
}
//条目可能不准确,更新条目
cache.cleanUp();
long size1 = cache.estimatedSize();
System.out.println("size1 = " + size1);
}
控制台打印:
size = 10
1
无值
无值
2
3
无值
无值
无值
7
8
9
size1 = 5
4.1.2.权重
当缓存里面元素的权重达到最大权重时,则会触发缓存驱逐策略
示例:当年龄之和达到21时,则会触发驱逐策略
/**
* 基于权重
*/
@Test
public void baseWeight(){
LoadingCache<String, Stuedent> cache = Caffeine.newBuilder()
.maximumWeight(21)
.weigher((String s, Stuedent stu) -> stu.getAge())
.build(key -> new Stuedent("1",1));
Stuedent stuedent;
for (int i = 1; i < 6; i++) {
stuedent=new Stuedent("张三"+i,i*2);
System.out.println("stuedent = " + stuedent);
cache.put("张三"+i,stuedent);
}
long size = cache.estimatedSize();
System.out.println("size = " + size);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//条目可能不准确,更新条目
cache.cleanUp();
size = cache.estimatedSize();
System.out.println("size = " + size);
for (int i = 1; i < 6; i++) {
Stuedent stuedent1 = cache.get("张三" + i);
System.out.println("stuedent1 = " + stuedent1);
}
}
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Stuedent {
private String name;
private Integer age;
}
控制台打印:
stuedent = Stuedent(name=张三1, age=2)
stuedent = Stuedent(name=张三2, age=4)
stuedent = Stuedent(name=张三3, age=6)
stuedent = Stuedent(name=张三4, age=8)
stuedent = Stuedent(name=张三5, age=10)
size = 5
size = 2
stuedent1 = Stuedent(name=1, age=1)
stuedent1 = Stuedent(name=1, age=1)
stuedent1 = Stuedent(name=1, age=1)
stuedent1 = Stuedent(name=张三4, age=8)
stuedent1 = Stuedent(name=张三5, age=10)
4.2.基于时间
Caffeine提供了三种方法进行基于时间的驱逐:
expireAfterAccess(long, TimeUnit):
最后一次访问之后,隔多久没有被再次访问的话,就过期。访问包括了 读 和 写expireAfterWrite(long, TimeUnit):
某个数据在多久没有被更新后,就过期。只能是被更新,才能延续数据的生命,即便是数据被读取了,也不行,时间一到,也会过期。expireAfter(Expiry):
一个元素将会在指定的时间后被认定为过期项。当被缓存的元素过期时间收到外部资源影响的时候,这是理想的选择。
4.2.1.expireAfterAccess
示例:
/**
* 基于时间读写
*/
@Test
public void expireAfterAccess(){
LoadingCache<Integer, String> cache = Caffeine.newBuilder()
.maximumSize(10)
.expireAfterAccess(1, TimeUnit.SECONDS)
.build(key -> "无值");
cache.put(1,"1");
String s = cache.get(1);
System.out.println("s = " + s);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
s = cache.get(1);
System.out.println("s = " + s);
}
控制台打印:
s = 1
s = 无值
4.2.2.expireAfterWrite
示例:
/**
* 基于时间创建更新
*/
@Test
public void expireAfterWrite(){
LoadingCache<Integer, String> cache = Caffeine.newBuilder()
.maximumSize(10)
.expireAfterWrite(1, TimeUnit.SECONDS)
.build(key -> "无值");
cache.put(1,"1");
String s = cache.get(1);
System.out.println("s = " + s);
try {
Thread.sleep(900);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
s = cache.get(1);
System.out.println("s = " + s);
cache.put(1,"1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
s = cache.get(1);
System.out.println("s = " + s);
}
控制台打印:
/**
* 基于时间创建更新
*/
@Test
public void expireAfterWrite(){
LoadingCache<Integer, String> cache = Caffeine.newBuilder()
.maximumSize(10)
.expireAfterWrite(1, TimeUnit.SECONDS)
.build(key -> "无值");
cache.put(1,"1");
String s = cache.get(1);
System.out.println("s = " + s);
try {
Thread.sleep(900);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
s = cache.get(1);
System.out.println("s = " + s);
cache.put(1,"1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
s = cache.get(1);
System.out.println("s = " + s);
}
4.2.3.expireAfter
示例基于具体业务实现
Caffeine<String, Person> caffeine = Caffeine.newBuilder()
.maximumWeight(30)
.expireAfter(new Expiry<String, Person>() {
@Override
public long expireAfterCreate(String s, Person person, long l) {
if(person.getAge() > 60){ //首次存入缓存后,年龄大于 60 的,过期时间为 4 秒
return 4000000000L;
}
return 2000000000L; // 否则为 2 秒
}
@Override
public long expireAfterUpdate(String s, Person person, long l, long l1) {
if(person.getName().equals("one")){ // 更新 one 这个人之后,过期时间为 8 秒
return 8000000000L;
}
return 4000000000L; // 更新其它人后,过期时间为 4 秒
}
@Override
public long expireAfterRead(String s, Person person, long l, long l1) {
return 3000000000L; // 每次被读取后,过期时间为 3 秒
}
})
.weigher((String key, Person value)-> value.getAge());
Cache<String, Person> cache = caffeine.build();
4.3.基于引用
不常用
// 当key和缓存元素都不再存在其他强引用的时候驱逐
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.weakKeys()
.weakValues()
.build(key -> createExpensiveGraph(key));
// 当进行GC的时候进行驱逐
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.softValues()
.build(key -> createExpensiveGraph(key));
Caffeine 允许你配置你的缓存去让GC去帮助清理缓存当中的元素,其中key支持弱引用,而value则支持弱引用和软引用。记住 AsyncCache
不支持软引用和弱引用。
Caffeine.weakKeys()
在保存key的时候将会进行弱引用。这允许在GC的过程中,当key没有被任何强引用指向的时候去将缓存元素回收。由于GC只依赖于引用相等性。这导致在这个情况下,缓存将会通过引用相等(==)而不是对象相等 equals()
去进行key之间的比较。
Caffeine.weakValues()
在保存value的时候将会使用弱引用。这允许在GC的过程中,当value没有被任何强引用指向的时候去将缓存元素回收。由于GC只依赖于引用相等性。这导致在这个情况下,缓存将会通过引用相等(==)而不是对象相等 equals()
去进行value之间的比较。
Caffeine.softValues()
在保存value的时候将会使用软引用。为了相应内存的需要,在GC过程中被软引用的对象将会被通过LRU算法回收。由于使用软引用可能会影响整体性能,我们还是建议通过使用基于缓存容量的驱逐策略代替软引用的使用。同样的,使用 softValues()
将会通过引用相等(==)而不是对象相等 equals()
去进行value之间的比较。
5.缓存移除
术语:
- 驱逐 缓存元素因为策略被移除
- 失效 缓存元素被手动移除
- 移除 由于驱逐或者失效而最终导致的结果
5.1.显式移除
在任何时候,你都可以手动去让某个缓存元素失效而不是只能等待其因为策略而被驱逐。
// 失效key
cache.invalidate(key)
// 批量失效key
cache.invalidateAll(keys)
// 失效所有的key
cache.invalidateAll()
示例:
/**
* 手动移除
*/
@Test
public void invalidate(){
LoadingCache<Integer, String> cache = Caffeine.newBuilder().maximumSize(101).build(key -> "无值");
for (int i = 0; i < 100; i++) {
cache.put(i,i+"");
}
cache.cleanUp();
long size = cache.estimatedSize();
System.out.println("size = " + size);
//移除单个元素
cache.invalidate(1);
cache.cleanUp();
size = cache.estimatedSize();
System.out.println("size = " + size);
//批量移除
ArrayList<Integer> keys = Lists.newArrayList();
for (int i = 0; i < 50; i++) {
keys.add(i);
}
cache.invalidateAll(keys);
//条目可能不准确,更新条目
cache.cleanUp();
size = cache.estimatedSize();
System.out.println("size = " + size);
//移除全部
cache.invalidateAll();
//条目可能不准确,更新条目
cache.cleanUp();
size = cache.estimatedSize();
System.out.println("size = " + size);
}
控制台打印:
size = 100
size = 99
size = 50
size = 0
5.2.移除监听器
你可以为你的缓存通过Caffeine.removalListener(RemovalListener)
方法定义一个移除监听器在一个元素被移除的时候进行相应的操作。这些操作是使用 Executor 异步执行的,其中默认的 Executor 实现是 ForkJoinPool.commonPool() 并且可以通过覆盖Caffeine.executor(Executor)
方法自定义线程池的实现。
当移除之后的自定义操作必须要同步执行的时候,你需要使用 Caffeine.evictionListener(RemovalListener)
。这个监听器将在 RemovalCause.wasEvicted()
为 true 的时候被触发。为了移除操作能够明确生效, Cache.asMap()
提供了方法来执行原子操作。
记住任何在 RemovalListener
中被抛出的异常将会被打印日志 (通过Logger)并被吞食。
Cache<Key, Graph> graphs = Caffeine.newBuilder()
.evictionListener((Key key, Graph graph, RemovalCause cause) ->
System.out.printf("Key %s was evicted (%s)%n", key, cause))
.removalListener((Key key, Graph graph, RemovalCause cause) ->
System.out.printf("Key %s was removed (%s)%n", key, cause))
.build();
示例:
/**
* 移除监听器
*/
@Test
public void invalidateListener(){
Cache<Integer, String> cache = Caffeine.newBuilder()
.maximumSize(10)
.removalListener((Integer key, String graph, RemovalCause cause) ->
System.out.printf("Key %s was removed (%s)%n", key, cause))
.build();
for (int i = 0; i < 15; i++) {
cache.put(i,i+"");
}
cache.cleanUp();
long size = cache.estimatedSize();
System.out.println("size = " + size);
//移除单个元素
cache.invalidate(1);
cache.cleanUp();
size = cache.estimatedSize();
System.out.println("size = " + size);
//批量移除
ArrayList<Integer> keys = Lists.newArrayList();
for (int i = 0; i < 50; i++) {
keys.add(i);
}
cache.invalidateAll(keys);
//条目可能不准确,更新条目
cache.cleanUp();
size = cache.estimatedSize();
System.out.println("size = " + size);
//移除全部
cache.invalidateAll();
//条目可能不准确,更新条目
cache.cleanUp();
size = cache.estimatedSize();
System.out.println("size = " + size);
}
控制台打印:
Key 0 was removed (SIZE)
Key 1 was removed (SIZE)
Key 2 was removed (SIZE)
Key 3 was removed (SIZE)
size = 10
Key 4 was removed (SIZE)
size = 10
Key 5 was removed (EXPLICIT)
Key 8 was removed (EXPLICIT)
Key 6 was removed (EXPLICIT)
Key 9 was removed (EXPLICIT)
Key 7 was removed (EXPLICIT)
Key 10 was removed (EXPLICIT)
size = 0
Key 14 was removed (EXPLICIT)
Key 13 was removed (EXPLICIT)
Key 12 was removed (EXPLICIT)
Key 11 was removed (EXPLICIT)
size = 0
6.缓存刷新
写操作完成后多久才将数据刷新进缓存中
与 expireAfterWrite
相反,refreshAfterWrite
将会使在写操作之后的一段时间后允许key对应的缓存元素进行刷新,但是只有在这个key被真正查询到的时候才会正式进行刷新操作。所以打个比方,你可以在同一个缓存中同时用到 refreshAfterWrite
和expireAfterWrite
,这样缓存元素的在被允许刷新的时候不会直接刷新使得过期时间被盲目重置。当一个元素在其被允许刷新但是没有被主动查询的时候,这个元素也会被视为过期。
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.maximumSize(10_000)
.refreshAfterWrite(1, TimeUnit.MINUTES)
.build(key -> createExpensiveGraph(key));
7.缓存统计
默认情况下,缓存的状态会用一个 CacheStats 对象记录下来,通过访问 CacheStats 对象就可以知道当前缓存的各种状态指标,那究竟有哪些指标呢?
先说一下什么是“加载”,当查询缓存时,缓存未命中,那就需要去第三方数据库中查询,然后将查询出的数据先存入缓存,再返回给查询者,这个过程就是加载。
totalLoadTime:总共加载时间。
loadFailureRate:加载失败率,= 总共加载失败次数 / 总共加载次数
averageLoadPenalty :平均加载时间,单位-纳秒
evictionCount:被淘汰出缓存的数据总个数
evictionWeight:被淘汰出缓存的那些数据的总权重
hitCount:命中缓存的次数
hitRate:命中缓存率
loadCount:加载次数
loadFailureCount:加载失败次数
loadSuccessCount:加载成功次数
missCount:未命中次数
missRate:未命中率
requestCount:用户请求查询总次数
示例:
@Test
public void recordStats(){
MyStatsCounter counter = new MyStatsCounter();
Cache<String, Stuedent> cache = Caffeine.newBuilder()
.maximumWeight(10)
.recordStats(()->counter)
.weigher((String key, Stuedent value)-> value.getAge())
.build();
Stuedent stuedent;
for (int i = 1; i < 6; i++) {
stuedent=new Stuedent("张三"+i,i*2);
cache.put("张三"+i,stuedent);
}
cache.getIfPresent("张三1");
cache.getIfPresent("张三");
CacheStats stats = counter.snapshot();
//等待结果
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("stats = " + stats);
}
自定义统计:
import com.github.benmanes.caffeine.cache.RemovalCause;
import com.github.benmanes.caffeine.cache.stats.CacheStats;
import com.github.benmanes.caffeine.cache.stats.StatsCounter;
import org.checkerframework.checker.index.qual.NonNegative;
import org.checkerframework.checker.nullness.qual.NonNull;
/**
* @author aaa
*/
public class MyStatsCounter implements StatsCounter {
@Override
public void recordHits(@NonNegative int count) {
System.out.println("命中次数:" + count);
}
@Override
public void recordMisses(@NonNegative int count) {
System.out.println("未命中次数:" + count);
}
@Override
public void recordLoadSuccess(@NonNegative long loadTime) {
System.out.println("加载成功次数:" + loadTime);
}
@Override
public void recordLoadFailure(@NonNegative long loadTime) {
System.out.println("加载失败次数:" + loadTime);
}
@Override
public void recordEviction() {
}
@Override
public void recordEviction(@NonNegative int weight, RemovalCause cause) {
System.out.println("因为缓存权重限制,执行了一次缓存清除工作,清除的数据的权重为"+weight+",原因:"+cause);
}
@Override
public @NonNull CacheStats snapshot() {
return null;
}
}
控制台打印:
命中次数:1
未命中次数:1
因为缓存权重限制,执行了一次缓存清除工作,清除的数据的权重为2,原因:SIZE
因为缓存权重限制,执行了一次缓存清除工作,清除的数据的权重为4,原因:SIZE
因为缓存权重限制,执行了一次缓存清除工作,清除的数据的权重为6,原因:SIZE
stats = null
8.缓存规范
CaffeineSpec
为Caffeine
提供了一个简单的字符格式配置。这里的字符串语法是一系列由逗号隔开的键值对组成,其中每个键值对对应一个配置方法。但是这里的字符配置不支持需要对象来作为参数的配置方法,比如 removalListener
,这样的配置必须要在代码中进行配置。
以下是各个配置键值对字符串所对应的配置方法。将maximumSize
和maximumWeight
或者将weakValues
和weakValues
在一起使用是不被允许的。
- initialCapacity=[integer]: 相当于配置
Caffeine.initialCapacity
- maximumSize=[long]: 相当于配置
Caffeine.maximumSize
- maximumWeight=[long]: 相当于配置
Caffeine.maximumWeight
- expireAfterAccess=[持续时间]: 相当于配置
Caffeine.expireAfterAccess
- expireAfterWrite=[持续时间]: 相当于配置
Caffeine.expireAfterWrite
- refreshAfterWrite=[持续时间]: 相当于配置
Caffeine.refreshAfterWrite
- weakKeys: 相当于配置
Caffeine.weakKeys
- weakValues: 相当于配置
Caffeine.weakValues
- softValues: 相当于配置
Caffeine.softValues
- recordStats: 相当于配置
Caffeine.recordStats
持续时间可以通过在一个integer类型之后跟上一个"d",“h”,“m”,或者"s"来分别表示天,小时,分钟或者秒。另外,ISO-8601标准的字符串也被支持来配置持续时间,并通过Duration.parse来进行解析。出于表示缓存持续时间的目的,这里不支持配置负的持续时间,并将会抛出异常。两种持续时间表示格式的示例如下所示。
普通 | ISO-8601 | 描述 |
---|---|---|
50s | PT50S | 50秒 |
11m | PT11M | 11分钟 |
6h | PT6H | 6小时 |
3d | P3D | 3天 |
P3DT3H4M | 3天3小时4分钟 | |
-PT7H3M | -7小时,-3分钟(不支持) |
CaffeineSpec spec = CaffeineSpec.parse(
"maximumWeight=1000, expireAfterWrite=10m, recordStats");
LoadingCache<Key, Graph> graphs = Caffeine.from(spec)
.weigher((Key key, Graph graph) -> graph.vertices().size())
.build(key -> createExpensiveGraph(key));
9.缓存清理
在默认情况下,当一个缓存元素过期的时候,Caffeine不会自动立即将其清理和驱逐。而它将会在写操作之后进行少量的维护工作,在写操作较少的情况下,也偶尔会在读操作之后进行。如果你的缓存吞吐量较高,那么你不用去担心你的缓存的过期维护问题。但是如果你的缓存读写操作都很少,可以像下文所描述的方式额外通过一个线程去通过Cache.cleanUp()
方法在合适的时候触发清理操作。
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.scheduler(Scheduler.systemScheduler())
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(key -> createExpensiveGraph(key));
Scheduler
可以提前触发过期元素清理移除。在过期事件之间进行调度,以期在短时间内最小化连续的批处理操作的数量。这里的调度是尽可能做到合理,并不能保证在一个元素过期的时候就将其清除。Java 9以上的用户可以通过Scheduler.systemScheduler()
来利用专用的系统范围内的调度线程。
Cache<Key, Graph> graphs = Caffeine.newBuilder().weakValues().build();
Cleaner cleaner = Cleaner.create();
cleaner.register(graph, graphs::cleanUp);
graphs.put(key, graph);
Java 9以上的用户也可以通过Cleaner去触发移除关于基于引用的元素(在使用了 weakKeys
, weakValues
, 或者 softValues
的情况下)。只要将key或者缓存的元素value注册到Cleaner
上,就可以在程序运行中调用Cache.cleanUp()
方法触发缓存的维护工作。
10.自定义策略
策略的选择在缓存的构造中是灵活可选的。在程序的运行过程中,这些策略的配置也可以被检查并修改。策略通过Optional表明当前缓存是否支持其该策略。
基于容量
cache.policy().eviction().ifPresent(eviction -> {
eviction.setMaximum(2 * eviction.getMaximum());
});
如果当前缓存容量是受最大权重所限制的,那么可以通过weightedSize()
方法获得当前缓存。这与Cache.estimatedSize()
区别在于,Cache.estimatedSize()
将会返回当前缓存中存在的元素个数。
缓存的最大容量或者总权重可以通过getMaximum()
得到并且可以通过setMaximum(long)
方法对其进行调整。缓存将会不断驱逐元素,直到符合最新的阈值。
如果想要得到缓存中最有可能被保留和最有可能被驱逐的元素子集,可以通过 hottest(int)
和coldest(int)
方法获得以上两个子集的元素快照。
基于时间
cache.policy().expireAfterAccess().ifPresent(expiration -> ...);
cache.policy().expireAfterWrite().ifPresent(expiration -> ...);
cache.policy().expireVariably().ifPresent(expiration -> ...);
cache.policy().refreshAfterWrite().ifPresent(refresh -> ...);
ageOf(key, TimeUnit)
方法提供了查看缓存元素在expireAfterAccess
,expireAfterWrite
或者 refreshAfterWrite
策略下的空闲时间的途径。缓存中的元素最大可持续时间可以通过getExpiresAfter(TimeUnit)
方法获取,并且可以通过setExpiresAfter(long, TimeUnit)
方法来进行调整。
如果需要查看最接近保留或者最接近过期的元素子集,那么需要调用 youngest(int)
和oldest(int)
方法来得到以上两个子集的元素快照。
11.缓存测试
FakeTicker ticker = new FakeTicker(); // Guava的测试库
Cache<Key, Graph> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.executor(Runnable::run)
.ticker(ticker::read)
.maximumSize(10)
.build();
cache.put(key, graph);
ticker.advance(30, TimeUnit.MINUTES)
assertThat(cache.getIfPresent(key), is(nullValue()));
当测试基于时间的驱逐策略的时候,不需要等着现实时间的推进。可以使用Ticker
接口和Caffeine.ticker(Ticker)
方法在你的缓存构造器中去定义一个自定义的时间源而不是等待系统时钟的转动。 为了这个目的,Guava的测试库提供了FakeTicker
。过期的元素将在周期性的维护中被清除,所以在测试基于驱逐策略的时候也可以直接使用 Cache.cleanUp()
方法来立即触发一次维护操作。
Caffeine 将通过Executor
来执行周期维护,移除通知和异步生成。这将对于调用者来说使得响应耗时变得更加可靠,并且线程池的默认实现是 ForkJoinPool.commonPool()。可以通过在缓存构造器中使用Caffeine.executor(Executor)
方法来指定一个直接(同线程)的executor 而不是等待异步任务执行完毕。
我们推荐使用Awaitility来进行多线程下的测试。