1. 什么是缓存抽象?
在本教程中,我们将深入掌握 Spring 的缓存抽象机制,并通过实际案例提升系统性能。
我们会为一些真实场景的方法添加缓存支持,并探讨如何通过合理的缓存管理策略,显著优化方法调用的效率。缓存是提升响应速度的“利器”,但用不好也会踩坑,比如数据不一致、内存溢出等问题。所以,关键在于“智能管理”。
2. 准备工作
Spring 的核心缓存功能定义在 spring-context
模块中。如果你使用 Maven,只需引入以下依赖:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.3</version>
</dependency>
但注意,还有一个增强模块叫 spring-context-support
,它基于 spring-context
构建,并额外支持 EhCache、Caffeine 等第三方缓存实现。如果你打算用这些缓存框架,应该引入它:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>5.3.3</version>
</dependency>
✅ 提示:spring-context-support
会自动依赖 spring-context
,无需重复声明。
2.1 Spring Boot 场景
如果你用的是 Spring Boot,事情更简单——直接引入缓存 Starter 即可:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
<version>2.4.0</version>
</dependency>
这个 Starter 内部已经包含了 spring-context-support
,缓存功能开箱即用。
3. 启用缓存
Spring 使用注解来开启缓存功能,风格统一,简单粗暴。
只需要在任意一个配置类上加上 @EnableCaching
注解:
@Configuration
@EnableCaching
public class CachingConfig {
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("addresses");
}
}
当然,你也可以用 XML 配置实现相同效果:
<beans>
<cache:annotation-driven />
<bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
<property name="caches">
<set>
<bean
class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"
name="addresses"/>
</set>
</property>
</bean>
</beans>
⚠️ 注意:启用缓存后,必须注册一个 CacheManager
Bean,否则会报错。这是最基础的“踩坑点”。
3.1 Spring Boot 中的缓存配置
在 Spring Boot 中,只要类路径下有 spring-boot-starter-cache
,并且加了 @EnableCaching
,就会自动配置一个 ConcurrentMapCacheManager
,无需手动声明。
如果想自定义缓存名称,可以使用 CacheManagerCustomizer
:
@Component
public class SimpleCacheCustomizer
implements CacheManagerCustomizer<ConcurrentMapCacheManager> {
@Override
public void customize(ConcurrentMapCacheManager cacheManager) {
cacheManager.setCacheNames(Arrays.asList("users", "transactions"));
}
}
Spring Boot 的 CacheAutoConfiguration
会在 CacheManager
初始化前自动应用这些定制器,非常方便。
4. 使用注解进行缓存操作
启用缓存后,下一步就是通过注解将缓存行为绑定到具体方法上。
4.1 @Cacheable
最常用的注解,标记方法结果可缓存。执行时先查缓存,命中则直接返回,不执行方法:
@Cacheable("addresses")
public String getAddress(Customer customer) {
// 模拟耗时操作
return customer.getAddress();
}
✅ 支持多个缓存名:
@Cacheable({"addresses", "directory"})
public String getAddress(Customer customer) { ... }
只要任意一个缓存命中,就返回结果,避免重复计算。
4.2 @CacheEvict
用于清除缓存,防止缓存膨胀或数据过期。典型场景是更新或删除操作后清理相关缓存。
@CacheEvict(value = "addresses", allEntries = true)
public void updateAddress(Customer customer, String newAddress) {
customer.setAddress(newAddress);
}
allEntries = true
:清空整个缓存区- 默认只清空当前 key 对应的条目
⚠️ 不要滥用 allEntries=true
,否则缓存命中率会暴跌。
4.3 @CachePut
这个注解有点特殊:方法一定会执行,执行完后把结果放入缓存。适用于“先更新数据库,再更新缓存”的场景:
@CachePut(value = "addresses", key = "#customer.id")
public String updateAddress(Customer customer) {
// 实际更新逻辑
return customer.getAddress();
}
✅ 与 @Cacheable
的关键区别:
注解 | 方法是否执行 | 缓存是否更新 |
---|---|---|
@Cacheable |
否(缓存命中时) | 是(未命中时) |
@CachePut |
是(总是执行) | 是(总是更新) |
4.4 @Caching
Java 不允许对同一方法重复使用相同注解。比如你想同时清除多个缓存,直接写两个 @CacheEvict
会编译失败:
// ❌ 编译错误
@CacheEvict("addresses")
@CacheEvict("directory")
public void clearCaches() { ... }
正确做法是使用 @Caching
进行分组:
@Caching(evict = {
@CacheEvict("addresses"),
@CacheEvict(value = "directory", key = "#customer.name")
})
public void clearCaches(Customer customer) { ... }
✅ 支持组合 cacheable
、put
、evict
,灵活度高。
4.5 @CacheConfig
如果一个类中多个方法共用相同的缓存配置,可以在类上使用 @CacheConfig
统一设置,避免重复:
@CacheConfig(cacheNames = {"addresses"})
public class CustomerDataService {
@Cacheable
public String getAddress(Customer customer) { ... }
@CachePut
public String updateAddress(Customer customer) { ... }
}
这样方法上就不用再写 value = "addresses"
了,简洁不少。
5. 条件缓存
有时候我们不希望无差别缓存所有结果,而是根据条件动态决定。
5.1 condition
参数
通过 SpEL 表达式控制是否启用缓存,基于输入参数判断:
@CachePut(value = "addresses", condition = "#customer.name == 'Tom'")
public String getAddress(Customer customer) { ... }
只有当客户名字是 Tom 时,才更新缓存。
5.2 unless
参数
与 condition
相反,unless
是基于方法返回值来决定是否缓存:
@CachePut(value = "addresses", unless = "#result.length() < 64")
public String getAddress(Customer customer) { ... }
✅ 只有当地址长度 ≥64 时才缓存,避免缓存过短无意义的数据。
⚠️ condition
和 unless
可与所有缓存注解配合使用,是精细化控制的利器。
6. 基于 XML 的声明式缓存
如果你无法修改源码,或者想通过外部配置注入缓存逻辑,可以使用 XML 配置:
<!-- 目标服务 -->
<bean id="customerDataService"
class="com.example.service.CustomerDataService"/>
<!-- 缓存管理器 -->
<bean id="cacheManager"
class="org.springframework.cache.support.SimpleCacheManager">
<property name="caches">
<set>
<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" name="directory"/>
<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" name="addresses"/>
</set>
</property>
</bean>
<!-- 定义缓存切面 -->
<cache:advice id="cachingBehavior" cache-manager="cacheManager">
<cache:caching cache="addresses">
<cache:cacheable method="getAddress" key="#customer.name"/>
</cache:caching>
</cache:advice>
<!-- 绑定切面到目标类 -->
<aop:config>
<aop:advisor advice-ref="cachingBehavior"
pointcut="execution(* com.example.service.CustomerDataService.*(..))"/>
</aop:config>
这种方式适合遗留系统改造,但现代项目更推荐注解方式,更直观。
7. 基于 Java 的配置
等价的 Java 配置如下:
@Configuration
@EnableCaching
public class CachingConfig {
@Bean
public CacheManager cacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
cacheManager.setCaches(Arrays.asList(
new ConcurrentMapCache("directory"),
new ConcurrentMapCache("addresses")));
return cacheManager;
}
}
服务类:
@Component
public class CustomerDataService {
@Cacheable(value = "addresses", key = "#customer.name")
public String getAddress(Customer customer) {
return customer.getAddress();
}
}
8. 总结
本文系统介绍了 Spring 缓存抽象的核心机制:
- ✅ 使用
@EnableCaching
开启缓存 - ✅ 四大核心注解:
@Cacheable
、@CachePut
、@CacheEvict
、@Caching
- ✅ 支持条件缓存(
condition
/unless
) - ✅ 类级别配置
@CacheConfig
- ✅ 支持 XML 和 Java 配置
缓存虽好,但要警惕数据一致性问题。建议结合具体业务场景,合理设置缓存过期、更新策略,避免“缓存雪崩”、“穿透”等经典问题。
完整代码示例可在 GitHub 项目中查看:https://github.com/eugenp/tutorials/tree/master/spring-boot-modules/spring-caching