过期、刷新与填充
什么是Caffeine
Springboot在2.0
之前,缓存组件集成的是Ehcache/Guava Cache。
Guava Cache是基于LRU机制实现的进程内缓存,而Ehcache则是纯Java实现的进程内缓存。
Caffeine在Guava Cache的基础上青出于蓝而胜于蓝
。
所以Springboot在2.0
之后,默认的缓存组件就是Caffeine,据说速度很快,性能很好,不管是读还是写,号称拿望远镜都看不到对手
。
可以把它当成是:可以自动过期的ConcurrentMap
,以及半持久化的Redis(需要手动写入外部资源)。

引入依赖。
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
先运行试试效果。
package com.xiangwang.commons.caffeine;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import java.util.concurrent.TimeUnit;
public class CaffeineTest01 {
public static void main(String[] args) throws InterruptedException {
LoadingCache<String, Object> caffeine = Caffeine.newBuilder()
.initialCapacity(1)
.maximumSize(100)
.expireAfterWrite(1, TimeUnit.MINUTES)
.build(key -> {
// 当get(key)无值时返回
return "default";
});
caffeine.put("username", "lixingyun");
System.out.println(caffeine.get("username"));
System.out.println(caffeine.get("1"));
}
}
initialCapacity(1)
:定义初始缓存条数。maximumSize(100)
:定义最大缓存条数。expireAfterWrite
:最后一次写入多久后缓存过期。build(CacheLoader<? super K1, V1> loader)
:自定义缓存加载方式,如指定一个默认值。
过期策略
所有的缓存都有数据过期的清理机制,Caffeine也一样。
Caffeine有这么几种过期策略。
基于大小
:数量大小与权重大小(指定最大权重后,过期会删除最小的)基于时机
:expireAfterWrite、expireAfterAccess、expireAfter基于引用
:强引用、软引用、弱引用。
还有一种无界模式
,其实就是去掉了基于时机
的过期机制,但仍然会受其他过期策略的制约,如大小和引用。
基于数量大小
package com.xiangwang.commons.caffeine;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import java.util.concurrent.TimeUnit;
/**
* 过期策略:基于数量大小
*
*/
public class CaffeineTest02 {
public static void main(String[] args) throws InterruptedException {
LoadingCache<String, Object> caffeine = Caffeine.newBuilder()
// 基于数量大小
.maximumSize(1)
.build(key -> {
// 当get(key)无值时返回
return "default";
});
caffeine.put("A", "v1");
TimeUnit.SECONDS.sleep(1);
caffeine.put("B", "v1");
TimeUnit.SECONDS.sleep(1);
caffeine.put("C", "v1");
TimeUnit.SECONDS.sleep(1);
caffeine.put("D", "v1");
TimeUnit.SECONDS.sleep(1);
System.out.println(caffeine.estimatedSize());
System.out.println(caffeine.get("A"));
System.out.println(caffeine.get("D"));
}
}
基于权重大小
package com.xiangwang.commons.caffeine;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import java.util.concurrent.TimeUnit;
/**
* 过期策略:基于权重大小
*
*/
public class CaffeineTest03 {
public static void main(String[] args) throws InterruptedException {
LoadingCache<String, Object> caffeine = Caffeine.newBuilder()
// 基于权重大小
.maximumWeight(1)
.weigher((k, v) -> k.toString().length())
.build(key -> {
// 当get(key)无值时返回
return "default";
});
caffeine.put("A", "v1");
TimeUnit.SECONDS.sleep(1);
caffeine.put("B", "v1");
TimeUnit.SECONDS.sleep(1);
caffeine.put("C", "v1");
TimeUnit.SECONDS.sleep(1);
caffeine.put("D", "v1");
TimeUnit.SECONDS.sleep(1);
System.out.println(caffeine.estimatedSize());
System.out.println(caffeine.get("A"));
System.out.println(caffeine.get("D"));
}
}
基于expireAfterAccess
基于时机(expireAfterAccess)
的过期策略会在最后一次读取并经过指定时间后失效,但如果一直访问就不会失效。
package com.xiangwang.commons.caffeine;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import java.util.concurrent.TimeUnit;
/**
* 过期策略:基于时机(expireAfterAccess)
*
*/
public class CaffeineTest04 {
public static void main(String[] args) throws InterruptedException {
LoadingCache<String, Object> caffeine = Caffeine.newBuilder()
.maximumSize(1)
// 最后一次读取并经过指定时间后失效,如果一直访问就不会失效
// 指定时间不访问后就失效
.expireAfterAccess(4, TimeUnit.SECONDS)
.build(key -> {
// 当get(key)无值时返回
return "default";
});
System.out.println("开始 ----> ");
caffeine.put("test", 1);
TimeUnit.SECONDS.sleep(3);
System.out.println("3秒后 ----> " + caffeine.get("test"));
TimeUnit.SECONDS.sleep(3);
System.out.println("3秒后 ----> " + caffeine.get("test"));
TimeUnit.SECONDS.sleep(1);
System.out.println("1秒后 ----> " + caffeine.get("test"));
TimeUnit.SECONDS.sleep(5);
System.out.println("5秒后 ----> " + caffeine.get("test"));
}
}
运行后输出如下。
开始 ---->
3秒后 ----> 1
3秒后 ----> 1
1秒后 ----> 1
5秒后 ----> default
基于expireAfterWrite
基于时机(expireAfterWrite)
的过期策略会在最后一次写入并经过指定时间后失效,但如果一直访问就不会失效。
同样的代码,仅仅是将expireAfterAccess
改为expireAfterWrite
效果就不同。
package com.xiangwang.commons.caffeine;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import java.util.concurrent.TimeUnit;
/**
* 过期策略:基于时机(expireAfterWrite)
*
*/
public class CaffeineTest05 {
public static void main(String[] args) throws InterruptedException {
LoadingCache<String, Object> caffeine = Caffeine.newBuilder()
.maximumSize(1)
// 最后一次写入并经过指定时间后失效,如果一直访问就不会失效
// 指定时间不访问后就失效
.expireAfterWrite(4, TimeUnit.SECONDS)
.build(key -> {
// 当get(key)无值时返回
return "default";
});
System.out.println("开始 ----> ");
caffeine.put("test", 1);
TimeUnit.SECONDS.sleep(3);
System.out.println("3秒后 ----> " + caffeine.get("test"));
TimeUnit.SECONDS.sleep(3);
System.out.println("3秒后 ----> " + caffeine.get("test"));
TimeUnit.SECONDS.sleep(1);
System.out.println("1秒后 ----> " + caffeine.get("test"));
TimeUnit.SECONDS.sleep(5);
System.out.println("5秒后 ----> " + caffeine.get("test"));
}
}
运行后输出如下。
开始 ---->
3秒后 ----> 1
3秒后 ----> default
1秒后 ----> default
5秒后 ----> default
基于expireAfter
最后一个基于时机(expireAfter)
的过期策略和前两个是互斥的,代码如下。
package com.xiangwang.commons.caffeine;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.Expiry;
import com.github.benmanes.caffeine.cache.LoadingCache;
import org.checkerframework.checker.index.qual.NonNegative;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.concurrent.TimeUnit;
/**
* 过期策略:基于时机(expireAfterWrite)
*
*/
public class CaffeineTest06 {
public static void main(String[] args) throws InterruptedException {
LoadingCache<String, String> caffeine = Caffeine.newBuilder()
.maximumSize(1)
// 自定义不同的过期策略
.expireAfter(new Expiry<String, String>() {
// 创建之后2秒过期
@Override
public long expireAfterCreate(@NonNull String key, @NonNull String value, long currentTime) {
return currentTime + 2000;
}
@Override
public long expireAfterUpdate(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long duration) {
return 0;
}
@Override
public long expireAfterRead(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long duration) {
return 0;
}
})
.build(key -> {
// 当get(key)无值时返回
return "default";
});
caffeine.put("username", "lixingyun");
TimeUnit.SECONDS.sleep(1);
System.out.println(caffeine.get("username"));
TimeUnit.SECONDS.sleep(2);
System.out.println(caffeine.get("username"));
}
}
运行后输出如下。
lixingyun
default
基于引用
基于引用的过期策略和JVM GC关联比较紧密,强引用对象不会轻易被JVM回收。
这种过期策略用得不多,且大多情况下都是强引用,否则缓存就失去意义了。
刷新策略
Caffeine的刷新策略有如下几种。
基于类
Caffeine.refreshAfterWrite(time, duration)
方法。基于接口
LoadingCache的refresh(key)
方法。基于
CacheLoader的reload(K, V)
。
refreshAfterWrite()方法
package com.xiangwang.commons.caffeine;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import java.util.concurrent.TimeUnit;
/**
* 刷新策略:Caffeine.refreshAfterWrite()方法实现更新
*
*/
public class CaffeineTest07 {
public static void main(String[] args) throws InterruptedException {
LoadingCache<String, String> caffeine = Caffeine.newBuilder()
.initialCapacity(1)
.maximumSize(100)
.expireAfterWrite(1, TimeUnit.DAYS)
// 写后2秒刷新
.refreshAfterWrite(2, TimeUnit.SECONDS)
.build(key -> {
// 当get(key)无值时返回
return "default";
});
caffeine.put("username", "lixingyun");
// 写后不到1秒
System.out.println("不到1秒 ----> " + caffeine.get("username"));
// 休眠3秒
TimeUnit.SECONDS.sleep(3);
System.out.println("3秒后第一次访问 ----> " + caffeine.get("username"));
System.out.println("3秒后第二次访问 ----> " + caffeine.get("username"));
}
}
运行后输出如下。
不到1秒 ----> lixingyun
3秒后第一次访问 ----> lixingyun
3秒后第二次访问 ----> default
从运行结果可以看到,refreshAfterWrite()
方法是一种被动
更新。
必须设置
CacheLoad
,key
过期后并不立即刷新value
。当过期后第一次调用
get()
方法时,得到的仍然是过期值。当过期后第二次调用
get()
方法时,才会得到更新后的值。
refresh()方法
package com.xiangwang.commons.caffeine;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import java.util.concurrent.TimeUnit;
/**
* 刷新策略:LoadingCache.refresh()方法实现更新
*
*/
public class CaffeineTest08 {
private static String value = "lixingyun";
public static String getValue() {
return value;
}
public static void setValue(String value) {
CaffeineTest08.value = value;
}
public static void main(String[] args) throws InterruptedException {
LoadingCache<String, Object> caffeine = Caffeine.newBuilder()
.initialCapacity(1)
.maximumSize(1)
.expireAfterWrite(1, TimeUnit.MINUTES)
.build(key -> {
// 从外部加载数据
return getValue();
});
System.out.println("更新前:" + caffeine.get("username"));
setValue("wanglin");
System.out.println("已更新但未刷新:" + caffeine.get("username"));
caffeine.refresh("username");
System.out.println("已刷新:" + caffeine.get("username"));
}
}
运行后输出如下。
更新前:lixingyun
已更新但未刷新:lixingyun
已刷新:wanglin
只有
LoadingCache
接口才有refresh(Key)
方法。只要调用了,当再次读取时,就是最新的值(和
refreshAfterWrite()
方法的区别)。注意
get(Key)
和getIfPresent(Key)
的区别:get(Key)
在未显式设置Key
时,会从CacheLoader.load()
方法取值,而getIfPresent(Key)
不会。
reload(K, V)方法
package com.xiangwang.commons.caffeine;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.concurrent.TimeUnit;
/**
* 刷新策略:CacheLoader.reload(K, V)方法实现更新
*
*/
public class CaffeineTest09 {
private static String value = "lixingyun";
public static String getValue() {
return value;
}
public static void setValue(String value) {
CaffeineTest09.value = value;
}
public static void main(String[] args) throws InterruptedException {
/*
* Cache覆盖CacheLoader.reload(K, V)
*
*/
Cache<String, String> cache1 = Caffeine.newBuilder()
.initialCapacity(1)
.maximumSize(1)
.expireAfterWrite(1, TimeUnit.MINUTES)
// new CacheLoader<String, String>()配合refreshAfterWrite()一起使用
.refreshAfterWrite(2, TimeUnit.SECONDS)
.build(new CacheLoader<String, String>() {
@Override
public @Nullable String load(@NonNull String s) throws Exception {
return getValue();
}
@Override
public @Nullable String reload(@NonNull String key, @NonNull String oldValue) throws Exception {
return getValue();
}
});
cache1.put("username", "lixingyun");
System.out.println("更新前:" + cache1.get("username", key -> "default"));
setValue("wanglin");
TimeUnit.SECONDS.sleep(1);
System.out.println("已刷新第一次访问:" + cache1.get("username", key -> "default"));
System.out.println("已刷新第二次访问:" + cache1.get("username", key -> "default"));
TimeUnit.SECONDS.sleep(2);
System.out.println("已刷新且超时后第一次访问:" + cache1.get("username", key -> "default"));
System.out.println("已刷新且超时后第二次访问:" + cache1.get("username", key -> "default"));
System.out.println("========================");
/*
* LoadingCache覆盖CacheLoader.reload(K, V)
*
*/
LoadingCache<String, String> cache2 = Caffeine.newBuilder()
.initialCapacity(1)
.maximumSize(1)
.expireAfterWrite(1, TimeUnit.MINUTES)
// 去掉refreshAfterWrite()
.build(new CacheLoader<String, String>() {
@Override
public @Nullable String load(@NonNull String s) throws Exception {
return getValue();
}
@Override
public @Nullable String reload(@NonNull String key, @NonNull String oldValue) throws Exception {
return getValue();
}
});
// 如果仅使用cache2.get("username"),那么会返回“lixingyun”,但这里使用的是key -> "default"
System.out.println("更新前:" + cache2.get("username", key -> "default"));
setValue("wanglin");
System.out.println("已更新但未刷新:" + cache2.get("username", key -> "default"));
cache2.refresh("username");
System.out.println("已刷新:" + cache2.get("username", key -> "default"));
}
}
运行后输出如下。
更新前:lixingyun
已刷新第一次访问:lixingyun
已刷新第二次访问:lixingyun
已刷新且超时后第一次访问:lixingyun
已刷新且超时后第二次访问:wanglin
========================
更新前:default
已更新但未刷新:default
已刷新:wanglin
CacheLoader.reload(K, V)
需要配合其他两种方式一起使用。可以比较使用不同的
Cache
,看看其中的细微差别。使用不同的
Cache
涉及到Caffeine另一个方面:缓存填充。
填充策略
所谓填充策略
就是将数据保存到缓存的方式,Caffeine提供了三种填充
方式:手动
、同步
和异步
。
package com.xiangwang.commons.caffeine;
import com.github.benmanes.caffeine.cache.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/**
* 填充策略:手动、同步和异步
*
*/
public class CaffeineTest10 {
public static void main(String[] args) throws InterruptedException, ExecutionException {
/*
* 手动填充
*
*/
Cache<String, String> cache1 = Caffeine.newBuilder()
.initialCapacity(1)
.maximumSize(1)
.expireAfterWrite(1, TimeUnit.MINUTES)
.build(key -> "default");
cache1.put("username", "lixingyun");
System.out.println("手动填充:" + cache1.getIfPresent("username"));
System.out.println(cache1.get("username", key -> "default"));
System.out.println(cache1.get("new_key", key -> "default"));
System.out.println("========================");
/*
* 同步填充
*
*/
LoadingCache<String, String> cache2 = Caffeine.newBuilder()
.initialCapacity(1)
.maximumSize(1)
.expireAfterWrite(1, TimeUnit.MINUTES)
.build(key -> "default");
cache2.put("username", "lixingyun");
System.out.println("同步填充:" + cache2.getIfPresent("username"));
System.out.println(cache2.get("username", key -> "default"));
System.out.println(cache2.get("new_key", key -> "default"));
System.out.println("========================");
/*
* 异步填充
*
*/
AsyncLoadingCache<String, String> cache3 = Caffeine.newBuilder()
.initialCapacity(1)
.maximumSize(1)
.expireAfterWrite(1, TimeUnit.MINUTES)
.buildAsync(key -> "default");
cache3.put("username", CompletableFuture.supplyAsync(() -> {
return "lixingyun";
}));
System.out.println("异步填充:" + cache3.getIfPresent("username").get());
System.out.println(cache3.get("username", key -> "default").get());
System.out.println(cache3.get("new_key", key -> "default").get());
}
}
运行后输出如下。
手动填充:lixingyun
lixingyun
default
========================
同步填充:lixingyun
lixingyun
default
========================
异步填充:lixingyun
lixingyun
default
可以看到三种方式所得到的结果都一样。
手动方式可以显式地控制检索、更新和删除条目,控制起来比较方便,适用于大多数场景。
异步方式由于需要使用组合异步
CompletableFuture
,很明显是用于保存需要占用较大内存的对象,且这类保存操作比较费时。同步方式介于手动和异步之间,适用于需要从外部加载数据源,或者需要保存全局变量的情况(其实
get(key, k -> v)
方法更适合)。同步方式对
refresh(Key)
方法支持得很好,而且get()
、get(key, k -> v)
和getIfPresent()
对结果也有影响。通过
不同的填充策略
+不同的get()方法
+不同的刷新策略
,就会产生不同的结果,这需要开发者对Caffeine有足够多的使用和了解,才能完全驾驭。
另外,不管是哪种填充方式,都有get(key, k -> v)
方法,而且get(key, k -> v)
方法以阻塞方式调用,在多个线程环境下,它只会调用一次,这可以避免与其他线程的写入竞争,因此get(key, k -> v)
的执行效率要优于getIfPresent()
。
因此,建议使用get(key, k -> v)
方法,更安全,更通用。
Caffeine几乎完全继承自Guava Cache(除了TinyLFU),它就是翻版的Guava Cache。
感谢支持
更多内容,请移步《超级个体》。