Guava Cache面试题
2026/1/15大约 6 分钟JavaGuava缓存本地缓存后端
Guava Cache面试题
基础概念
1. Guava Cache 和 ConcurrentHashMap 有什么区别?
答案:
| 特性 | Guava Cache | ConcurrentHashMap |
|---|---|---|
| 过期策略 | 支持多种过期策略 | 不支持 |
| 容量限制 | 支持 maximumSize/Weight | 不支持 |
| 自动加载 | 支持 CacheLoader | 不支持 |
| 统计功能 | 支持命中率等统计 | 不支持 |
| 移除监听 | 支持 RemovalListener | 不支持 |
| 引用类型 | 支持弱引用/软引用 | 仅强引用 |
Guava Cache 是专门为缓存场景设计的,提供了丰富的缓存管理功能;ConcurrentHashMap 只是一个线程安全的 Map 实现。
2. Guava Cache 的过期策略有哪些?
答案:
expireAfterWrite:写入后固定时间过期
- 适合数据有固定有效期的场景
- 无论是否被访问都会过期
expireAfterAccess:最后访问后固定时间过期
- 适合热点数据缓存
- 频繁访问的数据不会过期
refreshAfterWrite:写入后固定时间刷新
- 不会删除旧数据,而是触发重新加载
- 刷新是惰性的,下次访问时才触发
CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.expireAfterAccess(5, TimeUnit.MINUTES)
.refreshAfterWrite(1, TimeUnit.MINUTES)
.build(loader);3. expireAfterWrite 和 refreshAfterWrite 有什么区别?
答案:
| 特性 | expireAfterWrite | refreshAfterWrite |
|---|---|---|
| 触发时机 | 写入后固定时间 | 写入后固定时间 |
| 行为 | 删除数据,下次访问重新加载 | 触发异步刷新,返回旧值 |
| 阻塞 | 加载时阻塞 | 不阻塞,返回旧值 |
| 适用场景 | 数据必须新鲜 | 允许短暂返回旧数据 |
4. Guava Cache 如何处理 null 值?
答案:
Guava Cache 默认不允许缓存 null 值,CacheLoader 返回 null 会抛出异常。
解决方案:
- 使用 Optional 包装
LoadingCache<String, Optional<User>> cache = CacheBuilder.newBuilder()
.build(CacheLoader.from(key -> Optional.ofNullable(findUser(key))));- 使用空对象模式
public static final User EMPTY = new User();
LoadingCache<String, User> cache = CacheBuilder.newBuilder()
.build(CacheLoader.from(key -> {
User user = findUser(key);
return user != null ? user : EMPTY;
}));缓存加载
5. CacheLoader 的 load 和 loadAll 方法有什么区别?
答案:
- load(key):加载单个 key 的数据,
get(key)时调用 - loadAll(keys):批量加载多个 key 的数据,
getAll(keys)时调用
CacheLoader<String, User> loader = new CacheLoader<String, User>() {
@Override
public User load(String key) {
return userRepository.findById(key);
}
@Override
public Map<String, User> loadAll(Iterable<? extends String> keys) {
// 批量查询,减少数据库访问
return userRepository.findAllById(keys);
}
};如果没有实现 loadAll,getAll 会多次调用 load 方法。
6. 如何实现缓存的异步刷新?
答案:
使用 CacheLoader.asyncReloading 包装 CacheLoader:
ExecutorService executor = Executors.newFixedThreadPool(4);
CacheLoader<String, User> asyncLoader = CacheLoader.asyncReloading(
new CacheLoader<String, User>() {
@Override
public User load(String key) {
return userRepository.findById(key);
}
},
executor
);
LoadingCache<String, User> cache = CacheBuilder.newBuilder()
.refreshAfterWrite(1, TimeUnit.MINUTES)
.build(asyncLoader);缓存淘汰
7. Guava Cache 的淘汰算法是什么?
答案:
Guava Cache 使用的是 LRU(Least Recently Used) 算法的变体。
当缓存达到容量限制时,会优先淘汰:
- 最近最少使用的数据
- 已过期的数据
实现原理:
- 内部使用分段锁(类似 ConcurrentHashMap)
- 每个段维护一个访问队列和写入队列
- 淘汰时从队列头部移除
8. RemovalCause 有哪些类型?
答案:
| 类型 | 说明 |
|---|---|
| EXPLICIT | 手动调用 invalidate 删除 |
| REPLACED | 被新值替换(put 相同 key) |
| COLLECTED | 被 GC 回收(弱引用/软引用) |
| EXPIRED | 过期被淘汰 |
| SIZE | 超出容量限制被淘汰 |
cache.removalListener(notification -> {
switch (notification.getCause()) {
case EXPLICIT:
// 手动删除
break;
case EXPIRED:
// 过期淘汰
break;
case SIZE:
// 容量淘汰
break;
}
});9. 为什么需要使用异步的 RemovalListener?
答案:
默认的 RemovalListener 是同步执行的,在缓存操作(get/put/invalidate)时触发。如果监听器执行耗时操作,会阻塞缓存操作,影响性能。
// 同步监听器 - 可能阻塞
RemovalListener<K, V> syncListener = notification -> {
// 耗时操作会阻塞缓存
saveToDatabase(notification.getValue());
};
// 异步监听器 - 不阻塞
RemovalListener<K, V> asyncListener = RemovalListeners.asynchronous(
notification -> {
saveToDatabase(notification.getValue());
},
executor
);性能与监控
10. 如何监控 Guava Cache 的命中率?
答案:
- 开启统计:
recordStats() - 获取统计信息:
cache.stats()
Cache<String, Object> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.recordStats() // 开启统计
.build();
// 获取统计
CacheStats stats = cache.stats();
double hitRate = stats.hitRate(); // 命中率
long hitCount = stats.hitCount(); // 命中次数
long missCount = stats.missCount(); // 未命中次数
long loadCount = stats.loadCount(); // 加载次数
double avgLoadTime = stats.averageLoadPenalty(); // 平均加载时间11. 缓存命中率低可能是什么原因?
答案:
缓存容量太小
- 频繁淘汰导致命中率低
- 解决:增加 maximumSize
过期时间太短
- 数据还没被重复访问就过期了
- 解决:增加过期时间
数据访问模式不适合缓存
- 数据访问分散,没有热点
- 解决:分析访问模式,只缓存热点数据
缓存 key 设计不合理
- key 粒度太细或太粗
- 解决:优化 key 设计
缓存预热不足
- 冷启动时命中率低
- 解决:应用启动时预热缓存
12. Guava Cache 是线程安全的吗?
答案:
是的,Guava Cache 是线程安全的。
实现原理:
- 内部使用分段锁(Segment),类似 ConcurrentHashMap
- 不同 key 可能落在不同的段,减少锁竞争
- 可以通过
concurrencyLevel配置并发级别
Cache<String, Object> cache = CacheBuilder.newBuilder()
.concurrencyLevel(16) // 并发级别,默认 4
.build();对比与选型
13. Guava Cache 和 Caffeine 如何选择?
答案:
| 特性 | Guava Cache | Caffeine |
|---|---|---|
| 性能 | 高 | 更高(约 2 倍) |
| API | 原版 | 兼容 Guava |
| 淘汰算法 | LRU | W-TinyLFU |
| 异步支持 | 有限 | 完善 |
| 维护状态 | 维护中 | 活跃 |
选择建议:
- 新项目推荐使用 Caffeine
- 已有 Guava Cache 的项目可以平滑迁移到 Caffeine
- Spring Boot 2.x 默认使用 Caffeine
14. 什么场景下应该使用本地缓存?
答案:
适合使用本地缓存的场景:
- 数据量小,可以放入内存
- 数据更新频率低
- 对一致性要求不高
- 需要极低的访问延迟
- 单机应用或数据不需要共享
不适合使用本地缓存的场景:
- 数据量大,超出内存限制
- 数据更新频繁
- 需要强一致性
- 分布式环境需要数据共享
- 需要持久化
实战问题
15. 如何解决缓存穿透问题?
答案:
缓存穿透:查询不存在的数据,每次都穿透到数据库。
解决方案:
- 缓存空值
LoadingCache<String, Optional<User>> cache = CacheBuilder.newBuilder()
.build(CacheLoader.from(key -> Optional.ofNullable(findUser(key))));- 布隆过滤器
BloomFilter<String> bloomFilter = BloomFilter.create(
Funnels.stringFunnel(Charset.defaultCharset()),
1000000,
0.01
);
public User getUser(String id) {
if (!bloomFilter.mightContain(id)) {
return null; // 一定不存在
}
return cache.get(id);
}16. 如何解决缓存雪崩问题?
答案:
缓存雪崩:大量缓存同时过期,导致请求全部打到数据库。
解决方案:
- 过期时间加随机值
int baseExpire = 10;
int randomExpire = new Random().nextInt(5);
cache.put(key, value);
// 过期时间 10-15 分钟- 使用 refreshAfterWrite
CacheBuilder.newBuilder()
.refreshAfterWrite(9, TimeUnit.MINUTES)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(asyncLoader);- 多级缓存
// L1: 本地缓存
// L2: Redis
public User getUser(String id) {
User user = localCache.getIfPresent(id);
if (user == null) {
user = redisCache.get(id);
if (user != null) {
localCache.put(id, user);
}
}
return user;
}小结
本章汇总了 Guava Cache 相关的常见面试题,涵盖基础概念、缓存加载、缓存淘汰、性能监控和实战问题。掌握这些知识点有助于深入理解 Guava Cache 的设计原理和最佳实践。
