缓存加载与刷新
2026/1/15大约 4 分钟JavaGuava缓存本地缓存后端
缓存加载与刷新
CacheLoader 详解
CacheLoader 是 LoadingCache 的核心组件,定义了缓存未命中时的数据加载逻辑。
基本实现
CacheLoader<String, User> loader = new CacheLoader<String, User>() {
@Override
public User load(String userId) throws Exception {
// 从数据库加载
return userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException(userId));
}
};
LoadingCache<String, User> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.build(loader);Lambda 简化写法
LoadingCache<String, User> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.build(CacheLoader.from(userId -> userRepository.findById(userId).orElse(null)));批量加载
实现 loadAll 方法可以优化批量查询性能。
CacheLoader<String, User> loader = new CacheLoader<String, User>() {
@Override
public User load(String userId) throws Exception {
return userRepository.findById(userId).orElse(null);
}
@Override
public Map<String, User> loadAll(Iterable<? extends String> userIds) throws Exception {
// 批量查询,减少数据库访问次数
List<String> ids = Lists.newArrayList(userIds);
List<User> users = userRepository.findAllById(ids);
return users.stream()
.collect(Collectors.toMap(User::getId, Function.identity()));
}
};
// 批量获取
ImmutableMap<String, User> users = cache.getAll(Arrays.asList("1", "2", "3"));缓存获取方式
1. get(key) - 自动加载
// 缓存命中直接返回,未命中则调用 CacheLoader.load()
User user = cache.get("user-1");如果加载过程抛出异常,会包装成 ExecutionException 抛出。
2. getUnchecked(key) - 无检查异常
// 不抛出检查异常,适合 CacheLoader 不会抛出检查异常的场景
User user = cache.getUnchecked("user-1");3. getIfPresent(key) - 仅查询
// 只查询缓存,不触发加载
User user = cache.getIfPresent("user-1"); // 可能为 null4. get(key, callable) - 指定加载逻辑
// 使用指定的 Callable 加载,而非 CacheLoader
User user = cache.get("user-1", () -> {
return userRepository.findById("user-1").orElse(null);
});5. getAll(keys) - 批量获取
// 批量获取,会调用 loadAll(如果实现了)
ImmutableMap<String, User> users = cache.getAll(Arrays.asList("1", "2", "3"));缓存刷新
手动刷新
// 刷新单个 key
cache.refresh("user-1");
// 刷新所有
cache.asMap().keySet().forEach(cache::refresh);refresh 方法是异步的,会在后台重新加载数据,期间旧值仍然可用。
自动刷新
使用 refreshAfterWrite 配置自动刷新。
LoadingCache<String, User> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.refreshAfterWrite(1, TimeUnit.MINUTES) // 写入 1 分钟后刷新
.build(loader);异步刷新
默认的刷新是同步的,可以通过 asyncReloading 实现异步刷新。
ExecutorService executor = Executors.newFixedThreadPool(4);
CacheLoader<String, User> asyncLoader = CacheLoader.asyncReloading(
new CacheLoader<String, User>() {
@Override
public User load(String userId) {
return userRepository.findById(userId).orElse(null);
}
},
executor
);
LoadingCache<String, User> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.refreshAfterWrite(1, TimeUnit.MINUTES)
.build(asyncLoader);缓存写入
put - 手动写入
cache.put("user-1", new User("张三"));putAll - 批量写入
Map<String, User> users = new HashMap<>();
users.put("user-1", new User("张三"));
users.put("user-2", new User("李四"));
cache.putAll(users);invalidate - 删除缓存
// 删除单个
cache.invalidate("user-1");
// 批量删除
cache.invalidateAll(Arrays.asList("user-1", "user-2"));
// 清空所有
cache.invalidateAll();处理 null 值
Guava Cache 默认不允许缓存 null 值,如果 CacheLoader 返回 null,会抛出异常。
方案一:使用 Optional
LoadingCache<String, Optional<User>> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.build(new CacheLoader<String, Optional<User>>() {
@Override
public Optional<User> load(String userId) {
return Optional.ofNullable(userRepository.findById(userId).orElse(null));
}
});
// 使用
Optional<User> userOpt = cache.get("user-1");
User user = userOpt.orElse(null);方案二:使用空对象
public class User {
public static final User EMPTY = new User();
public boolean isEmpty() {
return this == EMPTY;
}
}
LoadingCache<String, User> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.build(new CacheLoader<String, User>() {
@Override
public User load(String userId) {
User user = userRepository.findById(userId).orElse(null);
return user != null ? user : User.EMPTY;
}
});
// 使用
User user = cache.get("user-1");
if (!user.isEmpty()) {
// 处理用户
}异常处理
加载异常
try {
User user = cache.get("user-1");
} catch (ExecutionException e) {
// 处理加载异常
Throwable cause = e.getCause();
if (cause instanceof UserNotFoundException) {
// 用户不存在
}
}使用 getUnchecked
// CacheLoader 不抛出检查异常时使用
LoadingCache<String, User> cache = CacheBuilder.newBuilder()
.build(CacheLoader.from(userId -> {
// 不抛出检查异常
return userRepository.findById(userId).orElse(User.EMPTY);
}));
User user = cache.getUnchecked("user-1");实战示例
配置缓存
@Component
public class ConfigCache {
private final LoadingCache<String, String> cache;
private final ConfigRepository configRepository;
public ConfigCache(ConfigRepository configRepository) {
this.configRepository = configRepository;
this.cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.refreshAfterWrite(1, TimeUnit.MINUTES)
.build(CacheLoader.asyncReloading(
CacheLoader.from(this::loadConfig),
Executors.newSingleThreadExecutor()
));
}
private String loadConfig(String key) {
return configRepository.findByKey(key)
.map(Config::getValue)
.orElse("");
}
public String get(String key) {
try {
return cache.get(key);
} catch (ExecutionException e) {
log.error("加载配置失败: key={}", key, e);
return "";
}
}
public void refresh(String key) {
cache.refresh(key);
}
public void invalidate(String key) {
cache.invalidate(key);
}
}小结
Guava Cache 提供了灵活的缓存加载和刷新机制,通过 CacheLoader 可以实现自动加载,通过 refreshAfterWrite 可以实现自动刷新。合理使用这些特性可以大大简化缓存管理代码。
面试题预览
常见面试题
- CacheLoader 的 load 和 loadAll 方法有什么区别?
- refresh 和 invalidate 有什么区别?
- 如何处理 Guava Cache 中的 null 值?
