引言
凡是用过缓存的相信都有一个痛点,那就是在查询缓存的时候都需要写大量的判断,当缓存不存在的时候我们是需要去数据库进行查询的,然后查询的结果还要再来丢进缓存里面,烦不胜烦,要是手误一下,还可能代码没闭环,给自己挖一个坑。
一、一般情况缓存的查询使用方法
在日常开发中,我们通常会先检查缓存中是否存在目标数据,如果不存在,再从数据库或其他数据源加载数据并存入缓存,以供后续使用。
时序图如下:
sequenceDiagram
participant User
participant Service
participant Cache
participant Database
Note over User,Database: 通过缓存查询用户数据
User->>+Service: 查询数据
Service->>+Cache: 查询数据
Cache-->>-Service: 返回查询结果
alt 命中缓存
Service-->>-User: 返回查询结果给调用者
end
alt 未命中缓存
Service->>+Database: 查询数据
Database-->>-Service: 返回数据
Service->>+Cache: 将数据写入缓存
Cache-->>-Service: 数据写入成功
Service-->>+User: 返回查询结果给调用者
end
实现代码如下:
public User getUser(String id) {
User user = RedisUtils.getInstance().get(id, User.class);
if (Objects.isNull(user)) {
user = mapper.selectByPrimaryKey(id);
if (Objects.nonNull(user)) {
RedisUtils.getInstance().put(id, user);
}
}
return user;
}
类似上面这种,每一个查询的地方,我们都要做对应的判断,实际可以看到代码还是比较麻烦的。这种方式冗余且容易出错。
而且上面还只是查询的的代码,实际开发中,我们还要保证缓存的一致性,需要自己去写刷新缓存的代码。
小结:传统缓存管理存在两个痛点:手动加载相当繁琐、缓存过期与刷新相当复杂
二、认识LoadingCache
LoadingCache 是 Google Guava 提供的一个强大的缓存接口,它继承自Cache接口,能够自动加载和管理缓存数据。它通过 CacheLoader 来指定加载逻辑,当缓存中不存在请求的键时,会自动调用 CacheLoader 的 load 方法加载数据。
2.1 LoadingCache的特性
-
自动加载:当请求的键不存在于缓存中时,
LoadingCache会自动调用CacheLoader的load方法来加载数据,并将其存储到缓存中。 -
缓存过期策略:支持多种过期策略,例如基于时间的过期(
expireAfterAccess访问时间、expireAfterWrite写入时间)和基于大小的过期(maximumSize)。 -
自动刷新:通过
refreshAfterWrite方法,可以设置缓存项在写入后经过一定时间自动刷新。 -
并发支持:支持高并发访问,可通过
concurrencyLevel参数设置并发级别。 -
缓存统计:通过
recordStats方法,可以开启缓存统计功能,获取命中率、加载时间等统计信息。 -
监听器:支持
RemovalListener,可以在缓存项被移除时执行特定操作。
2.2 LoadingCache的创建和使用
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import java.util.concurrent.TimeUnit;
public class Demo {
public static void main(String[] args) throws Exception {
// 创建 LoadingCache
LoadingCache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(1024 * 1024) // 设置最大缓存项数
.expireAfterWrite(8, TimeUnit.HOURS) // 设置写入后 8 小时过期
.refreshAfterWrite(90, TimeUnit.MINUTES) // 设置写入后 90 分钟刷新
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
return fetchDataFromDatabase(key); // 模拟从数据库加载数据
}
});
// 使用缓存
System.out.println(cache.get("key1")); // 第一次加载
}
private static String fetchDataFromDatabase(String key) {
// 模拟数据库操作
return "Data for " + key;
}
}
在上面的案例中
-
CacheLoader的 load 方法定义了如何加载数据。 -
maximumSize 设置了缓存的最大项数。
-
expireAfterWrite 设置了缓存项的过期时间。
-
refreshAfterWrite 设置了缓存项的刷新时间。
三、 LoadingCache 的智能化特性
简单认识了,回归正文,文章标题就说了 LoadingCache 让缓存操作更智能,那么到底怎么智能呢?下面我们一起来看看LoadingCache有那些智能化特性。
3.1 自动加载缓存数据
LoadingCache 通过 CacheLoader 自动加载缓存数据,无需手动检查缓存是否存在。减少代码量,降低出错风险,提升开发效率。
LoadingCache<String, String> cache = CacheBuilder.newBuilder()
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
// 自动加载逻辑...注意,这里直接就是从数据源里面去加载数据,不需要做空判断
return fetchDataFromDatabase(key);
}
});
3.2 自动刷新缓存
LoadingCache 通过 refreshAfterWrite 方法,LoadingCache 可以在缓存项写入后经过一定时间自动刷新。确保缓存数据的时效性,无需手动更新缓存。
LoadingCache<String, String> cache = CacheBuilder.newBuilder()
.refreshAfterWrite(5, TimeUnit.MINUTES) // 5分钟后自动刷新...这个只是案例,实际应用要根据业务场景自己定义刷新时间
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
return fetchDataFromDatabase(key);
}
});
3.3 异常处理与容错机制
LoadingCache 在加载数据时可能会抛出异常,但可以通过 RemovalListener 或其他机制进行容错处理。提高系统的健壮性,避免因加载失败导致的系统崩溃。
LoadingCache<String, String> cache = CacheBuilder.newBuilder()
.removalListener(new RemovalListener<String, String>() {
@Override
public void onRemoval(RemovalNotification<String, String> notification) {
if (notification.getCause() == RemovalCause.EXPIRED) {
System.out.println("Cache entry expired: " + notification.getKey());
}
}
})
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
return fetchDataFromDatabase(key);
}
});
注意:我看到有部分文章将统计与监控写成了LoadingCache的智能特性,不能说错,但是实际这些是继承自他的父类的,不是他自己私有的特性。
四、总结
LoadingCache 的核心优势主要在于自动加载、自动刷新、异常处理这些,其在简化开发和程序健壮方面的作用非常明显。
可以说它非常优雅的解决了传统缓存管理存在两个痛点:手动加载相当繁琐和缓存过期与刷新相当复杂。
注意:本文并没有写缓存的应用场景,这个能找到这篇文章的应该都非常熟悉了,就不做额外介绍。
另外要了解他的父类,可以阅读我的另外一篇文章# 好用的Java内存缓存库,Guava Cache
最后:如果你从本篇文章中学到一点点东西,麻烦发财的小手点一下赞,如有疑问或者错误,欢迎提出和指正。