Caffeine面试题
2026/1/15大约 6 分钟JavaCaffeine缓存本地缓存后端
Caffeine面试题
基础概念
1. Caffeine 和 Guava Cache 有什么区别?
答案:
| 特性 | Caffeine | Guava Cache |
|---|---|---|
| 性能 | 更高(约2倍) | 高 |
| 淘汰算法 | W-TinyLFU | LRU |
| 异步支持 | 原生支持 AsyncCache | 有限支持 |
| API | 兼容 Guava | 原版 |
| Spring 集成 | 默认实现 | 需要配置 |
| 自定义过期 | 支持 expireAfter | 不支持 |
| 维护状态 | 活跃 | 维护中 |
主要区别:
- 性能:Caffeine 使用更高效的数据结构和算法
- 淘汰算法:W-TinyLFU 比 LRU 命中率更高
- 异步支持:Caffeine 原生支持异步加载和刷新
2. W-TinyLFU 算法的原理是什么?
答案:
W-TinyLFU 是 Window Tiny Least Frequently Used 的缩写,结合了 LRU 和 LFU 的优点。
核心组件:
- Window Cache(1%):新数据进入的窗口区域,使用 LRU
- Main Cache(99%):主缓存区域
- Protected(80%):保护区,存放高频数据
- Probation(20%):观察区,存放待观察数据
- TinyLFU:使用 Count-Min Sketch 统计访问频率
工作流程:
- 新数据进入 Window Cache
- Window 满时,候选者与 Probation 受害者比较频率
- 频率高的进入 Main Cache,低的被淘汰
- Probation 中被访问的数据晋升到 Protected
3. Count-Min Sketch 是什么?
答案:
Count-Min Sketch 是一种概率数据结构,用于估算元素的频率。
特点:
- 空间效率高:使用位数组,内存占用极小
- 查询快速:O(1) 时间复杂度
- 可能高估:由于哈希冲突,频率可能被高估,但不会低估
- 定期衰减:通过右移操作衰减所有计数器
// 增加频率
void increment(key) {
for (int i = 0; i < depth; i++) {
int index = hash(key, i) % width;
table[i][index]++;
}
}
// 查询频率(取最小值)
int frequency(key) {
int min = Integer.MAX_VALUE;
for (int i = 0; i < depth; i++) {
int index = hash(key, i) % width;
min = Math.min(min, table[i][index]);
}
return min;
}4. 为什么 Caffeine 的性能比 Guava Cache 高?
答案:
更好的淘汰算法
- W-TinyLFU 命中率更高
- 减少了不必要的数据加载
更高效的数据结构
- 使用 ConcurrentHashMap 的改进版本
- 减少锁竞争
更好的并发设计
- 读操作几乎无锁
- 写操作使用更细粒度的锁
惰性清理优化
- 批量处理过期清理
- 减少清理开销
缓存配置
5. Caffeine 的过期策略有哪些?
答案:
- expireAfterWrite:写入后固定时间过期
Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)- expireAfterAccess:最后访问后固定时间过期
Caffeine.newBuilder()
.expireAfterAccess(5, TimeUnit.MINUTES)- expireAfter:自定义过期策略
Caffeine.newBuilder()
.expireAfter(new Expiry<String, User>() {
@Override
public long expireAfterCreate(String key, User user, long currentTime) {
return user.isVip() ?
TimeUnit.HOURS.toNanos(1) :
TimeUnit.MINUTES.toNanos(10);
}
// ...
})- refreshAfterWrite:写入后固定时间刷新(惰性)
Caffeine.newBuilder()
.refreshAfterWrite(1, TimeUnit.MINUTES)6. refreshAfterWrite 和 expireAfterWrite 有什么区别?
答案:
| 特性 | expireAfterWrite | refreshAfterWrite |
|---|---|---|
| 触发时机 | 写入后固定时间 | 写入后固定时间 |
| 行为 | 删除数据,重新加载 | 异步刷新,返回旧值 |
| 阻塞 | 加载时阻塞 | 不阻塞 |
| 数据新鲜度 | 保证新鲜 | 可能返回旧数据 |
最佳实践:组合使用
Caffeine.newBuilder()
.refreshAfterWrite(1, TimeUnit.MINUTES) // 1分钟后刷新
.expireAfterWrite(10, TimeUnit.MINUTES) // 10分钟后过期
.build(loader);7. Caffeine 的三种缓存类型有什么区别?
答案:
- Cache:手动加载
Cache<String, User> cache = Caffeine.newBuilder().build();
User user = cache.get(key, k -> loadUser(k));- LoadingCache:同步自动加载
LoadingCache<String, User> cache = Caffeine.newBuilder()
.build(key -> loadUser(key));
User user = cache.get(key); // 自动加载- AsyncLoadingCache:异步自动加载
AsyncLoadingCache<String, User> cache = Caffeine.newBuilder()
.buildAsync(key -> loadUser(key));
CompletableFuture<User> future = cache.get(key);Spring 集成
8. Spring Cache 的常用注解有哪些?
答案:
| 注解 | 作用 |
|---|---|
@Cacheable | 查询缓存,不存在则执行方法并缓存结果 |
@CachePut | 执行方法并更新缓存 |
@CacheEvict | 删除缓存 |
@Caching | 组合多个缓存操作 |
@CacheConfig | 类级别的缓存配置 |
@Cacheable(value = "users", key = "#id")
public User getById(Long id) { ... }
@CachePut(value = "users", key = "#user.id")
public User update(User user) { ... }
@CacheEvict(value = "users", key = "#id")
public void delete(Long id) { ... }9. @Cacheable 的 condition 和 unless 有什么区别?
答案:
- condition:在方法执行前判断,决定是否查询缓存
- unless:在方法执行后判断,决定是否缓存结果
@Cacheable(
value = "users",
key = "#id",
condition = "#id > 0", // id > 0 才查询缓存
unless = "#result == null" // 结果不为 null 才缓存
)
public User getById(Long id) {
return userRepository.findById(id).orElse(null);
}10. 如何为不同业务配置不同的缓存策略?
答案:
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
List<CaffeineCache> caches = new ArrayList<>();
// 用户缓存:大容量,长过期
caches.add(new CaffeineCache("users",
Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(30, TimeUnit.MINUTES)
.build()));
// 配置缓存:小容量,短过期
caches.add(new CaffeineCache("configs",
Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build()));
cacheManager.setCaches(caches);
return cacheManager;
}
}性能优化
11. 如何监控 Caffeine 缓存的命中率?
答案:
- 开启统计:
Caffeine.newBuilder()
.recordStats()
.build();- 获取统计信息:
CacheStats stats = cache.stats();
double hitRate = stats.hitRate();
long hitCount = stats.hitCount();
long missCount = stats.missCount();- 集成 Micrometer:
CaffeineCacheMetrics.monitor(registry, cache, "myCache");12. 缓存命中率低可能是什么原因?
答案:
缓存容量太小
- 频繁淘汰导致命中率低
- 解决:增加 maximumSize
过期时间太短
- 数据还没被重复访问就过期了
- 解决:增加过期时间
数据访问模式不适合缓存
- 数据访问分散,没有热点
- 解决:分析访问模式,只缓存热点数据
缓存 key 设计不合理
- key 粒度太细或太粗
- 解决:优化 key 设计
缓存预热不足
- 冷启动时命中率低
- 解决:应用启动时预热缓存
13. 如何解决缓存穿透问题?
答案:
缓存穿透:查询不存在的数据,每次都穿透到数据库。
解决方案:
- 缓存空值
LoadingCache<String, Optional<User>> cache = Caffeine.newBuilder()
.build(key -> Optional.ofNullable(findUser(key)));- 布隆过滤器
BloomFilter<String> bloomFilter = BloomFilter.create(...);
public User getUser(String id) {
if (!bloomFilter.mightContain(id)) {
return null; // 一定不存在
}
return cache.get(id);
}14. 如何解决缓存雪崩问题?
答案:
缓存雪崩:大量缓存同时过期,导致请求全部打到数据库。
解决方案:
- 过期时间加随机值
int baseExpire = 10;
int randomExpire = new Random().nextInt(5);
// 过期时间 10-15 分钟- 使用 refreshAfterWrite
Caffeine.newBuilder()
.refreshAfterWrite(9, TimeUnit.MINUTES)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(asyncLoader);- 多级缓存
// L1: Caffeine 本地缓存
// L2: Redis 分布式缓存对比与选型
15. 什么场景下应该使用 Caffeine?
答案:
适合使用 Caffeine 的场景:
- 高并发读取,需要极高性能
- 热点数据缓存
- Spring Boot 应用
- 替换 Guava Cache 升级性能
- 需要异步加载的场景
不适合使用 Caffeine 的场景:
- 分布式缓存(需要 Redis)
- 大数据量缓存(超出 JVM 内存)
- 需要持久化
- 需要跨进程共享数据
16. Caffeine 和 Redis 如何选择?
答案:
| 特性 | Caffeine | Redis |
|---|---|---|
| 类型 | 本地缓存 | 分布式缓存 |
| 访问速度 | 纳秒级 | 毫秒级 |
| 数据共享 | 单机 | 多机共享 |
| 容量 | 受 JVM 限制 | 可水平扩展 |
| 持久化 | 不支持 | 支持 |
| 一致性 | 单机一致 | 需要同步 |
最佳实践:多级缓存
// L1: Caffeine(热点数据,高性能)
// L2: Redis(共享数据,大容量)
public User getUser(String id) {
// 先查 L1
User user = caffeineCache.getIfPresent(id);
if (user != null) return user;
// 再查 L2
user = redisCache.get(id);
if (user != null) {
caffeineCache.put(id, user);
return user;
}
// 查数据库
user = userRepository.findById(id);
if (user != null) {
redisCache.put(id, user);
caffeineCache.put(id, user);
}
return user;
}小结
本章汇总了 Caffeine 相关的常见面试题,涵盖基础概念、缓存配置、Spring 集成、性能优化和对比选型。掌握这些知识点有助于深入理解 Caffeine 的设计原理和最佳实践。
