LoadingCache基础
LoadingCacheDemo
LoadingCache是Guava中一个提供自动加载功能的缓存接口。它允许咱们通过一个CacheLoader来指定如何加载缓存。这就意味着,当咱们尝试从缓存中读取一个值,如果这个值不存在,LoadingCache就会自动调用预定义的加载机制去获取数据,然后将其加入到缓存中。
public class LoadingCacheDemo {
public static void main(String[] args) throws ExecutionException {
LoadingCache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(100) // 最大缓存项数
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
return "Hello, " + key; // 定义缓存加载的方式
}
});
// 使用缓存
printCacheContents(cache);
System.out.println(cache.getUnchecked("hello")); // 输出: HELLO
System.out.println(cache.getUnchecked("guava")); // 输出: GUAVA
printCacheContents(cache);
}
public static void printCacheContents(LoadingCache<String, String> cache) {
Map<String, String> cacheMap = cache.asMap();
if (cacheMap.isEmpty()) {
System.out.println("现在缓存里还没有内容====");
} else {
System.out.println("缓存内容如下内容====");
for (Map.Entry<String, String> entry : cacheMap.entrySet()) {
System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}
}
}
}
运行内容如下
直接打印缓存内容
现在缓存里还没有内容====
Hello, hello
Hello, guava
使用后打印缓存内容
缓存内容如下内容====
Key: hello, Value: Hello, hello
Key: guava, Value: Hello, guava
解释:在这个例子里,创建了一个简单的LoadingCache实例。通过getUnchecked
方法获取一个缓存项时,如果这个项不存在,CacheLoader会自动加载一个新值。在这里是简单地返回一个字符串。
LoadingCache详解
Google Guava 缓存工具使用详解_google loadingcache-CSDN博客
CacheBuilder类
CacheBuilder类是用于创建Guava缓存的构建器。可以使用该类的newBuilder()方法创建一个构建器实例,并通过一系列方法设置缓存的属性,例如最大容量、过期时间等。最后可以通过build()方法构建一个Cache实例。
部分方法详解:
initialCapacity:设置缓存的初始容量
这个方法将通过一个整数值设置缓存的初始大小。它是一个可选的方法,如果没有指定,缓存将采用默认的初始容量。
concurrencyLevel:设置并发级别
用于估计同时写入的线程数。这个方法将通过一个整数值设置并发级别,用于内部数据结构的调整,以提高并发写入的性能。它是一个可选的方法,缺省值为 4。
maximumSize:设置缓存的最大容量
这个方法将通过一个 long 类型的值设置缓存的最大容量。当缓存的条目数达到这个容量时,会触发缓存清除策略来移除一些条目以腾出空间。它是一个可选的方法,如果没有指定最大容量,缓存将不会有大小限制。
maximumWeight:设置缓存的最大权重
这个方法将通过一个 long 类型的值设置缓存的最大权重。权重可以根据缓存条目的大小计算,通常用于缓存对象大小不同的场景。当缓存的总权重达到这个值时,会触发缓存清除策略来移除一些条目以腾出空间。它是一个可选的方法,如果没有指定最大权重,缓存将不会有权重限制。
weigher:设置缓存的权重计算器
这个方法将通过一个实现了 Weigher 接口的对象设置缓存的权重计算器。通过权重计算器,可以根据缓存条目的键和值来计算它们的权重,以便在达到最大权重时触发缓存清除策略。它是一个可选的方法,如果没有设置权重计算器,缓存将不会有权重限制。
expireAfterWrite:设置写入后过期时间
这个方法通过一个 java.time.Duration 对象设置缓存条目的写入后过期时间。过期时间从最后一次写入条目开始计算。一旦超过指定的时间,条目将被认为是过期的并被清除。这是一个可选的方法,如果没有指定过期时间,条目将不会主动过期。
expireAfterAccess:设置访问后过期时间
这个方法通过一个 java.time.Duration 对象设置缓存条目的访问后过期时间。过期时间从最后一次访问条目开始计算。一旦超过指定的时间,条目将被认为是过期的并被清除。这是一个可选的方法,如果没有指定过期时间,条目将不会主动过期。
refreshAfterWrite:设置写入后自动刷新时间
这个方法通过一个 java.time.Duration 对象设置缓存条目的自动刷新时间。自动刷新时间从最后一次写入条目开始计算。一旦超过指定的时间,当条目被访问时,缓存将自动刷新该条目,即会调用 CacheLoader 的 load 方法重新加载该条目。这是一个可选的方法,如果没有设置自动刷新时间,条目将不会自动刷新。
recordStats():开启缓存统计信息记录
这个方法用于开启缓存的统计信息记录功能。一旦开启,可以通过 Cache.stats() 方法获取缓存的统计信息,如命中率、加载次数、平均加载时间等。这是一个可选的方法,如果不开启统计信息记录,将无法获取缓存的统计信息。
注意事项:
maximumSize与maximumWeight不能同时设置
设置maximumWeight时必须设置weigher
当缓存失效后,refreshAfterWrite设置的写入后自动刷新时间不会再有用
注意:expireAfterWrite、expireAfterAccess、refreshAfterWrite三个值的使用
开启recordStats后,才进行统计
CacheLoader类
CacheLoader 可以被视为一种从存储系统(如磁盘、数据库或远程节点)中加载数据的方法。CacheLoader 通常搭配refreshAfterWrite使用,在写入指定的时间周期后会调用CacheLoader 的load方法来获取并刷新为新值。
load 方法在以下情况下会被触发调用:
- 当设置了refreshAfterWrite(写入后自动刷新时间),达到自动刷新时间时,会调用reload 方法来重新加载该键的值,如果reload 方法违背重写,reload 的默认实现会调用 load 方法来重新加载该键的值。
- 调用 Cache.get(key) 方法获取缓存中指定键的值时,如果该键的值不存在,则会调用 load 方法来加载该键的值。
- 调用 Cache.get(key, callable) 方法获取缓存中指定键的值时,如果该键的值存在,则直接返回;如果该键的值不存在,则会调用 callable 参数指定的回调函数来计算并加载该键的值。
- 调用 Cache.getUnchecked(key) 方法获取缓存中指定键的值时,无论该键的值存在与否,都会调用 load 方法来加载该键的值。
需要注意的是:
当调用 load 方法加载缓存值时,可能会发生 IO 操作或其他耗时操作,因此建议在加载操作中使用异步方式来避免阻塞主线程。另外,加载操作的实现要考虑缓存的一致性和并发性,避免多个线程同时加载同一个键的值。
关于reload 方法:
reload 方法用于异步地刷新缓存值。它接收两个参数:key 和 oldValue,分别表示需要刷新的键以及该键之前对应的旧值。实现者需要通过在 reload 方法体内通过新的数据源或其他方式来重新加载和构建缓存数据,并在加载完成之后返回新的缓存值即可。通过异步地来更新缓存数据,让余下的请求可以同时从旧值中访问数据。
相对于 load 方法,reload 方法多了一个参数 oldValue。这是因为 reload 方法在执行时,缓存项可能已经过期了,它需要使用旧值来保证其它线程可以继续获得前一缓存项的值,避免出现缓存穿透的情况。同时,reload 方法需要保证异步刷新缓存的情况下线程安全。
CacheStats类
CacheStats 对象提供了诸如缓存命中率、加载缓存项数、缓存项回收数等统计信息的访问。
它可以通过 Cache.stats() 方法来获取,从而方便开发者监控缓存状态。
主要属性:

