人人都会AI编程

14.1 Spring Cache 缓存抽象:注解式缓存开发与缓存注解

更新时间:2026-07-11

缓存是提升系统性能最直接的手段之一。Spring 从 3.1 版本开始提供了 Spring Cache Abstraction,它的设计目标非常明确:让开发者通过简单的注解声明就能使用缓存,而不需要在业务代码中编写缓存读写和清除相关的样板逻辑。与 Spring 的其他抽象层一样,Spring Cache 也遵循“面向接口编程”的原则——你的代码只依赖 CacheManager 和几个注解,底层实现可以自由切换(如 Caffeine、Redis、EhCache 等)。

14.1.1 缓存抽象的核心概念

在 Spring Cache 体系中,有两个最核心的概念:

  • Cache:表示一个具体的缓存区域(类似 Map),负责存储键值对并提供基本的存取、清除操作。每个 Cache 都有一个唯一名称,例如 "users""products"
  • CacheManager:缓存管理器,负责根据名称返回对应的 Cache 实例。Spring 允许同时存在多个 CacheManager,但通常只有一个作为默认。

简单理解:你的代码通过注解告诉 Spring“我要读/写名叫 users 的缓存”,Spring 通过 CacheManager 找到对应的 Cache 实例,然后执行操作。你的业务方法完全不用知道底层是内存缓存还是 Redis,甚至不用知道缓存是如何读写的。

14.1.2 启用缓存支持

与 Spring 的事务类似,要使用声明式缓存,必须先在一个配置类上添加 @EnableCaching 注解:

@Configuration
@EnableCaching
public class CacheConfig {
    // CacheManager 的 Bean 定义或自动配置
}

在实际项目中,如果你使用的是 Spring Boot,只需引入对应的缓存 Starter(如 spring-boot-starter-cache 配合 spring-boot-starter-data-redis),Spring Boot 的自动配置会自动检测 classpath 下的缓存实现并创建对应的 CacheManager,无需手动写 @EnableCaching 以外的代码。

14.1.3 核心缓存注解详解

Spring Cache 提供了五个直接的注解,以及一个元注解,用法非常平实清晰。

1. @Cacheable:触发缓存读取

@Cacheable 可标注在方法上,表示这个方法的返回结果应该被缓存。当相同参数再次调用时,直接从缓存中返回结果,跳过方法体实际的业务逻辑。

常用属性:

  • value / cacheNames:指定关联的 Cache 名称(也就是缓存区域的名称),可指定多个,例如 cacheNames = {"users", "topUsers"}
  • key:缓存的键,支持 SpEL 表达式,默认是方法的所有参数经过 SimpleKeyGenerator 生成的 key。你也可以明确指定,例如 key = "#userId"
  • condition:缓存的条件表达式,满足条件时才使用缓存,例如 condition = "#userId > 100"
  • unless:否决缓存的条件,满足条件时不缓存结果,例如 unless = "#result == null" 表示方法返回 null 时不写入缓存。
  • keyGenerator:自定义键生成器的 bean 名称,与 key 互斥。

示例:根据用户 ID 查询用户信息

@Service
public class UserService {
    @Cacheable(cacheNames = "users", key = "#userId")
    public User getUser(Long userId) {
        // 第一次调用会执行这里的数据库查询,后续调用命中缓存
        return userRepository.findById(userId).orElse(null);
    }
}

当第一次调用 getUser(1001L) 时,方法体执行,结果存入名为 users 的缓存中。后续再次用 1001L 调用时,Spring 直接返回缓存中的值,不会执行方法体。

2. @CachePut:强制更新缓存

@CachePut@Cacheable 最大的区别在于:它的方法体始终会执行,执行完毕后将返回值放入缓存。这适用于更新缓存的场景,例如用户更新了自己的信息后,需要刷新缓存。

属性与 @Cacheable 基本一致(cacheNameskeycondition 等),但因为始终会执行方法体,所以没有 unless 这个概念。

示例:更新用户信息后刷新缓存

@CachePut(cacheNames = "users", key = "#user.id")
public User updateUser(User user) {
    // 更新数据库,然后返回最新用户信息,返回值会放入缓存
    return userRepository.save(user);
}

调用 updateUser 后,数据库被更新,同时缓存中 users 区域对应该用户 id 的旧数据被新数据覆盖。

3. @CacheEvict:清除缓存

@CacheEvict 用于删除一条或多条缓存记录。通常用于删除数据、数据不一致等需要失效缓存的场合。

常用属性:

  • cacheNameskey:与上面相同,指定要清除的缓存区域和键。
  • allEntries:若设为 true,则忽略 key,清除该缓存区域中的所有条目(类似于清空整个缓存表)。通常用于数据发生整批变动时(如批量插入)。
  • beforeInvocation:默认为 false,表示在方法执行成功后再清除缓存;若设为 true,则在方法执行前就清除缓存,即使方法抛异常,缓存也已经被清除。

