Spring Boot 如何整合 Spring Cache 实现缓存抽象?

来源:C语言教程作者:广州GEO公司头衔:草根站长
导读:本期聚焦于广州GEO公司创作的《Spring Boot 如何整合 Spring Cache 实现缓存抽象?》,敬请观看详情。缓存抽象是 Spring 框架提供的一套统一缓存访问机制,它把缓存操作从业务代码中解耦出来。Spring Boot 通过自动配置让这套机制几乎开箱即用。本文从缓存注解的底层代理逻辑切入,演示 @Cacheable、@CacheEvict、@CachePut 的组合用法,并对比 Redis 与本地缓存的接入差异。还会讨论自定义 Key 生成策略、缓存穿透与击穿的防护思路,以及多级缓存和条件缓存等进阶配置。文中包含完整代码示例和配置片段,适合正在从本地缓存迁移到分布式缓存的读者参考。代码以 Spring Boot 3.x 和 Java 17 为基准,但思路同样适用于 2.x 版本。

Spring 的缓存抽象并不负责具体缓存存储,而是提供一层统一的注解驱动模型。借助 AOP,业务方法在调用前后被拦截,缓存读写由代理完成。Spring Boot 自动配置进一步简化了接入步骤,只需引入对应缓存实现,配置连接信息,就能在方法上通过 @Cacheable 等注解声明缓存行为。

Spring Boot 如何整合 Spring Cache 实现缓存抽象?

一、缓存注解与代理执行模型

Spring Cache 的核心注解包括 @Cacheable、@CacheEvict、@CachePut 和 @Caching。@Cacheable 用于查询方法,方法被执行前会先检查缓存,命中则直接返回缓存值,未命中才执行方法体并将结果写入缓存。@CacheEvict 负责失效缓存,通常用在更新或删除操作上。@CachePut 与 @Cacheable 相反,它总会执行方法,并把返回结果更新到缓存中,适合需要刷新缓存但又不希望跳过方法逻辑的场景。

这些注解之所以能生效,是因为 Spring 在容器初始化阶段为标注了缓存注解的 Bean 创建了代理对象。实际调用进入代理后,由 CacheInterceptor 统一解析注解元数据,再交给 CacheAspectSupport 执行缓存读写。整个链路与事务管理类似,开发者只需要关注注解配置是否正确,底层对缓存 API 的调用细节全部被屏蔽。下面是一个最基础的查询缓存示例:

@Service
public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Cacheable(value = "user", key = "#id")
    public User getUserById(Long id) {
        return userRepository.findById(id).orElse(null);
    }
}

这里的 value 指定缓存名称,key 使用 SpEL 表达式取方法入参 id。首次调用 getUserById(1L) 会执行数据库查询并把结果放入名为 user 的缓存区域;再次使用相同参数调用时,方法体不会执行,直接从缓存返回对象。

二、Spring Boot 配置 Redis Cache

如果只使用本地简单缓存,Spring Boot 默认会基于 ConcurrentHashMap 创建 ConcurrentMapCacheManager。这种方式适合单机测试,但生产环境通常需要分布式缓存。Redis 是 Spring Cache 最常用的实现之一,接入后缓存数据可以跨实例共享,并且支持过期时间、序列化策略等更细粒度的控制。

在 Spring Boot 项目中,只需在 pom.xml 或 Gradle 构建脚本中加入 spring-boot-starter-data-redis 和 spring-boot-starter-cache 依赖。然后通过 application.yml 指定缓存类型为 Redis:

spring:
  cache:
    type: redis
  data:
    redis:
      host: localhost
      port: 6379
      password:
      database: 0

仅仅这样配置还不够,默认的 JDK 序列化方式要求缓存对象实现 Serializable 接口,生成的数据可读性差,占用空间也较大。实际项目更推荐使用 JSON 序列化方案。下面配置 RedisCacheManager 使用 GenericJackson2JsonRedisSerializer,并为不同缓存名称设置不同的过期时间:

@Configuration
public class RedisCacheConfig {

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
        RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(30))
                .disableCachingNullValues()
                .serializeKeysWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new GenericJackson2JsonRedisSerializer()));

        Map<String, RedisCacheConfiguration> cacheConfigs = new HashMap<>();
        cacheConfigs.put("user", defaultConfig.entryTtl(Duration.ofHours(2)));
        cacheConfigs.put("product", defaultConfig.entryTtl(Duration.ofMinutes(10)));

        return RedisCacheManager.builder(factory)
                .cacheDefaults(defaultConfig)
                .withInitialCacheConfigurations(cacheConfigs)
                .build();
    }
}

