在互联网应用中,高频数据查询(如商品详情、用户信息)往往成为性能瓶颈。每次请求都触发数据库查询,不仅增加服务器压力,还会导致响应延迟。Spring 框架提供的@Cacheable注解,就像给方法加了一个 "智能备忘录",让重复请求直接从缓存中获取结果,瞬间提升系统性能。本文将通过实战案例,带您掌握这个高效的缓存工具。
一、@Cacheable 核心作用:让重复计算一键跳过
想象一个电商系统的商品查询接口:用户每次点击商品详情页,都要执行一次数据库查询。如果同一商品被频繁访问,数据库压力会急剧增加。这时@Cacheable就派上用场了:
@Service
public class ProductService {
@Cacheable(value = "products", key = "#id")
public Product getProductById(Long id) {
// 模拟耗时的数据库查询
return database.queryProduct(id); // 首次执行,结果存入缓存
}
}
当getProductById(1)第一次调用时,方法正常执行并将结果存入名为 "products" 的缓存,键为参数id的值。后续再次调用getProductById(1)时,Spring 会直接从缓存中返回结果,跳过方法体执行,响应时间从毫秒级缩短到纳秒级。
二、3 步快速上手:从配置到使用
1. 启用缓存功能
在 Spring 配置类中添加@EnableCaching注解,开启缓存支持:
@Configuration
@EnableCaching
public class CacheConfig { }
2. 选择缓存实现
- 内存缓存(默认):使用 Spring 自带的ConcurrentMapCacheManager,适合单体应用或简单场景。
- 分布式缓存:集成 Redis、Caffeine 等(后文会详解 Redis 集成)。
3. 标记目标方法
通过value指定缓存名称,key定义缓存键(支持 SpEL 表达式):
// 多缓存名称示例
@Cacheable(cacheNames = {"userCache", "commonCache"}, key = "#userId")
public User getUser(Long userId) { ... }
三、进阶用法:精准控制缓存策略
1. 条件缓存:按需存储结果
- 仅当参数满足条件时缓存:
@Cacheable(value = "products", condition = "#id > 0")
public Product getProduct(Long id) { ... } // id≤0时不缓存
- 排除特定结果不缓存:
@Cacheable(value = "products", unless = "#result.price < 100")
public Product getProduct(Long id) { ... } // 价格<100的商品不缓存
2. 自定义缓存键:避免参数冲突
当方法有多个参数时,默认键生成规则会使用所有参数。通过 SpEL 表达式可自定义键逻辑:
@Cacheable(value = "orders", key = "#userId + '-' + #orderType")
public List<Order> getOrders(Long userId, String orderType) {
// 键格式:123-DEFAULT(userId=123,orderType=DEFAULT)
}
3. 应对代理限制:解决同类方法调用问题
由于@Cacheable基于 AOP 代理实现,同类内部方法调用不会触发缓存。解决方法是注入自身代理:
@Service
public class ProductService {
@Autowired
private ProductService self; // 注入代理对象
public void internalCall(Long id) {
self.getProductById(id); // 通过代理调用,触发缓存
}
@Cacheable("products")
public Product getProductById(Long id) { ... }
}
四、缓存生态:与 @CacheEvict、@CachePut 联动
1. 清除缓存:数据更新时保持一致性
使用@CacheEvict在数据删除或修改时清除旧缓存:
@CacheEvict(value = "products", key = "#id")
public void deleteProduct(Long id) {
database.deleteProduct(id); // 删除数据库后,同步删除缓存
}
2. 更新缓存:写入时强制刷新
@CachePut用于写入操作,确保方法执行后更新缓存(即使参数未变化):
@CachePut(value = "products", key = "#product.id")
public Product updateProduct(Product product) {
database.update(product); // 数据库更新后,缓存同步更新
return product; // 返回值会存入缓存
}
五、实战:集成 Redis 实现分布式缓存
当应用扩展为分布式架构时,内存缓存无法共享,需使用 Redis 等外部缓存:
1. 添加依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2. 配置 Redis 连接(application.yml)
yaml
spring:
redis:
host: your-redis-host
port: 6379
password: your-password
database: 0
3. 直接使用 @Cacheable
无需修改方法注解,Spring 会自动切换为 Redis 缓存,且支持数据序列化(需确保返回对象实现Serializable接口)。
六、避坑指南:常见问题排查
- 缓存未生效?检查是否添加@EnableCaching方法必须为public(非 public 方法无法代理)避免同类内部直接调用(改用代理对象)
- 缓存数据不一致?数据更新时务必使用@CacheEvict或@CachePut复杂场景可结合CacheManager手动操作缓存
- 键冲突导致数据覆盖?确保key表达式唯一(尤其多参数场景)建议显式定义key,避免依赖默认规则
总结:何时使用 @Cacheable?
- 高频读、低频写场景(如商品详情、字典数据)
- 方法执行耗时较长(数据库查询、复杂计算)
- 结果在一段时间内稳定不变
合理使用缓存能显著减少数据库压力,但需注意缓存一致性和内存占用。结合@CacheEvict、@CachePut形成完整的缓存管理方案,配合 Redis 等分布式缓存,可轻松应对高并发场景。
如果您在实际项目中遇到缓存问题,或想了解更多优化技巧,欢迎在评论区留言讨论!关注我,获取更多 Spring 实战干货。