上一篇介绍了 Caffeine 的淘汰算法(W-TinyLFU),这一篇我们聚焦怎么用:从 Caffeine 的基础用法入手,再到生产中最常见的高性能方案——Caffeine 本地缓存 + Redis 分布式缓存组成的二级缓存,最后梳理一下它和 Spring Cache、Ehcache、Guava Cache 等缓存生态的关系。
一、Caffeine 入门
Caffeine 是一个基于 Java 8 的高性能本地缓存库,是 Spring Boot 默认推荐的本地缓存实现,性能优于 Guava Cache。
1. 引入依赖
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
2. 基本用法
Cache<String, User> cache = Caffeine.newBuilder()
.maximumSize(10_000) // 容量上限,超出按 W-TinyLFU 淘汰
.expireAfterWrite(Duration.ofMinutes(5)) // 写入 5 分钟后过期
.recordStats() // 开启命中率统计
.build();
// 读:缓存未命中时,通过 mappingFunction 自动加载并回填
User user = cache.get(userId, id -> userService.getById(id));
3. 三种加载方式
- Cache(手动):
get(key, mappingFunction)手动控制,最灵活; - LoadingCache(同步加载):构造时传入
CacheLoader,未命中自动同步加载; - AsyncLoadingCache(异步加载):基于
CompletableFuture,未命中时异步加载,适合 IO 密集场景。
4. 常用配置
| 配置 | 作用 |
|---|---|
maximumSize(n) |
按条目数淘汰 |
maximumWeight(n) |
按权重淘汰 |
expireAfterWrite(d) |
写入后过期 |
expireAfterAccess(d) |
最后一次访问后过期 |
refreshAfterWrite(d) |
写入后异步刷新,避免阻塞 |
recordStats() |
记录命中率、加载耗时等指标 |
removalListener(...) |
条目被淘汰/替换时回调 |
二、为什么要做"二级缓存"
单用 Redis 足够吗?在高并发读场景下,Redis 虽然是内存数据库,但仍有网络 I/O 开销,每次查询都要走一次网络往返;而且 Redis 宕机时缓存层就整体失效,流量瞬间打穿到数据库。
于是有了本地缓存 + 分布式缓存的组合:
请求 → Caffeine(本地,进程内,最快)
↓ 未命中
Redis(分布式,跨进程共享)
↓ 未命中
数据库(兜底,回填 Redis + Caffeine)
- 第一级:Caffeine 本地缓存——零网络开销,微秒级响应,扛住超高并发读;
- 第二级:Redis 分布式缓存——多实例共享,容量大、可扩展,命中率兜底;
- 第三级:数据库——最后兜底,回填两级缓存。
好处是性能拉满(本地命中完全不走网络),坏处是一致性变难——同一个 key 在多个 JVM 进程里各有一份拷贝,更新时要保证各副本同步。
三、Caffeine + Redis 二级缓存实战
1. 基础流程
public User getUser(Long id) {
// 1. 先查本地缓存
User user = caffeineCache.getIfPresent(id);
if (user != null) {
return user;
}
// 2. 本地未命中,查 Redis
String key = "user:" + id;
String json = redisTemplate.opsForValue().get(key);
if (json != null) {
user = JSON.parseObject(json, User.class);
caffeineCache.put(id, user); // 回填本地
return user;
}
// 3. 两级都未命中,查库并回填两级缓存
user = userMapper.selectById(id);
caffeineCache.put(id, user);
redisTemplate.opsForValue().set(key, JSON.toJSONString(user), 30, TimeUnit.MINUTES);
return user;
}
2. 如何保证一致性(重点)
二级缓存最容易踩的坑是:更新数据库后,本地缓存和 Redis 没同步更新,读到旧数据。常用手段:
- 延迟双删:更新数据库后,先删 Redis,再延迟一小段时间删一次(配合第二次删除兜底并发窗口);
- 本地缓存用极短 TTL:本地缓存的过期时间设置得比 Redis 短得多(比如 Redis 30 分钟、本地 30 秒),让本地副本快速过期自愈,即使不一致也只在很短的窗口内发生;
- 消息通知失效:更新方通过 MQ/Redis Pub-Sub 广播"某 key 已变更",各实例收到后主动清掉本地缓存;
- 版本号比对:缓存里带上版本号,读取时校验,版本过期则重新加载。
业界更推荐"本地缓存短 TTL + 删除广播"组合:本地靠短 TTL 自愈兜底,广播负责主动快速失效,兼顾一致性与性能。
3. 监听淘汰回调
Caffeine.newBuilder()
.removalListener((key, value, cause) -> {
// cause: EXPIRED / SIZE / REPLACED / EXPLICIT
log.info("本地缓存淘汰 key={}, cause={}", key, cause);
})
.build();
可用来在本地淘汰时回查 Redis,或做缓存击穿防护的兜底日志。
四、Caffeine 与其他缓存生态的关系
| 缓存 | 定位 | 与 Caffeine 的关系 |
|---|---|---|
| Guava Cache | 谷歌的本地缓存 | Caffeine 是其高配升级版,API 几乎兼容 |
| Ehcache | 本地/分布式缓存,老牌 | 同为本地缓存,Caffeine 性能更高、更轻量 |
| Redis | 分布式缓存 | Caffeine 与其组成二级缓存,Caffeine 管本地、Redis 管共享 |
| Memcached | 分布式缓存 | 与 Redis 同层,可被 Redis 替代,通常不直接与 Caffeine 组合 |
| Spring Cache | 缓存抽象层 | 统一 @Cacheable 注解,可配置 Caffeine 或 Redis 作为底层实现 |
Spring Cache + Caffeine 组合示例
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCaffeine(Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(30)));
return manager;
}
}
之后在方法上加 @Cacheable(value = "user", key = "#id"),Spring 就会自动走 Caffeine。想切 Redis 时,换一个 RedisCacheManager 即可,业务代码零改动——这就是缓存抽象层的价值。
五、总结
- Caffeine 是高性能本地缓存,适合放"单 JVM 内、超高频读"的数据;
- 二级缓存用"本地 Caffeine + 分布式 Redis"组合,本地扛并发、Redis 兜底共享,能显著降低 Redis 与数据库压力;
- 一致性是二级缓存的难点,推荐"本地短 TTL + 删除广播/延迟双删"组合;
- Spring Cache 统一了缓存抽象,Caffeine、Redis、Ehcache 都可以作为它的底层实现,按需切换。
从淘汰算法(W-TinyLFU)到入门用法,再到二级缓存与生态关联,Caffeine 这条线就完整了。下一篇可以继续深入"二级缓存一致性的完整方案"或"Redis 生产级部署",需要的话随时说。