缓存淘汰与监听
2026/1/15大约 4 分钟JavaGuava缓存本地缓存后端
缓存淘汰与监听
缓存淘汰策略
Guava Cache 支持多种缓存淘汰策略,当缓存达到限制条件时会自动淘汰数据。
1. 基于容量淘汰
maximumSize
当缓存条目数超过限制时,使用 LRU 算法淘汰最近最少使用的数据。
Cache<String, Object> cache = CacheBuilder.newBuilder()
.maximumSize(1000) // 最多 1000 个条目
.build();maximumWeight
基于权重淘汰,适合缓存对象大小不一的场景。
Cache<String, byte[]> cache = CacheBuilder.newBuilder()
.maximumWeight(1024 * 1024 * 100) // 最大 100MB
.weigher((String key, byte[] value) -> value.length) // 权重 = 字节数
.build();2. 基于时间淘汰
expireAfterWrite
写入后固定时间过期。
Cache<String, Object> cache = CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();expireAfterAccess
最后访问后固定时间过期。
Cache<String, Object> cache = CacheBuilder.newBuilder()
.expireAfterAccess(5, TimeUnit.MINUTES)
.build();3. 基于引用淘汰
weakKeys / weakValues
使用弱引用,当没有其他强引用时可被 GC 回收。
Cache<Object, Object> cache = CacheBuilder.newBuilder()
.weakKeys()
.weakValues()
.build();softValues
使用软引用,内存不足时被 GC 回收。
Cache<String, Object> cache = CacheBuilder.newBuilder()
.softValues()
.build();淘汰原因
Guava Cache 定义了 RemovalCause 枚举,表示缓存条目被移除的原因:
| 原因 | 说明 |
|---|---|
EXPLICIT | 手动调用 invalidate 删除 |
REPLACED | 被新值替换(put 相同 key) |
COLLECTED | 被 GC 回收(弱引用/软引用) |
EXPIRED | 过期被淘汰 |
SIZE | 超出容量限制被淘汰 |
移除监听器
RemovalListener
监听缓存条目被移除的事件。
RemovalListener<String, User> listener = notification -> {
String key = notification.getKey();
User value = notification.getValue();
RemovalCause cause = notification.getCause();
log.info("缓存移除: key={}, value={}, cause={}", key, value, cause);
// 根据不同原因处理
switch (cause) {
case EXPIRED:
// 过期处理
break;
case SIZE:
// 容量淘汰处理
break;
case EXPLICIT:
// 手动删除处理
break;
}
};
Cache<String, User> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.removalListener(listener)
.build();异步监听器
默认的监听器是同步执行的,可能影响缓存操作性能。使用异步监听器可以避免这个问题。
ExecutorService executor = Executors.newSingleThreadExecutor();
RemovalListener<String, User> asyncListener = RemovalListeners.asynchronous(
notification -> {
// 异步处理
log.info("缓存移除: key={}", notification.getKey());
},
executor
);
Cache<String, User> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.removalListener(asyncListener)
.build();监听器应用场景
1. 资源清理
RemovalListener<String, Connection> listener = notification -> {
Connection conn = notification.getValue();
if (conn != null && !conn.isClosed()) {
try {
conn.close();
log.info("关闭连接: {}", notification.getKey());
} catch (SQLException e) {
log.error("关闭连接失败", e);
}
}
};2. 数据同步
RemovalListener<String, User> listener = notification -> {
if (notification.getCause() == RemovalCause.EXPLICIT) {
// 手动删除时同步到其他节点
clusterService.invalidate(notification.getKey());
}
};3. 统计分析
RemovalListener<String, Object> listener = notification -> {
metrics.counter("cache.removal." + notification.getCause().name()).increment();
};手动淘汰
invalidate - 删除单个
cache.invalidate("user-1");invalidateAll - 批量删除
// 删除指定 keys
cache.invalidateAll(Arrays.asList("user-1", "user-2", "user-3"));
// 清空所有
cache.invalidateAll();cleanUp - 触发清理
Guava Cache 的过期清理是惰性的,只有在访问时才会检查过期。调用 cleanUp 可以主动触发清理。
// 主动触发过期清理
cache.cleanUp();可以配合定时任务使用:
@Scheduled(fixedRate = 60000) // 每分钟执行
public void cleanUpCache() {
cache.cleanUp();
}缓存视图
asMap - 获取 Map 视图
ConcurrentMap<String, User> map = cache.asMap();
// 遍历缓存
map.forEach((key, value) -> {
System.out.println(key + " -> " + value);
});
// 获取所有 key
Set<String> keys = map.keySet();
// 获取所有 value
Collection<User> values = map.values();
// 条件删除
map.entrySet().removeIf(entry -> entry.getValue().isExpired());注意
通过 asMap() 获取的视图不会触发缓存加载,也不会更新访问时间。
实战示例
带清理的连接池缓存
@Component
public class ConnectionCache {
private final Cache<String, Connection> cache;
public ConnectionCache() {
this.cache = CacheBuilder.newBuilder()
.maximumSize(100)
.expireAfterAccess(30, TimeUnit.MINUTES)
.removalListener(this::onRemoval)
.build();
}
private void onRemoval(RemovalNotification<String, Connection> notification) {
Connection conn = notification.getValue();
if (conn != null) {
try {
if (!conn.isClosed()) {
conn.close();
log.info("连接已关闭: key={}, cause={}",
notification.getKey(), notification.getCause());
}
} catch (SQLException e) {
log.error("关闭连接失败", e);
}
}
}
public Connection get(String key, Callable<Connection> loader) throws ExecutionException {
return cache.get(key, loader);
}
public void remove(String key) {
cache.invalidate(key);
}
@PreDestroy
public void destroy() {
// 应用关闭时清理所有连接
cache.invalidateAll();
}
}带统计的缓存淘汰
@Component
public class MonitoredCache<K, V> {
private final Cache<K, V> cache;
private final MeterRegistry meterRegistry;
public MonitoredCache(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.cache = CacheBuilder.newBuilder()
.maximumSize(10000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.recordStats()
.removalListener(this::recordRemoval)
.build();
}
private void recordRemoval(RemovalNotification<K, V> notification) {
meterRegistry.counter("cache.removal",
"cause", notification.getCause().name()
).increment();
}
public CacheStats getStats() {
return cache.stats();
}
}小结
Guava Cache 提供了多种缓存淘汰策略,包括基于容量、时间和引用的淘汰。通过 RemovalListener 可以监听缓存移除事件,实现资源清理、数据同步等功能。
面试题预览
常见面试题
- Guava Cache 的淘汰策略有哪些?
- RemovalCause 有哪些类型?分别代表什么?
- 为什么需要使用异步的 RemovalListener?
