2. Spring Boot缓存抽象:Spring Cache抽象层介绍、@Cacheable/@CachePut/@CacheEvict注解详解、缓存管理器(CacheManager)配置

2.1 Spring Cache抽象层设计理念

Spring Framework从3.1版本开始引入缓存抽象层(Spring Cache Abstraction),其核心设计思想是:将缓存操作与业务逻辑解耦,通过声明式注解实现缓存读写,开发者无需直接操作底层缓存客户端(如Redis、Ehcache、Caffeine等)。

抽象层位于应用程序与具体缓存实现之间,提供统一的编程模型。其架构层次如下:

Spring Cache 抽象层架构 业务层(Service / Controller) @Cacheable / @CachePut / @CacheEvict 注解声明层 CacheManager 接口 + Cache 接口 核心抽象层(缓存管理器 + 缓存对象) 具体缓存实现 RedisCacheManager / EhCacheCacheManager / CaffeineCacheManager / ConcurrentMapCacheManager 核心原则:业务代码只与注解和CacheManager接口耦合,底层缓存实现可无缝切换
设计要点:Spring Cache抽象层遵循“面向接口编程”原则,CacheManager和Cache是核心接口,所有缓存实现都实现这两个接口。业务代码通过注解声明缓存行为,运行时由AOP切面拦截方法调用,自动执行缓存读写。

2.2 核心注解详解

2.2.1 @Cacheable — 缓存读取注解

@Cacheable用于标记方法,表示方法的返回值可以被缓存。当方法被调用时,Spring先检查缓存中是否存在对应key的数据,如果存在则直接返回缓存值,不执行方法体;如果不存在则执行方法,并将返回值存入缓存。

属性类型默认值说明
value / cacheNamesString[]—缓存名称,指定缓存区域,可指定多个
keyString方法参数组合SpEL表达式,自定义缓存key
keyGeneratorString—自定义key生成器的Bean名称
cacheManagerString—指定使用的缓存管理器Bean名称
conditionString—SpEL条件表达式,满足条件才缓存
unlessString—SpEL条件表达式,满足条件时不缓存
syncbooleanfalse是否同步模式,防止缓存击穿
// 基础用法:缓存用户查询结果
@Cacheable(value = "users", key = "#id")
public User getUserById(Long id) {
    // 模拟数据库查询
    return userRepository.findById(id).orElse(null);
}

// 多条件组合key
@Cacheable(value = "users", key = "#name + ':' + #age")
public List<User> findUsersByNameAndAge(String name, Integer age) {
    return userRepository.findByNameAndAge(name, age);
}

// 条件缓存:仅当userId大于1000时才缓存
@Cacheable(value = "users", condition = "#id > 1000")
public User getSpecialUser(Long id) {
    return userRepository.findById(id).orElse(null);
}

// 排除缓存:返回值为null时不缓存
@Cacheable(value = "users", unless = "#result == null")
public User getUserOrNull(Long id) {
    return userRepository.findById(id).orElse(null);
}

// 同步模式:防止缓存击穿(高并发场景推荐)
@Cacheable(value = "users", key = "#id", sync = true)
public User getUserSync(Long id) {
    return userRepository.findById(id).orElse(null);
}
最佳实践:高并发场景下务必开启sync = true,Spring会通过锁机制保证同一时刻只有一个线程执行方法体,其他线程等待缓存结果,有效防止缓存击穿。

2.2.2 @CachePut — 缓存更新注解

@CachePut始终执行方法体,并将返回值更新到缓存中。与@Cacheable不同,它不会检查缓存是否存在,每次都会执行方法。常用于数据更新操作后刷新缓存。

属性类型默认值说明
value / cacheNamesString[]—缓存名称,指定缓存区域
keyString方法参数组合SpEL表达式,自定义缓存key
conditionString—满足条件才更新缓存
unlessString—满足条件时不更新缓存
// 更新用户信息后刷新缓存
@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
    return userRepository.save(user);
}

// 条件更新:仅当返回值不为null时更新缓存
@CachePut(value = "users", key = "#id", unless = "#result == null")
public User updateUserName(Long id, String newName) {
    User user = userRepository.findById(id).orElse(null);
    if (user != null) {
        user.setName(newName);
        return userRepository.save(user);
    }
    return null;
}

2.2.3 @CacheEvict — 缓存驱逐注解

@CacheEvict用于删除缓存数据,通常在数据删除或批量更新时使用。可以删除单个缓存条目,也可以清空整个缓存区域。

