Loading...
Loading...
Caffeine JVM 本地缓存技能。覆盖本地缓存 vs 远程缓存(Redis)选择决策树、expireAfterWrite vs expireAfterAccess vs refreshAfterWrite 三种过期策略对比与组合规则(refresh < expire)、CacheLoader加载与reload异步刷新模式、Spring Cache @Cacheable集成、maximumSize驱逐策略、recordStats缓存统计。 纠正 LLM:用 ConcurrentHashMap+手动过期、所有缓存走Redis、refreshAfterWrite > expireAfterWrite导致永远不刷新、CacheLoader返回null导致缓存穿透。
npx skill4agent add full-stack-skills/java-skills caffeine-patterns| 策略 | 行为 | 适用场景 |
|---|---|---|
| expireAfterWrite | 写入后固定时间过期 | ✅ 推荐(大多数业务),可预测的数据刷新 |
| expireAfterAccess | 最后访问后固定时间过期 | 会话缓存/用户登录状态 |
| refreshAfterWrite | 写后固定时间异步刷新(返回旧值) | 热点数据免阻塞刷新 |
| expireAfterWrite + refreshAfterWrite | 刷新在过期前执行,永不阻塞 | ⭐ 推荐组合 |
⚠️必须 <refreshAfterWrite,否则刷新永远不触发expireAfterWrite
| # | 错误 | 正确做法 |
|---|---|---|
| 1 | | Caffeine CacheBuilder 窗口 TinyLFU 算法 |
| 2 | 所有缓存都走 Redis | 单服务高频缓存走 Caffeine(纳秒级,Redis 毫秒级) |
| 3 | 不设置 maximumSize | |
| 4 | refreshAfterWrite 设置后不处理加载异常 | 旧值仍可用,加载失败不影响服务 |
| 5 | refreshAfterWrite > expireAfterWrite | 永远不刷新(过期就直接删除了) |
| 6 | CacheLoader.load() 返回 null | 返回 null = 缓存穿透(每次查都走 load) |
| 7 | 只设置 refreshAfterWrite 不设 maximumSize | 无界缓存 → OOM |
| 8 | 修改缓存返回的对象 | 缓存对象被外部修改 → 数据不一致 |
// ✅ expireAfterWrite + refreshAfterWrite 组合(推荐)
Cache<String, User> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(5, TimeUnit.MINUTES) // 5分钟过期
.refreshAfterWrite(3, TimeUnit.MINUTES) // 3分钟后台刷新(返回旧值)
.recordStats() // 记录命中率
.build(key -> loadFromDB(key)); // CacheLoaderLoadingCache<String, User> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.refreshAfterWrite(3, TimeUnit.MINUTES)
.build(new CacheLoader<String, User>() {
@Override
public @Nullable User load(String key) {
// 缓存未命中时加载
User user = userMapper.selectById(Long.parseLong(key));
// ✅ 不要返回 null!使用 Optional 或空对象占位
return user != null ? user : NULL_USER;
}
@Override
public @Nullable User reload(String key, User oldValue) {
// ✅ 刷新时优雅降级:加载失败返回旧值
try {
return load(key);
} catch (Exception e) {
log.error("刷新缓存失败, key={}", key, e);
return oldValue; // ← 返回旧值,不影响服务
}
}
});
// 使用
User user = cache.get("user:1"); // 自动加载@Configuration
@EnableCaching // ← 必须!
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager mgr = new CaffeineCacheManager();
mgr.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(10_000)
.recordStats());
return mgr;
}
}
@Service
public class UserService {
@Cacheable(value = "users", key = "#id", unless = "#result == null")
public User getUser(Long id) {
return userMapper.selectById(id);
}
@CacheEvict(value = "users", key = "#id")
public void updateUser(Long id, UserDTO dto) {
userMapper.updateById(dto.toEntity(id));
}
@CacheEvict(value = "users", allEntries = true) // 清空整个缓存
public void refreshAll() { }
}AsyncCache<String, User> asyncCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.refreshAfterWrite(1, TimeUnit.MINUTES) // 异步刷新
.buildAsync(key -> loadFromDB(key));
// 异步获取
CompletableFuture<User> future = asyncCache.get("user:1");
future.thenAccept(user -> System.out.println(user));Cache<String, User> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.recordStats() // 启用统计
.build();
// 手动写入
cache.put("user:1", user);
// 手动失效
cache.invalidate("user:1");
// 统计数据
CacheStats stats = cache.stats();
log.info("命中率: {}, 加载次数: {}, 驱逐次数: {}",
stats.hitRate(), stats.loadCount(), stats.evictionCount());