caffeine-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Caffeine JVM 本地缓存

Caffeine JVM 本地缓存

Capability Boundaries

能力边界

✅ Strong Suits

✅ 强适用场景

  1. 本地 vs 远程选择 — 单服务高频数据用Caffeine,多服务共享用Redis
  2. 过期策略 — expireAfterWrite(写入后) vs expireAfterAccess(访问后) vs refreshAfterWrite(异步刷新)
  3. CacheLoader — 自动加载缺失数据 + reload 异步刷新 + 优雅降级(返回旧值)
  4. Spring Cache 集成 — @Cacheable/@CacheEvict + CaffeineCacheManager
  5. 驱逐策略 — maximumSize(数量)/maximumWeight(权重)/Window TinyLFU
  6. 记录统计 — recordStats() + CacheStats 监控命中率
  1. 本地与远程缓存选择 — 单服务高频数据用Caffeine,多服务共享用Redis
  2. 过期策略 — expireAfterWrite(写入后) vs expireAfterAccess(访问后) vs refreshAfterWrite(异步刷新)
  3. CacheLoader — 自动加载缺失数据 + reload 异步刷新 + 优雅降级(返回旧值)
  4. Spring Cache 集成 — @Cacheable/@CacheEvict + CaffeineCacheManager
  5. 驱逐策略 — maximumSize(数量)/maximumWeight(权重)/Window TinyLFU
  6. 记录统计 — recordStats() + CacheStats 监控命中率

❌ Out of Scope

❌ 不适用范围

  1. 分布式缓存/共享数据 → redis-redisson-patterns
  2. Redis + Caffeine 两级缓存 → redis-redisson-patterns(已覆盖)
  1. 分布式缓存/共享数据 → redis-redisson-patterns
  2. Redis + Caffeine 两级缓存 → redis-redisson-patterns(已覆盖)

过期策略选择决策

过期策略选择决策

策略行为适用场景
expireAfterWrite写入后固定时间过期✅ 推荐(大多数业务),可预测的数据刷新
expireAfterAccess最后访问后固定时间过期会话缓存/用户登录状态
refreshAfterWrite写后固定时间异步刷新(返回旧值)热点数据免阻塞刷新
expireAfterWrite + refreshAfterWrite刷新在过期前执行,永不阻塞⭐ 推荐组合
⚠️
refreshAfterWrite
必须 <
expireAfterWrite
,否则刷新永远不触发
策略行为适用场景
expireAfterWrite写入后固定时间过期✅ 推荐(大多数业务),可预测的数据刷新
expireAfterAccess最后访问后固定时间过期会话缓存/用户登录状态
refreshAfterWrite写入后固定时间异步刷新(返回旧值)热点数据无阻塞刷新
expireAfterWrite + refreshAfterWrite刷新在过期前执行,始终不阻塞⭐ 推荐组合
⚠️
refreshAfterWrite
必须小于
expireAfterWrite
,否则刷新永远不会触发

LLM最常犯的错误

LLM 最常犯的错误

#错误正确做法
1
ConcurrentHashMap<String, Object>
+ 手动过期
Caffeine CacheBuilder 窗口 TinyLFU 算法
2所有缓存都走 Redis单服务高频缓存走 Caffeine(纳秒级,Redis 毫秒级)
3不设置 maximumSize
maximumSize(10000)
防止内存溢出
4refreshAfterWrite 设置后不处理加载异常旧值仍可用,加载失败不影响服务
5refreshAfterWrite > expireAfterWrite永远不刷新(过期就直接删除了)
6CacheLoader.load() 返回 null返回 null = 缓存穿透(每次查都走 load)
7只设置 refreshAfterWrite 不设 maximumSize无界缓存 → OOM
8修改缓存返回的对象缓存对象被外部修改 → 数据不一致
#错误正确做法
1
ConcurrentHashMap<String, Object>
+ 手动过期
使用 Caffeine CacheBuilder 的窗口 TinyLFU 算法
2所有缓存都走 Redis单服务高频缓存走 Caffeine(纳秒级,Redis 毫秒级)
3不设置 maximumSize
maximumSize(10000)
防止内存溢出
4设置 refreshAfterWrite 后不处理加载异常旧值仍可用,加载失败不影响服务
5refreshAfterWrite > expireAfterWrite永远不会刷新(过期就直接删除了)
6CacheLoader.load() 返回 null返回 null = 缓存穿透(每次查询都会走 load)
7只设置 refreshAfterWrite,不设置 maximumSize无界缓存 → OOM
8修改缓存返回的对象缓存对象被外部修改 → 数据不一致