属性类型默认值说明
value / cacheNamesString[]—缓存名称,指定缓存区域
keyString—SpEL表达式,指定要删除的缓存key
allEntriesbooleanfalse是否清空整个缓存区域
beforeInvocationbooleanfalse是否在方法执行前清除缓存
conditionString—满足条件才清除缓存
// 删除单个缓存条目
@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) {
    userRepository.deleteById(id);
}

// 清空整个users缓存区域
@CacheEvict(value = "users", allEntries = true)
public void clearAllUsersCache() {
    // 方法体可以为空,仅用于触发缓存清空
}

// 方法执行前清除缓存(防止方法执行失败导致缓存未清除)
@CacheEvict(value = "users", key = "#id", beforeInvocation = true)
public void deleteUserWithPreEvict(Long id) {
    userRepository.deleteById(id);
}

// 组合注解:更新时先驱逐旧缓存再写入新缓存
@Caching(evict = {
    @CacheEvict(value = "users", key = "#user.id"),
    @CacheEvict(value = "userLists", allEntries = true)
})
public User updateUserWithEvict(User user) {
    return userRepository.save(user);
}
注意:@CacheEvict默认在方法执行成功后清除缓存。如果方法抛出异常,缓存不会被清除。设置beforeInvocation = true可以在方法执行前清除缓存,但需注意如果方法执行失败,缓存已被清除,下次查询会重新加载数据。

2.2.4 @Caching — 组合注解

当需要同时应用多个缓存注解时,使用@Caching进行组合。例如:更新数据时需要同时更新多个缓存区域,或者同时执行缓存更新和缓存驱逐。

@Caching(
    put = {
        @CachePut(value = "users", key = "#user.id"),
        @CachePut(value = "userNames", key = "#user.name")
    },
    evict = {
        @CacheEvict(value = "userLists", allEntries = true)
    }
)
public User saveUser(User user) {
    return userRepository.save(user);
}

2.3 缓存管理器(CacheManager)配置

CacheManager是Spring Cache抽象层的核心管理器接口,负责创建和管理Cache实例。Spring Boot通过自动配置为常见缓存实现提供默认的CacheManager,也支持手动配置实现精细控制。

2.3.1 常用CacheManager实现对比

实现类底层存储适用场景特点
ConcurrentMapCacheManagerConcurrentHashMap单机开发/测试简单、无序列化、数据不持久
CaffeineCacheManagerCaffeine(内存)单机高性能支持过期、最大容量、统计
RedisCacheManagerRedis分布式缓存支持持久化、集群、TTL
EhCacheCacheManagerEhcache(磁盘+内存)Java EE传统项目支持磁盘持久化、XML配置
JCacheCacheManagerJCache (JSR-107)标准兼容遵循JCache规范

2.3.2 配置RedisCacheManager(最常用方案)

@Configuration
@EnableCaching
public class RedisCacheConfig {

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        // 默认缓存配置:全局TTL 10分钟
        RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(10))
            .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
            .disableCachingNullValues();

        // 为不同缓存区域设置不同TTL
        Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
        cacheConfigurations.put("users", RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(30))
            .disableCachingNullValues());
        cacheConfigurations.put("sessions", RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofHours(2))
            .disableCachingNullValues());
        cacheConfigurations.put("configs", RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofDays(1))
            .disableCachingNullValues());

        return RedisCacheManager.builder(connectionFactory)
            .cacheDefaults(defaultConfig)
            .withInitialCacheConfigurations(cacheConfigurations)
            .transactionAware()  // 支持事务
            .build();
    }
}

2.3.3 配置CaffeineCacheManager(单机高性能方案)

@Configuration
@EnableCaching
public class CaffeineCacheConfig {

    @Bean
    public CaffeineCacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        // 默认配置:最大容量1000,写入后10分钟过期
        cacheManager.setCaffeine(Caffeine.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .recordStats());  // 开启统计
        // 支持按缓存名称自定义配置
        cacheManager.setCacheNames(Arrays.asList("users", "sessions", "configs"));
        return cacheManager;
    }

    // 更精细的配置:为不同缓存区域设置不同策略
    @Bean
    public CacheManager customCaffeineCacheManager() {
        SimpleCacheManager cacheManager = new SimpleCacheManager();
        List<Cache> caches = new ArrayList<>();

        caches.add(new CaffeineCache("users", Caffeine.newBuilder()
            .maximumSize(5000)
            .expireAfterWrite(30, TimeUnit.MINUTES)
            .build()));

        caches.add(new CaffeineCache("sessions", Caffeine.newBuilder()
            .maximumSize(2000)
            .expireAfterWrite(2, TimeUnit.HOURS)
            .build()));

        cacheManager.setCaches(caches);
        return cacheManager;
    }
}

2.3.4 多缓存管理器共存配置

