Source:https://github.com/ben-manes/caffeine/wiki
GitHub:https://github.com/ben-manes/caffeine
Capability Boundaries
✅ Strong Suits
- Local vs remote choice — Use Caffeine for high-frequency data in a single service, use Redis for sharing across multiple services
- Expiration strategies — expireAfterWrite (after write) vs expireAfterAccess (after access) vs refreshAfterWrite (asynchronous refresh)
- CacheLoader — Automatic loading of missing data + reload asynchronous refresh + graceful degradation (return old value)
- Spring Cache integration — @Cacheable/@CacheEvict + CaffeineCacheManager
- Eviction strategies — maximumSize (count)/maximumWeight (weight)/Window TinyLFU
- Record statistics — recordStats() + CacheStats to monitor hit rate
❌ Out of Scope
- Distributed cache/shared data → redis-redisson-patterns
- Redis + Caffeine two-level cache → redis-redisson-patterns(already covered)
Expiration strategy selection decision
| Strategy | Behavior | Applicable scenarios |
|---|---|---|
| expireAfterWrite | Expires after a fixed time following write | ✅ Recommended (most business cases), predictable data refresh |
| expireAfterAccess | Expires after a fixed time following last access | Session cache/user login status |
| refreshAfterWrite | Asynchronously refreshes after a fixed time following write (returns old value) | Non-blocking refresh for hot data |
| expireAfterWrite + refreshAfterWrite | Refresh executes before expiration, never blocks | ⭐ Recommended combination |
⚠️
refreshAfterWritemust be <expireAfterWrite, otherwise refresh will never be triggered
The mistakes LLMs make most often
| # | Mistake | Correct approach |
|---|---|---|
| 1 | ConcurrentHashMap<String, Object> + manual expiration |
Caffeine CacheBuilder Window TinyLFU algorithm |
| 2 | All caches go through Redis | High-frequency caching in a single service goes through Caffeine (nanosecond-level, Redis is millisecond-level) |
| 3 | Not setting maximumSize | maximumSize(10000) to prevent memory overflow |
| 4 | Not handling loading exceptions after setting refreshAfterWrite | The old value is still usable; a load failure does not affect the service |
| 5 | refreshAfterWrite > expireAfterWrite | Never refresh (it is simply deleted on expiration) |
| 6 | CacheLoader.load() returns null | Returning null = cache penetration (every query goes through load) |
| 7 | Setting only refreshAfterWrite without setting maximumSize | Unbounded cache → OOM |
| 8 | Modifying the object returned by the cache | The cached object is modified externally → data inconsistency |
Core patterns
Pattern 1: Common Caffeine configuration (recommended combination)
// ✅ 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
Pattern 2: CacheLoader + reload graceful degradation
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"); // 自动加载
Pattern 3: Spring Cache integration
@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() { }
}
Pattern 4: Asynchronous loading (AsyncCache)
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));
Pattern 5: Manual cache operations + statistics
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
- Caffeine is an in-process cache — In a multi-instance deployment, each instance has its own independent cache, causing data inconsistency
- expireAfterAccess is suitable for session caching — If the post-write frequency is very high, set access
- refreshAfterWrite must be < expireAfterWrite — Otherwise refresh is meaningless
- CacheLoader.load() must not return null — Return an empty placeholder object or throw an exception
- Spring Cache's @Cacheable return value must be serializable — If Redis is used as the backend
- maximumSize eviction may lose data — Use a database as the ultimate safeguard for important data
- Working with Redisson — Caffeine (L1) + Redis (L2) is a classic two-level cache combination
- Be mindful of defensive copying when cached objects are mutable — External modification of cached objects causes data inconsistency
- recordStats() does not affect performance — Recommended to enable in production
- expireAfter(Expiry) supports custom expiration times — Dynamically determine TTL based on the value
- Caffeine 3.x and 2.x compatibility — Spring Boot 2.x uses Caffeine 2.x, Spring Boot 3.x uses 3.x
- Use weakKeys/softValues with caution — GC-triggered eviction is unpredictable
Data Privacy
This skill does not collect, store, or transmit any user data.