RemovalListener类
RemovalListener 用于在缓存中某个值被移除时执行相应的回调操作。
可以使用 CacheBuilder.removalListener() 方法为缓存设置 RemovalListener。
RemovalListener 的使用:
创建一个实现 RemovalListener 接口的类,实现 onRemoval 方法。这个方法会在缓存项被移除时被调用,接受两个参数: key 和 value。key 是被移除的缓存项的键,value 是被移除的缓存项的值。你可以根据需要在 onRemoval 方法中实现自定义的逻辑。
使用 CacheBuilder 的 removalListener 方法,将创建的 RemovalListener 对象传递给它。
缓存项被移除时,onRemoval 方法会自动被调用,方法会传入一个RemovalNotification 类型的参数,里面包含相应的 key 和 value等信息。你可以在这个方法中执行自定义的业务逻辑,例如日志记录、资源清理等操作。
RemovalNotification:
RemovalNotification 是 Guava 中用于表示缓存项被移除的通知的类。当在 Guava Cache 中注册了 RemovalListener 后,RemovalNotification 对象会在缓存项被移除时传递给 RemovalListener 的 onRemoval 方法。
RemovalNotification 包含了有关被移除缓存项的一些重要信息,例如键、值以及移除原因。下面是 RemovalNotification 类中常用的属性和方法:
getKey():获取被移除的缓存项的键。
getValue():获取被移除的缓存项的值。
getCause():获取移除原因,它是一个枚举类型RemovalCause,表示缓存项被移除的原因。
RemovalCause的可选值:
EXPLICIT:条目被显式删除,例如通过调用 Cache.invalidate(key) 方法。
REPLACED:条目被替换,例如通过调用 Cache.put(key, value) 方法重复放入相同的键。
EXPIRED:缓存条目由于达到了指定的过期时间而被移除。
SIZE:缓存条目由于超过了指定的大小限制而被移除。
COLLECTED:缓存条目被垃圾回收移除。这是在启用了缓存值的弱引用或软引用时发生的。
使用 RemovalNotification 可以让你在缓存项被移除时获取相关信息,并根据移除原因采取适当的处理措施。例如,你可以根据移除原因记录日志、执行清理操作、发送通知等。这样能够增强缓存的功能和可观察性。
注意事项:
RemovalListener 的 onRemoval 方法会在移除操作发生时同步调用,因此请确保不要在此方法中做耗时的操作,以免阻塞缓存的性能。
如果在缓存移除过程中抛出任何异常,它将被捕获并记录,不会影响缓存的正常运行。
需要注意的是,RemovalListener 只会在通过 Cache 的操作(如 invalidate、invalidateAll、put 进行替换)触发移除时被调用,并不会在缓存项因为过期失效而自动移除时被调用。
使用 RemovalListener 可以方便地在缓存项被移除时执行一些自定义的操作,例如清理相关资源、更新其他缓存或发送通知等。根据实际需要,合理利用 RemovalListener 可以增强缓存的功能和灵活性。
实际使用
自动加载和刷新机制
对于LoadingCache,当请求某个键的值时,如果这个值不存在或者需要刷新,LoadingCache会自动调用CacheLoader
去加载或刷新数据。
public class AutoRefreshCache {
public static void main(String[] args) throws Exception {
LoadingCache<String, String> cache = CacheBuilder.newBuilder()
.refreshAfterWrite(1, TimeUnit.MINUTES) // 设置1分钟后刷新
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) {
return fetchDataFromDatabase(key); // 模拟从数据库加载数据
}
});
System.out.println("First load: " + cache.get("key")); // 第一次加载
System.out.println("Waiting for 2 minute...");
Thread.sleep(TimeUnit.MINUTES.toMillis(2));
// 2分钟后,尝试再次获取,将触发刷新操作
System.out.println("Second load after refresh: " + cache.get("key"));
}
private static String fetchDataFromDatabase(String key) {
// 模拟数据库操作
return System.currentTimeMillis() + key;
}
}
运行结果
First load: 1720669175847key
Waiting for 2 minute...
Second load after refresh: 1720669295853key
异常处理
public class ExceptionHandlingCache {
public static void main(String[] args) throws Exception {
LoadingCache<String, String> cache = CacheBuilder.newBuilder()
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
if (!checkKey(key)) {
throw new Exception("Loading error");
}
return "Data for " + key;
}
});
getByKey(cache,"key");
getByKey(cache,"errorKey");
}
public static String getByKey(LoadingCache<String, String> cache, String key) {
try {
String value = cache.get(key);
System.out.println("Value for key \"" + key + "\": " + value);
return value;
} catch (Exception e) {
System.out.println("Error during cache load for key \"" + key + "\": " + e.getMessage());
return null;
}
}
public static boolean checkKey(String key) {
if ("errorKey".equals(key)){
return false;
}
return true;
}
}
输出结果
Value for key "key": Data for key
Error during cache load for key "errorKey": java.lang.Exception: Loading error
统计和监听功能
LoadingCache还提供了缓存统计和监听功能,这对于监控缓存性能和行为非常有用。
public class CacheMonitoring {
public static void main(String[] args) throws Exception {
// 创建Cache实例
LoadingCache<String, String> cache = CacheBuilder.newBuilder()
.initialCapacity(2) // 设置初始容量
.concurrencyLevel(4) // 设置并发级别
.maximumSize(5) // 设置最大容量
// .maximumWeight(1000) // 设置最大权重
// .weigher((Weigher<String, String>) (k, v) -> v.length()) // 设置权重计算器
.expireAfterWrite(10, TimeUnit.SECONDS) // 写入后x秒过期
.expireAfterAccess(20, TimeUnit.SECONDS) // 访问x秒过期
.refreshAfterWrite(5, TimeUnit.SECONDS) // 写入后自动刷新,
.recordStats() // 开启统计信息记录
.removalListener(notification -> { // 设置移除监听
// 缓存Key被移除时触发
String cause = "";
if (RemovalCause.EXPLICIT.equals(notification.getCause())) {
cause = "被显式移除";
} else if (RemovalCause.REPLACED.equals(notification.getCause())) {
cause = "被替换";
} else if (RemovalCause.EXPIRED.equals(notification.getCause())) {
cause = "被过期移除";
} else if (RemovalCause.SIZE.equals(notification.getCause())) {
cause = "被缓存条数超上限移除";
} else if (RemovalCause.COLLECTED.equals(notification.getCause())) {
cause = "被垃圾回收移除";
}
System.out.println(getCurrentFormattedTime() + " Key: " + notification.getKey() + " 移除了, 移除原因: " + cause);
})
.build(new CacheLoader<String, String>() { // 设置缓存重新加载逻辑
@Override
public String load(String key) {
// 重新加载指定Key的值
String newValue = "value" + (int)Math.random()*100;
System.out.println(getCurrentFormattedTime() + " Key: " + key + " 重新加载,新value:" + newValue);
return newValue;
}
});
// 将数据放入缓存
cache.put("key0", "value0");
cache.invalidate("key0");
cache.put("key1", "value1");
cache.put("key1", "value11");
cache.put("key2", "value22");
cache.put("key3", "value3");
cache.put("key4", "value4");
cache.put("key5", "value5");
cache.put("key6", "value6");
cache.put("key7", "value7");
cache.put("key8", "value8");
while (true) {
// 获取数据
System.out.println(getCurrentFormattedTime() + " get key1 value: " + cache.get("key1"));
// 统计信息
System.out.println(getCurrentFormattedTime() + " get stats: " + cache.stats());
Thread.sleep(3000);
}
}
public static String getCurrentFormattedTime() {
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
return now.format(formatter);
}
}
打印日志如下
2024-07-11 12:09:01 Key: key0 移除了, 移除原因: 被显式移除
2024-07-11 12:09:01 Key: key1 移除了, 移除原因: 被替换
2024-07-11 12:09:01 Key: key1 移除了, 移除原因: 被缓存条数超上限移除
2024-07-11 12:09:01 Key: key2 移除了, 移除原因: 被缓存条数超上限移除
2024-07-11 12:09:01 Key: key3 移除了, 移除原因: 被缓存条数超上限移除
2024-07-11 12:09:01 Key: key1 重新加载,新value:value0
2024-07-11 12:09:01 Key: key4 移除了, 移除原因: 被缓存条数超上限移除
2024-07-11 12:09:01 get key1 value: value0
2024-07-11 12:09:01 get stats: CacheStats{hitCount=0, missCount=1, loadSuccessCount=1, loadExceptionCount=0, totalLoadTime=951083, evictionCount=4}
2024-07-11 12:09:04 get key1 value: value0
2024-07-11 12:09:04 get stats: CacheStats{hitCount=1, missCount=1, loadSuccessCount=1, loadExceptionCount=0, totalLoadTime=951083, evictionCount=4}
2024-07-11 12:09:07 Key: key1 重新加载,新value:value0
2024-07-11 12:09:07 Key: key1 移除了, 移除原因: 被替换