核心模式

核心模式

模式 1: Caffeine 常用配置(推荐组合)

模式 1: Caffeine 常用配置(推荐组合)

java
// ✅ 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));           // CacheLoader
java
// ✅ 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));           // CacheLoader

模式 2: CacheLoader + reload 优雅降级

模式 2: CacheLoader + reload 优雅降级

java
LoadingCache<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");  // 自动加载
java
LoadingCache<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");  // 自动加载

模式 3: Spring Cache 集成

模式 3: Spring Cache 集成

java
@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() { }
}
java
@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() { }
}

模式 4: 异步加载(AsyncCache)

模式 4: 异步加载(AsyncCache)

java
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));
java
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));

模式 5: 手动缓存操作 + 统计

模式 5: 手动缓存操作 + 统计

java
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());
java
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());

Gotchas

注意事项

  1. Caffeine 是进程内缓存 — 多实例部署每个实例有独立缓存,数据不一致
  2. expireAfterAccess 适合会话缓存 — 写入后频率很高就设置 access
  3. refreshAfterWrite 必须 < expireAfterWrite — 否则刷新没意义
  4. CacheLoader.load() 不能返回 null — 返回空占位对象或抛异常
  5. Spring Cache 的 @Cacheable 返回值必须可序列化 — 如果用 Redis 作为后端
  6. maximumSize 驱逐可能丢失数据 — 重要数据用数据库作为最终保障
  7. 与 Redisson 配合 — Caffeine(L1) + Redis(L2) 是经典的两级缓存组合
  8. 缓存对象可变时注意防御性拷贝 — 外部修改缓存对象导致数据不一致
  9. recordStats() 不影响性能 — 生产环境建议开启
  10. expireAfter(Expiry) 支持自定义过期时间 — 根据 value 动态决定 TTL
  11. Caffeine 3.x 与 2.x 兼容性 — Spring Boot 2.x 使用 Caffeine 2.x,Spring Boot 3.x 使用 3.x
  12. weakKeys/softValues 慎用 — GC 触发的驱逐不可预测
  1. Caffeine 是进程内缓存 — 多实例部署时,每个实例都有独立缓存,数据可能不一致
  2. expireAfterAccess 适合会话缓存 — 写入后访问频率很高,就设置 access
  3. refreshAfterWrite 必须小于 expireAfterWrite — 否则刷新没有意义
  4. CacheLoader.load() 不能返回 null — 返回空占位对象或抛出异常
  5. Spring Cache 的 @Cacheable 返回值必须可序列化 — 如果使用 Redis 作为后端
  6. maximumSize 驱逐可能导致数据丢失 — 重要数据应使用数据库作为最终保障
  7. 与 Redisson 配合 — Caffeine(L1) + Redis(L2) 是经典的两级缓存组合
  8. 缓存对象可变时注意防御性拷贝 — 外部修改缓存对象会导致数据不一致
  9. recordStats() 不影响性能 — 生产环境建议开启
  10. expireAfter(Expiry) 支持自定义过期时间 — 根据 value 动态决定 TTL
  11. Caffeine 3.x 与 2.x 的兼容性 — Spring Boot 2.x 使用 Caffeine 2.x,Spring Boot 3.x 使用 3.x
  12. 谨慎使用 weakKeys/softValues — GC 触发的驱逐不可预测

Data Privacy

数据隐私

本技能不收集、存储或传输任何用户数据。
本技能不收集、存储或传输任何用户数据。