这段配置里,entryTtl 控制全局默认过期时间,withInitialCacheConfigurations 可以为不同命名空间单独设置 TTL。StringRedisSerializer 让键以可读字符串形式存储,GenericJackson2JsonRedisSerializer 把值序列化为 JSON,便于用 Redis 客户端直接查看和调试。需要注意 JSON 序列化会保留类型信息,反序列化时依赖类路径一致。

三、自定义 Key 与条件缓存策略

缓存 Key 的生成方式直接影响命中率。默认情况下,Spring 使用 SimpleKeyGenerator,当方法只有一个参数时,直接以该参数作为 Key;多个参数时则组合成 SimpleKey 对象。这种默认规则在参数顺序或对象状态变化时可能导致缓存错位,因此显式指定 SpEL 表达式是更稳妥的做法。

下面的示例展示了组合 Key 和条件缓存的使用:

@Cacheable(
    value = "product",
    key = "#category + ':' + #page + ':' + #size",
    condition = "#page >= 0 && #size > 0",
    unless = "#result == null"
)
public List<Product> listProducts(String category, int page, int size) {
    return productRepository.findByCategory(category, page, size);
}

这里 condition 在方法执行前判断,只有满足条件才走缓存逻辑;unless 在方法执行后判断,如果表达式成立,结果不会被写入缓存。例子中 page >= 0 和 size > 0 是条件缓存,返回值为 null 时不缓存,可以防止无效数据占据 Redis 空间。

如果 SpEL 表达式无法满足复杂场景,可以实现 KeyGenerator 接口,通过方法名、目标对象和入参自定义 Key 拼接规则。例如:

@Component("customKeyGenerator")
public class CustomKeyGenerator implements KeyGenerator {

    @Override
    public Object generate(Object target, Method method, Object... params) {
        return method.getName() + ":" + Arrays.toString(params);
    }
}

在注解中通过 keyGenerator = "customKeyGenerator" 引用即可。自定义 KeyGenerator 适合需要统一加版本号或租户标识的项目,避免每个注解都写一遍 SpEL 表达式。

四、缓存更新、失效与一致性防护

缓存读多写少的场景中,最难处理的是数据一致性。更新数据库后如果不及时清理缓存,后续请求可能拿到旧数据。Spring Cache 提供了 @CacheEvict 来处理失效操作。常见做法是在更新方法成功后删除对应缓存:

@CacheEvict(value = "user", key = "#user.id")
public User updateUser(User user) {
    return userRepository.save(user);
}

如果更新方法会同时影响多个缓存区域,可以使用 @Caching 组合多个失效规则。allEntries = true 可以清空整个命名空间,适合全量刷新场景:

@Caching(
    evict = {
        @CacheEvict(value = "user", key = "#user.id"),
        @CacheEvict(value = "userList", allEntries = true)
    }
)
public User updateUserProfile(User user) {
    return userRepository.save(user);
}

需要特别注意的是,@CacheEvict 默认在方法成功返回后执行。如果方法抛出异常,缓存不会被清理,这有利于事务一致性。但某些场景希望无论业务方法是否成功都清理缓存,可以设置 beforeInvocation = true。不过这种策略在事务回滚时可能导致缓存被提前删除,因此要结合具体业务谨慎使用。

在高并发访问下,缓存穿透和击穿是常见风险。缓存穿透指大量请求查询一个不存在的数据,缓存层无法命中,数据库压力骤增。可以在业务层对空结果设置短 TTL 占位缓存,或者使用布隆过滤器拦截非法 Key。缓存击穿则是某个热点 Key 在过期瞬间收到大量并发请求,同时回源数据库。可以通过互斥锁、逻辑过期或 sync = true 让 @Cacheable 在回源时加锁,降低击穿概率。Spring Cache 本身不提供完整的多级缓存方案,但可以在 CacheErrorHandler 中记录缓存操作异常,避免缓存中间件故障影响主流程。

总体来说,Spring Boot 整合 Spring Cache 的核心理念是注解声明加配置驱动。对查询方法用 @Cacheable,对更新方法用 @CachePut 或 @CacheEvict,再通过 Redis 或本地缓存实现具体存储。Key 策略和序列化方案需要根据数据规模、并发量以及团队维护习惯提前规划,否则后期调整成本会比较高。

Spring BootSpring Cache缓存抽象修改时间:2026-09-24 18:42:23

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0924/61417.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。