1. 为什么需要Spring Cache?
在构建现代Java应用时,缓存几乎是提升性能的必选项。想象一下这样的场景:你的电商平台商品详情页每天被访问100万次,而每次访问都要从数据库查询商品信息。这不仅给数据库带来巨大压力,还会导致响应时间变长。这就是Spring Cache要解决的核心问题。
Spring Cache并不是一个具体的缓存实现,而是一套抽象层。就像JDBC统一了数据库操作一样,它统一了各种缓存技术的使用方式。我见过不少项目直接使用Redis或Caffeine的API,结果当需要切换缓存方案时,不得不重写大量代码。Spring Cache通过注解的方式,让开发者可以轻松切换底层缓存实现,而业务代码几乎不需要改动。
2. 核心注解深度解析
2.1 @Cacheable:查询缓存的金钥匙
这是最常用的注解,它的工作流程很有意思:
- 方法被调用时,Spring会先检查缓存中是否存在对应的键
- 如果存在,直接返回缓存值(方法体不会执行)
- 如果不存在,执行方法体并将结果存入缓存
java复制@Cacheable(value = "products", key = "#id")
public Product getProductById(Long id) {
// 这里是耗时的数据库查询
return productRepository.findById(id).orElse(null);
}
注意:默认情况下,如果方法返回null,是不会被缓存的。这经常导致新手困惑,明明加了注解却没效果。可以通过设置
unless = "#result == null"来改变这一行为。
2.2 @CachePut:强制更新缓存
当你想更新缓存而不是读取时,就该用它了。常见误区是认为它和@Cacheable可以互换,实际上它们有本质区别:
java复制@CachePut(value = "products", key = "#product.id")
public Product updateProduct(Product product) {
return productRepository.save(product);
}
有趣的是,@CachePut标注的方法每次都会执行,而@Cacheable只在缓存未命中时执行。我曾经在一个库存更新功能上错误使用了@Cacheable,结果缓存永远不更新,导致超卖事故。
2.3 @CacheEvict:精准清除缓存
删除操作通常需要同步清理缓存:
java复制@CacheEvict(value = "products", key = "#id")
public void deleteProduct(Long id) {
productRepository.deleteById(id);
}
更强大的功能是allEntries参数,可以清空整个缓存区域:
java复制@CacheEvict(value = "products", allEntries = true)
public void reloadAllProducts() {
// 重新加载所有产品的逻辑
}
3. 缓存键的玄机
缓存键的设计直接影响缓存命中率。Spring使用SpEL表达式来生成键,默认策略是:
- 如果没有参数,返回SimpleKey.EMPTY
- 如果只有一个参数,返回该参数
- 如果有多个参数,返回包含所有参数的SimpleKey
但默认策略往往不够用。比如分页查询:
java复制@Cacheable(value = "products", key = "#pageable.pageNumber + '-' + #pageable.pageSize")
public Page<Product> getProducts(Pageable pageable) {
//...
}
我遇到过的一个坑是:当参数是复杂对象时,一定要确保正确实现了equals和hashCode方法,否则缓存键可能不符合预期。
4. 条件化缓存的高级玩法
Spring Cache支持基于条件的缓存,这在实际项目中非常实用:
java复制@Cacheable(value = "products",
key = "#id",
condition = "#id > 1000",
unless = "#result.price < 500")
public Product getProduct(Long id) {
//...
}
这个例子展示了:
condition:只在id大于1000时缓存unless:当产品价格低于500时不缓存
我曾经用这个特性实现了"只缓存VIP用户数据"的需求,避免了普通用户数据占用宝贵缓存空间。
5. 多缓存管理策略
大型项目往往需要多种缓存策略并存。Spring支持配置多个CacheManager:
java复制@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager redisCacheManager() {
// Redis配置
}
@Bean
public CacheManager caffeineCacheManager() {
// Caffeine配置
}
@Primary
@Bean
public CacheManager compositeCacheManager() {
// 组合多个CacheManager
}
}
在方法上指定使用哪个CacheManager:
java复制@Cacheable(value = "products", cacheManager = "redisCacheManager")
public Product getProduct(Long id) {
//...
}
6. 缓存异常处理实战
缓存不是万能的,处理不当反而会成为系统的脆弱点。我总结了几个常见问题及解决方案:
-
缓存穿透:大量查询不存在的数据
- 解决方案:缓存空值,设置较短过期时间
-
缓存雪崩:大量缓存同时失效
- 解决方案:设置不同的过期时间
-
缓存击穿:热点key失效瞬间大量请求
- 解决方案:使用互斥锁
java复制@Cacheable(value = "products", key = "#id", sync = true)
public Product getProductWithSync(Long id) {
//...
}
设置sync = true可以让多个线程只有一个去执行方法,其他线程等待结果。
7. 性能监控与调优
没有监控的缓存就像没有仪表的赛车。Spring Boot Actuator提供了缓存指标:
properties复制management.endpoints.web.exposure.include=caches
通过/actuator/caches端点可以查看:
- 缓存名称列表
- 每个缓存的命中率
- 缓存大小等信息
在我的经验中,缓存命中率低于70%就需要重新评估缓存策略了。可以使用AOP记录更详细的缓存日志:
java复制@Aspect
@Component
public class CacheMonitorAspect {
@Around("@annotation(org.springframework.cache.annotation.Cacheable)")
public Object monitorCache(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
Object result = pjp.proceed();
long duration = System.currentTimeMillis() - start;
// 记录日志或指标
return result;
}
}
8. 与Spring Boot的完美集成
Spring Boot为缓存提供了自动配置,只需添加依赖:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
然后在启动类添加@EnableCaching注解。Spring Boot会按以下顺序尝试配置缓存:
- Generic
- JCache (JSR-107)
- EhCache 3.x
- Hazelcast
- Infinispan
- Couchbase
- Redis
- Caffeine
- Simple
可以通过配置指定具体实现:
properties复制spring.cache.type=redis
9. 实际项目中的经验之谈
经过多个项目实践,我总结了这些宝贵经验:
-
缓存粒度:不是越细越好。太细会导致缓存命中率低,太粗会导致缓存无效率高。通常按业务场景划分。
-
过期策略:结合业务特点设置。高频变化的数据设置短过期时间,静态数据可以设置长些。
-
序列化:使用Redis时,选择高效的序列化方式。推荐Jackson或Kryo。
-
本地缓存:对于极高频访问的数据,可以结合Caffeine做二级缓存。
-
测试:一定要测试缓存生效情况。我曾经因为错误配置导致缓存根本没生效,直到性能测试才发现。
java复制@Test
public void testCache() {
Product p1 = productService.getProduct(1L); // 查数据库
Product p2 = productService.getProduct(1L); // 应该走缓存
assertThat(p1).isSameAs(p2); // 对象引用相同
}
10. 常见问题排查指南
当缓存不按预期工作时,可以按照这个流程排查:
- 确认
@EnableCaching已添加 - 检查方法是否是public的(Spring AOP限制)
- 检查是否发生了自调用(AOP不生效)
- 查看缓存配置是否正确
- 检查key生成策略是否符合预期
- 查看condition/unless条件是否过滤了缓存
- 检查缓存实现是否正常(如Redis连接)
一个典型的自调用问题示例:
java复制public Product getProduct(Long id) {
return getProductWithCache(id); // 自调用,缓存不生效
}
@Cacheable("products")
public Product getProductWithCache(Long id) {
//...
}
解决方法是将缓存方法移到另一个Bean中。