示例:删除用户时清除对应的缓存条目

@CacheEvict(cacheNames = "users", key = "#userId")
public void deleteUser(Long userId) {
    userRepository.deleteById(userId);
}

当调用 deleteUser 成功后,缓存 users 中 key 为 userId 的条目被移除。如果希望无论方法是否成功都清除缓存,可以设置 beforeInvocation = true

4. @Caching:组合多个缓存操作

当同一个方法需要执行多个缓存操作(比如同时使用 @Cacheable@CacheEvict,或者同时操作多个缓存区域)时,可以用 @Caching 将它们组合在一起。

示例:复杂的条件缓存

@Caching(
    cacheable = {@Cacheable(cacheNames = "users", key = "#userId")},
    evict = {@CacheEvict(cacheNames = "userList", allEntries = true)}
)
public User getUser(Long userId) {
    return userRepository.findById(userId).orElse(null);
}

这个方法在获取单个用户的同时,清空了名为 userList 的列表缓存,因为获取用户可能影响首页的列表展示。

5. @CacheConfig:类级别的缓存共享配置

如果某个类中所有方法都用到了同一个缓存区域,可以在这个类上用 @CacheConfig 统一指定,避免在每个方法上重复写 cacheNames

示例:

@Service
@CacheConfig(cacheNames = "products")
public class ProductService {
    @Cacheable(key = "#id")
    public Product getById(Long id) { ... }

    @CachePut(key = "#product.id")
    public Product update(Product product) { ... }

    @CacheEvict(key = "#id")
    public void delete(Long id) { ... }
}

类上的 cacheNames 配置被下方方法继承,各个方法只须单独指定 key,使注解更加简洁。注意,如果某个方法的注解中显式指明了 cacheNames,则覆盖类级别的配置。

14.1.4 SpEL 表达式在缓存注解中的使用

Spring 缓存注解的 keyconditionunless 属性都支持 SpEL(Spring Expression Language)。这让缓存的键生成和条件控制非常灵活。以下是一些常用的 SpEL 上下文变量:

  • #参数名:引用方法的参数,如 key = "#userId"
  • #result:在 @CachePut@Cacheableunless / condition 中引用方法返回结果。
  • #root.method:当前方法对象。
  • #root.target:当前被调用的目标对象。
  • #root.args:方法参数数组。

例如,要仅在非空结果时才缓存:

@Cacheable(cacheNames = "users", key = "#id", unless = "#result == null")
public User getUser(Long id) { ... }

14.1.5 实际项目中不可忽视的细节

1. 缓存的序列化机制

如果你的缓存后端是 Redis 等支持多种数据类型的中间件,需要指定序列化方式。Spring Cache 默认使用 JdkSerializationRedisSerializer,这要求被缓存的对象实现 Serializable 接口。在实际项目中,通常更倾向于使用 JSON 序列化器(如 Jackson2JsonRedisSerializer),方便人工查看和跨语言交互。这一点可通过自定义 RedisCacheConfiguration 完成。

2. 缓存穿透、击穿与雪崩

Spring Cache 本身不直接处理这些缓存高级问题,但你可以通过以下方式配合使用:

  • 缓存穿透:缓存不存在的数据。可以在 @Cacheable 中配合 unless = "#result == null",或添加空值缓存并设置短过期时间。
  • 缓存击穿:热点数据过期,大量并发请求打向数据库。一般通过加锁解决,但注解式缓存无法直接实现互斥锁,可改用 Redisson 等客户端提供的锁,或结合 @Cacheable(sync = true) 属性(从 Spring 4.3 开始支持),在该属性下,多个线程同时请求同一 key 时真正只有一个执行方法体,其他线程等待缓存填充完成,可以有效防止缓存击穿。
  • 缓存雪崩:大量缓存同时过期。可通过随机化的过期时间、缓存预热、高可用缓存集群等手段解决,这不属于 Spring Cache 注解层面的范畴。

3. 多个 CacheManager 与分层缓存

如果项目同时使用了多个缓存实现(比如既有 Redis 又有 Caffeine),可以通过 @Primary 指定默认 CacheManager,其他 Manager 在使用时需要在注解中指定 cacheManager 属性:

@Cacheable(cacheNames = "hotProducts", cacheManager = "caffeineCacheManager")
public List<Product> getHotProducts() { ... }

14.1.6 小结

Spring Cache 抽象真正做到了“业务代码与缓存逻辑解耦”。你只需关注业务方法本身,通过 @Cacheable@CachePut@CacheEvict@Caching 四个注解就能完成绝大多数缓存场景。配合 Spring Boot 的自动配置,一个可工作的缓存方案几乎零代码配置即可启动。在整个开发流程中,注解式缓存开发极大降低了认知负担,也避免了因手动编写缓存代码导致的重复和不一致。在下一节中,我们将进一步深入,讲解如何集成 Redis 作为缓存存储,并处理序列化策略、缓存过期策略等实际工程细节。