实际项目中可能需要同时使用多种缓存实现,例如:热点数据使用Caffeine本地缓存,全量数据使用Redis分布式缓存。Spring Cache支持配置多个CacheManager,通过@Cacheable的cacheManager属性指定。

@Configuration
@EnableCaching
public class MultiCacheManagerConfig {

    @Primary
    @Bean
    public CacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(10))
            .disableCachingNullValues();
        return RedisCacheManager.builder(connectionFactory)
            .cacheDefaults(config)
            .build();
    }

    @Bean
    public CacheManager caffeineCacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager("hotData");
        cacheManager.setCaffeine(Caffeine.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(5, TimeUnit.MINUTES));
        return cacheManager;
    }
}

// 使用示例:热点数据使用Caffeine本地缓存
@Cacheable(value = "hotData", key = "#id", cacheManager = "caffeineCacheManager")
public HotData getHotData(Long id) {
    return hotDataRepository.findById(id).orElse(null);
}

// 全量数据使用Redis分布式缓存
@Cacheable(value = "users", key = "#id", cacheManager = "redisCacheManager")
public User getUser(Long id) {
    return userRepository.findById(id).orElse(null);
}
多级缓存架构提示:在多缓存管理器场景下,建议使用@Primary标记主缓存管理器。业务代码中通过cacheManager属性显式指定,避免歧义。实际生产环境中,可结合Caffeine作为一级缓存、Redis作为二级缓存,实现“本地缓存+分布式缓存”的多级缓存架构,兼顾性能与一致性。

2.4 缓存注解执行流程

Spring Cache通过AOP切面实现缓存逻辑,其核心执行流程如下:

Spring Cache 注解执行流程 方法调用 AOP切面拦截 解析注解元数据 注解类型 判断 @Cacheable 检查缓存是否存在 存在则返回,不存在则执行 @CachePut / @CacheEvict 执行方法体 更新/清除缓存 返回结果
执行顺序总结:
  1. @Cacheable:先查缓存 → 命中则直接返回 → 未命中则执行方法 → 将结果存入缓存 → 返回结果
  2. @CachePut:直接执行方法 → 将结果存入缓存 → 返回结果(每次都会更新缓存)
  3. @CacheEvict:(默认)先执行方法 → 清除缓存 → 返回结果;(beforeInvocation=true)先清除缓存 → 执行方法 → 返回结果

2.5 缓存Key生成策略

Spring Cache默认使用SimpleKeyGenerator生成缓存key,规则如下:

  • 无参数方法:使用SimpleKey.EMPTY
  • 单个参数方法:直接使用该参数作为key
  • 多个参数方法:使用SimpleKey对象包装所有参数

自定义key生成器:

// 自定义KeyGenerator
@Component("customKeyGenerator")
public class CustomKeyGenerator implements KeyGenerator {
    @Override
    public Object generate(Object target, Method method, Object... params) {
        // 格式:类名.方法名:参数1_参数2
        return target.getClass().getSimpleName() + "." + method.getName() 
               + ":" + Arrays.stream(params).map(String::valueOf).collect(Collectors.joining("_"));
    }
}

// 使用自定义KeyGenerator
@Cacheable(value = "users", keyGenerator = "customKeyGenerator")
public User findUser(Long id, String name) {
    // 生成的key示例:UserService.findUser:100_张三
    return userRepository.findByIdAndName(id, name);
}
SpEL常用表达式示例:
表达式说明
#id方法参数id
#user.id参数对象的属性
#result方法返回值(仅用于unless)
#root.methodName当前方法名
#root.targetClass目标类
#root.args[0]第一个参数
#name + ':' + #age字符串拼接

2.6 常见问题与避坑指南

⚠️ 内部方法调用失效:Spring Cache基于AOP代理实现,只有通过代理对象调用的方法才会触发缓存逻辑。同一个类中的方法直接调用(如this.getUser())不会经过代理,缓存注解失效。

解决方案:
  • 将缓存方法抽取到独立的Service类中
  • 注入自身代理:@Autowired private UserService self; 然后调用self.getUser()
  • 使用AopContext.currentProxy()获取当前代理对象
✅ 推荐实践清单:
  • 所有缓存注解方法必须为public,private/protected方法不会触发缓存
  • 缓存key尽量使用业务唯一标识,避免使用对象引用
  • 合理设置TTL,避免缓存雪崩(建议加随机偏移量)
  • 写操作(增删改)务必同步清除或更新相关缓存
  • 使用@Cacheable(sync=true)应对高并发缓存击穿
  • 生产环境禁用ConcurrentMapCacheManager,使用Redis或Caffeine