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) { ... }

✅ 支持组合 cacheableputevict,灵活度高。

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 时才缓存,避免缓存过短无意义的数据。

⚠️ conditionunless 可与所有缓存注解配合使用,是精细化控制的利器。


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


原始标题:A Guide To Caching in Spring