Caffeine 本地缓存 + Spring Cache 深度
概述
Spring 从 3.1 版本开始引入了 Spring Cache Abstraction——它不是一个具体的缓存实现,而是一套统一的缓存抽象层。底层支持 Caffeine、Redis、EhCache、JCache(JSR-107)等多种缓存引擎。
本文导航
| 章节 | 内容 |
|---|---|
| 一 | Spring Cache 核心注解与 CacheManager 体系 |
| 二 | Caffeine 本地缓存深度集成 |
| 三 | RedisCacheManager + 多级缓存 |
| 四 | 缓存穿透/击穿/雪崩终极方案 |
| 五 | 实战:商品详情页多级缓存 |
一、Spring Cache 核心
1.1 核心注解
| 注解 | 说明 |
|---|---|
@Cacheable | 方法返回值缓存;存在则直接返回,不存在则执行方法后缓存 |
@CachePut | 每次都执行方法,并将返回值写入缓存 |
@CacheEvict | 删除缓存条目(allEntries 可清空整个缓存区域) |
@Caching | 组合多个缓存操作 |
@CacheConfig | 类级别统一配置 cacheNames、keyGenerator 等 |
1.2 @Cacheable 完整参数
java
@Service
public class ProductService {
@Cacheable(
cacheNames = "products", // 缓存区域
key = "#id", // SpEL 动态 key
keyGenerator = "myKeyGenerator", // 自定义 key 生成器(与 key 互斥)
cacheManager = "cacheManager", // 指定 CacheManager
condition = "#id > 0", // SpEL 条件 true 才缓存
unless = "#result == null", // SpEL 条件 true 不缓存
sync = true // 同步加载(防止缓存击穿)
)
public Product getById(Long id) {
return productMapper.selectById(id);
}
}1.3 CacheManager 体系
CacheManager (接口)
├── AbstractCacheManager
│ ├── ConcurrentMapCacheManager (默认,ConcurrentHashMap)
│ ├── EhCacheCacheManager (EhCache 2.x)
│ └── JCacheCacheManager (JSR-107)
├── RedisCacheManager (Spring Data Redis)
└── CaffeineCacheManager (Caffeine)二、Caffeine 集成
2.1 依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>2.2 Caffeine 配置
yaml
spring:
cache:
type: caffeine
caffeine:
spec: maximumSize=10000,expireAfterWrite=60s,recordStats或 Java Config 精细控制:
java
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public Caffeine<Object, Object> caffeineSpec() {
return Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(60, TimeUnit.SECONDS)
.expireAfterAccess(30, TimeUnit.MINUTES)
.refreshAfterWrite(30, TimeUnit.SECONDS) // 自动刷新(需搭配 AsyncCacheLoader)
.recordStats() // 开启统计
.softValues(); // 软引用(GC 时可回收)
}
@Bean
public CacheManager cacheManager(Caffeine<Object, Object> caffeine) {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCaffeine(caffeine);
manager.setCacheNames(Arrays.asList("products", "users", "orders"));
return manager;
}
}2.3 Caffeine 核心参数
| 参数 | 说明 | 推荐值 |
|---|---|---|
maximumSize | 最大条目数 | 10k ~ 1M |
maximumWeight | 最大权重(搭配 weigher) | 视对象大小 |
expireAfterWrite | 写入后过期 | 60 ~ 600s |
expireAfterAccess | 访问后过期 | 30min |
refreshAfterWrite | 写入后自动刷新 | 30 ~ 60s |
softValues | 软引用值 | GC 友好 |
weakKeys / weakValues | 弱引用键/值 | 防止内存泄漏 |
recordStats | 开启统计 | 生产环境建议开启 |
2.4 缓存统计
java
@RestController
public class CacheMonitorController {
@Autowired
private CacheManager cacheManager;
@GetMapping("/admin/cache/stats")
public Map<String, Object> cacheStats() {
Map<String, Object> stats = new HashMap<>();
for (String name : cacheManager.getCacheNames()) {
CaffeineCache cache = (CaffeineCache) cacheManager.getCache(name);
CacheStats cacheStats = cache.getNativeCache().stats();
Map<String, Object> info = new HashMap<>();
info.put("hitCount", cacheStats.hitCount());
info.put("missCount", cacheStats.missCount());
info.put("hitRate", cacheStats.hitRate());
info.put("evictionCount", cacheStats.evictionCount());
info.put("loadTime", cacheStats.averageLoadPenalty());
stats.put(name, info);
}
return stats;
}
}三、RedisCacheManager + 多级缓存
3.1 RedisCacheManager 配置
java
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues(); // 不缓存 null
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.withInitialCacheConfigurations(Map.of(
"products", config.entryTtl(Duration.ofMinutes(5)),
"users", config.entryTtl(Duration.ofHours(1))
))
.transactionAware()
.build();
}3.2 多级缓存实现
java
@Component
public class MultiLevelCacheManager implements CacheManager {
private final CaffeineCacheManager local;
private final RedisCacheManager remote;
private final Map<String, Cache> caches = new ConcurrentHashMap<>();
public MultiLevelCacheManager(CaffeineCacheManager local, RedisCacheManager remote) {
this.local = local;
this.remote = remote;
}
@Override
public Cache getCache(String name) {
return caches.computeIfAbsent(name, key -> new MultiLevelCache(
local.getCache(key), // L1: Caffeine (1ms)
remote.getCache(key) // L2: Redis (5ms)
));
}
@Override
public Collection<String> getCacheNames() {
return local.getCacheNames();
}
}java
public class MultiLevelCache implements Cache {
private final Cache local;
private final Cache remote;
@Override
public ValueWrapper get(Object key) {
// L1 → L2 → DB(由 @Cacheable 触发)
ValueWrapper value = local.get(key);
if (value != null) return value;
value = remote.get(key);
if (value != null) {
local.put(key, value.get());
}
return value;
}
@Override
public void put(Object key, Object value) {
local.put(key, value);
remote.put(key, value);
}
@Override
public void evict(Object key) {
local.evict(key);
remote.evict(key);
}
}四、缓存穿透 / 击穿 / 雪崩
4.1 缓存穿透
现象:查询不存在的数据,每次穿越缓存直达 DB。
解决方案:
java
@Cacheable(cacheNames = "products", unless = "#result == null")
public Product getById(Long id) {
return productMapper.selectById(id);
}
// 但这样会缓存大量的空值,改用 BloomFilter
@Component
public class BloomFilterCache {
private final BloomFilter<Long> bloomFilter = BloomFilter.create(
Funnels.longFunnel(), 100_000, 0.01);
@PostConstruct
public void init() {
productMapper.selectAllIds().forEach(bloomFilter::put);
}
public boolean mightContain(Long id) {
return bloomFilter.mightContain(id);
}
}4.2 缓存击穿
现象:热点 key 失效瞬间,大量请求同时穿透到 DB。
解决方案:
java
// 方案 1:sync=true(推荐)
@Cacheable(cacheNames = "hotProduct", key = "#id", sync = true)
public Product getHotProduct(Long id) {
return productMapper.selectById(id);
}
// 方案 2:互斥锁(Redisson)
public Product getHotProductWithLock(Long id) {
String cacheKey = "hotProduct:" + id;
Product product = cacheManager.getCache("products").get(cacheKey, Product.class);
if (product != null) return product;
RLock lock = redissonClient.getLock("lock:product:" + id);
lock.lock(5, TimeUnit.SECONDS);
try {
// 二次检查
product = cacheManager.getCache("products").get(cacheKey, Product.class);
if (product != null) return product;
product = productMapper.selectById(id);
cacheManager.getCache("products").put(cacheKey, product);
return product;
} finally {
lock.unlock();
}
}4.3 缓存雪崩
现象:大量 key 同一时间过期,请求全部打向 DB。
解决方案:
java
// 方案 1:过期时间加随机值
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(300 + ThreadLocalRandom.current().nextInt(60)));
return RedisCacheManager.builder(factory).cacheDefaults(config).build();
}
// 方案 2:Caffeine 本地缓存做 L1 保护
// 方案 3:限流降级
@Component
public class CacheFallback {
@Cacheable(cacheNames = "products", unless = "#result == null")
public Product getById(Long id) {
try {
return productMapper.selectById(id);
} catch (Exception e) {
// 降级:返回本地内存中的热点数据
return localCache.getDefaultProduct();
}
}
}五、实战:商品详情页多级缓存
5.1 缓存层次
用户请求
│
▼
┌─────────────┐ 1ms ┌──────────────┐
│ L1: Caffeine │──────▶│ 命中直接返回 │
│ (本地堆内) │ └──────────────┘
└──────┬───────┘
│ 未命中
▼
┌─────────────┐ 5ms ┌──────────────┐
│ L2: Redis │──────▶│ 反写 L1 │
│ (分布式) │ └──────────────┘
└──────┬───────┘
│ 未命中
▼
┌─────────────┐ 50ms ┌──────────────┐
│ L3: MySQL │──────▶│ 反写 L1+L2 │
│ (数据库) │ └──────────────┘
└─────────────┘5.2 实现代码
java
@Service
public class ProductDetailService {
@Autowired
private ProductMapper productMapper;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private RedissonClient redissonClient;
// Caffeine L1 缓存
private final Cache<Long, Product> localCache = Caffeine.newBuilder()
.maximumSize(5000)
.expireAfterWrite(10, TimeUnit.SECONDS)
.recordStats()
.build();
public Product getProductDetail(Long id) {
// 1. L1 缓存
Product product = localCache.getIfPresent(id);
if (product != null) return product;
// 2. L2 Redis 缓存
String key = "product:detail:" + id;
product = (Product) redisTemplate.opsForValue().get(key);
if (product != null) {
localCache.put(id, product);
return product;
}
// 3. 互斥锁防击穿
RLock lock = redissonClient.getLock("lock:product:" + id);
lock.lock(5, TimeUnit.SECONDS);
try {
// 双重检查
product = (Product) redisTemplate.opsForValue().get(key);
if (product != null) {
localCache.put(id, product);
return product;
}
// 4. L3 DB 查询
product = productMapper.selectById(id);
if (product == null) {
// 布隆过滤器防穿透
return null;
}
// 5. 反写 L2 + L1
redisTemplate.opsForValue().set(key, product, 30 + random.nextInt(30), TimeUnit.MINUTES);
localCache.put(id, product);
return product;
} finally {
lock.unlock();
}
}
// MQ 失效通知
@RabbitListener(queues = "cache.invalidate")
public void handleCacheInvalidate(CacheInvalidateMessage msg) {
localCache.invalidate(msg.getProductId());
redisTemplate.delete("product:detail:" + msg.getProductId());
}
}5.3 性能对比
| 层级 | 耗时 | 容量 | 特点 |
|---|---|---|---|
| L1 Caffeine | ~1ms | 5000 条 | 极快,进程内 |
| L2 Redis | ~5ms | 全量 | 分布式共享 |
| L3 MySQL | ~50ms | 全量 | 最终数据源 |
六、CacheInterceptor 源码分析
6.1 核心流程
java
// CacheInterceptor.execute() 简化流程
public Object execute(final InvocationContext context) {
CacheOperationExpressionEvaluator evaluator = new CacheOperationExpressionEvaluator();
// 1. 解析 @Cacheable/@CachePut/@CacheEvict 操作
Collection<CacheOperation> operations = getCacheOperations(context);
// 2. 遍历所有 Cache 操作
for (CacheOperation operation : operations) {
if (operation instanceof CacheableOperation) {
// 尝试从缓存获取
Object cached = cacheManager.getCache(operation.getCacheName())
.get(context.getKey());
if (cached != null && !(cached instanceof NullValue)) {
return cached; // 缓存命中
}
// sync=true 时加锁
if (operation.isSync()) {
return synchronizedGet(cache, key, () -> context.proceed());
}
}
}
// 3. 执行目标方法
Object result = context.proceed();
// 4. 写入缓存(@CachePut / @Cacheable 未命中)
updateCache(operations, result);
return result;
}6.2 关键类
| 类 | 职责 |
|---|---|
CacheInterceptor | 拦截 @Cacheable 等方法,实现缓存逻辑 |
CacheAspectSupport | 抽象基类,处理 SpEL 解析、Cache 操作合并 |
CacheOperationExpressionEvaluator | SpEL 表达式求值(key、condition、unless) |
SimpleKeyGenerator | 默认 key 生成器(参数组合) |
七、总结
| 知识点 | 要点 |
|---|---|
| Spring Cache 注解 | @Cacheable / @CachePut / @CacheEvict / @Caching / @CacheConfig |
| CacheManager 体系 | ConcurrentMap / Caffeine / Redis / EhCache / JCache |
| Caffeine 参数 | maximumSize、expireAfterWrite、refreshAfterWrite、recordStats |
| 多级缓存 | L1 Caffeine → L2 Redis → L3 DB |
| 缓存穿透 | BloomFilter + unless = "#result == null" |
| 缓存击穿 | sync = true + 互斥锁 Redisson |
| 缓存雪崩 | TTL 随机化 + L1 本地缓存保护 |
| 失效通知 | MQ 广播失效事件,L1+L2 同时失效 |
参考链接: