Spring Cache用法及常见问题解决方案

 更新时间:2025年09月11日 10:36:23   作者:何苏三月  
Spring Cache用法很简单,但你知道这中间的坑吗?今天我以一个亲身经历者的角度,系统的讲解SpringCache的用法,并且举例介绍使用SpringCache遇到的常见问题,感兴趣的朋友跟随小编一起看看吧

Spring Cache 作为 Spring 框架提供的缓存抽象层,确实能够显著简化项目中 Redis、Caffeine 等缓存技术的使用,但许多开发者在实际应用中会遇到各种"看似正确但缓存不生效"的问题。以下将系统性地分析这些问题的根源,并提供全面的解决方案。

这篇文章将会以一个亲身经历者的角度,系统的讲解SpringCache的用法,并且举例介绍使用SpringCache遇到的常见问题。

一、介绍

1.1 基本介绍

Spring Cache 是 Spring 框架提供的一个缓存抽象层,它通过在方法上添加简单的注解来实现缓存功能,从而减少重复计算,提高系统性能。

Spring Cache 利用了AOP,实现了基于注解的缓存功能,并且进行了合理的抽象,业务代码不用关心底层是使用了什么缓存框架,只需要简单地加一个注解,就能实现缓存功能了,做到了对代码侵入性做小。

由于市面上的缓存工具实在太多,SpringCache框架还提供了CacheManager接口,可以实现降低对各种缓存框架的耦合。它不是具体的缓存实现,它只提供一整套的接口和代码规范、配置、注解等,用于整合各种缓存方案,比如Redis、Caffeine、Guava Cache、Ehcache。

1.2 核心概念

(1)缓存抽象

Spring Cache 提供了一组通用的缓存抽象接口,主要包括:

  • Cache - 缓存接口,定义缓存操作
  • CacheManager - 缓存管理器,用于管理各种缓存组件

(2)主要注解

Spring Cache 通过以下注解提供声明式缓存:

  • @Cacheable - 表明方法的返回值可以被缓存
  • @CacheEvict - 表明方法会触发缓存的清除
  • @CachePut - 表明方法会更新缓存,但总会执行方法
  • @Caching - 组合多个缓存操作
  • @CacheConfig - 类级别的共享缓存配置

二、常见坑

2.1当一个类中的方法A(带有@Cacheable注解)被同一个类中的另一个方法B调用时,@Cacheable注解会失效,缓存机制不会起作用

(1)案例演示

package com.example.demo;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
public class Controller {
    @GetMapping("/test")
    public String get() {
        return innerGet();
    }
    @Cacheable(value = "inner")
    public String innerGet() {
        System.out.println("----------1111111111111------------");
        return "内部调用";
    }
}

 通过反复调用该接口,我们发现系统并没有命中缓存,而是每次都重复执行了innerGet()方法,redis缓存中也确实没有这个inner相关的key。

其实这个问题在我的idea中已经有警告提示了,如下图所示:

 意思是说

当一个类中的方法A(带有@Cacheable注解)被同一个类中的另一个方法B调用时,@Cacheable注解会失效,缓存机制不会起作用。

(2)原因分析

这是由于Spring AOP(面向切面编程)的实现方式导致的:

  • Spring的缓存功能是通过AOP代理实现的
  • 当方法从类外部调用时,会经过代理,缓存逻辑能正常执行
  • 但当方法从类内部调用时(自调用),会绕过代理直接调用,导致缓存逻辑被跳过

(3)解决办法

  1. 将缓存方法移到另一个类!!!(推荐)
  2. 自行注入自己(通过构造函数或@Autowired),然后再用这个注入的当前类的对象去调用这个方法。(不太推荐)

(4)引申

这种自调用失效的问题是Spring AOP的普遍现象,不仅限于@Cacheable,其他如@Transactional、@Async等注解也有同样的问题。

@Service
public class OrderService {
    public void placeOrder(Order order) {
        // 这里直接调用,@Transactional会失效
        updateInventory(order.getItems());  // 事务不会生效
        // 其他业务逻辑...
    }
    @Transactional
    public void updateInventory(List<Item> items) {
        // 更新库存操作
        items.forEach(item -> {
            inventoryRepository.reduceStock(item.getId(), item.getQuantity());
        });
    }
}

为什么失效?

AOP 代理机制:Spring 的事务管理是通过 AOP 代理实现的。当 placeOrder 直接调用 updateInventory 时,调用发生在目标对象内部,绕过了 Spring 创建的代理对象。因此事务拦截器没有机会介入,事务不会开启。

解决办法

解决办法是一样的,这里也是推荐拆分到不同服务类。

@Service
public class OrderService {
    @Autowired
    private InventoryService inventoryService;
    public void placeOrder(Order order) {
        inventoryService.updateInventory(order.getItems()); // 现在会走代理,事务生效
    }
}
@Service
public class InventoryService {
    @Transactional
    public void updateInventory(List<Item> items) {
        // 更新库存操作
    }
}

2.2 json序列化问题

(1)案例演示

存在乱码的情况,如:内部调用

(2)原因分析

  • 这是因为
  • Spring Cache 默认使用 JDK 序列化方式
  • JDK 序列化会产生二进制数据,导致 Redis 中显示乱码
  • 您看到的 内部调用 就是 JDK 序列化的结果

(3)解决办法

配置 CacheManager 使用 JSON 序列化

@EnableCaching
@Configuration
public class CacheConfig {
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
        return RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
    }
}

 这样就可以了,不信我们再写两个案例试试

@RestController
@RequestMapping("/api")
public class Controller {
    @Autowired
    private Controller controller;
    @GetMapping("/test")
    public String get() {
        return controller.innerGet();
    }
    @Cacheable(value = "inner")
    public String innerGet() {
        System.out.println("----------1111111111111------------");
        return "内部调用";
    }
    @GetMapping("/cacheUser")
    public User cacheUser() {
        return controller.getUser();
    }
    @Cacheable(value = "user")
    public User getUser() {
        System.out.println("----------2222222222222------------");
        User user = new User();
        user.setName("张三");
        user.setAge(18);
        user.setPassword("2342a18");
        user.setEmail("32432kjkj@1523.com");
        return user;
    }
}

(4)提醒

如果不使用springcache,想通过RedisTemplate直接编程式的操作redis的话,记得也需要配置一下RedisTemplate的序列化。否则也会有这样的问题。当然了,你还可以使用StringRedisTemplate,这在Redis入门教程中已经讲解过了,这里不在赘述。

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        // 使用 String 序列化 key
        template.setKeySerializer(new StringRedisSerializer());
        // 使用 Jackson 序列化 value
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }
}

三、正确使用教程⭐

3.1 导入依赖

这里的缓存我们用Redis。当然如果是其他缓存,请自行引入即可。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

3.2 缓存配置

这一步主要是进行序列化。

防止SpringCache注解加入缓存后,出现乱码的情况。因为Spring Cache 默认使用 JDK 序列化方式。另外对RedisTemplate也进行了JDK序列化自定义,除非你的项目一定不用编程式操作redis。

具体原因在第二章已经讲解了,这里只给出正确配置。

package com.example.demo;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@EnableCaching // 一定要开启缓存
@Configuration
public class CacheConfig {
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
        return RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
    }
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        // 使用 String 序列化 key
        template.setKeySerializer(new StringRedisSerializer());
        // 使用 Jackson 序列化 value
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }
}

3.3 常用注解

(1)@Cacheable

用于标记方法的返回值可以被缓存。

  • 常用属性:
    • value/cacheNames:指定缓存名称(value和cacheNames是互斥的,即只能指定其中一个,二选一,必填)
  • key:指定缓存的key。可以使用Spring Expression Language(SpEL)来编写key表达式,以实现动态键的生成
    • 默认情况下,若未显式指定key,Spring会使用方法的​​所有参数组合​​作为键(通过SimpleKeyGenerator生成)
    • keyGenerator冲突​​:keykeyGenerator属性不能同时使用,需二选一
  • condition:指定缓存条件
  • unless:否决缓存的条件
//​​ 参数作为键
@Cacheable(value = "userCache", key = "#id")  // 使用参数id作为键
public User getUserById(Long id) { ... }
// 若参数是对象,可访问其属性:
@Cacheable(value = "userCache", key = "#user.id")  // 使用user对象的id属性
public User find(User user) { ... }
// ​​多参数组合键
@Cacheable(value = "userCache", key = "#firstName + '-' + #lastName")
public User getUserByName(String firstName, String lastName) { ... }
// ​​方法信息作为键
@Cacheable(value = "userCache", key = "#root.methodName + #id")  // 方法名+参数
public User getUserById(Long id) { ... }
/*
SpEL支持的元数据​​
在SpEL表达式中,可通过以下变量生成键:
#root.methodName:当前方法名
#root.target:目标对象实例
#result:方法返回值(仅适用于unless或condition)
#参数名或#p0/#p1(按参数索引)
*/

(2)@CachePut

  • 总是执行方法,并将结果放入缓存
  • 通常用于更新操作后更新缓存
@CachePut(value="users", key="#user.id")
public User updateUser(User user) {...}

(3)@CacheEvict

  • 用于清除缓存
  • 常用属性:
    • allEntries:是否清除所有缓存(默认false)
    • beforeInvocation:是否在方法执行前清除(默认false)
@CacheEvict(value="users", key="#userId")
public void deleteUser(String userId) {...}

(4)@Caching

  • 用于组合多个缓存操作
@Caching(evict = {
    @CacheEvict(value="primary", key="#user.id"),
    @CacheEvict(value="secondary", key="#user.username")
})
public void updateUser(User user) {...}

3.4 注意事项

🔥方法A调用同类中带缓存的方法B时,若没有自行注入自己,则无法引入缓存。(Spring AOP的普遍现象

🔥未自定义序列化操作,则可能出现乱码现象

到此这篇关于Spring Cache用法很简单,但你知道这中间的坑吗?的文章就介绍到这了,更多相关Spring Cache用法内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • SpringCloud Ribbon负载均衡流程分析

    SpringCloud Ribbon负载均衡流程分析

    在Eureka注册中心中我们在添加完@LoadBalanced注解,即可实现负载均衡功能,现在一起探索一下负载均衡的原理(Ribbon),感兴趣的朋友一起看看吧
    2024-03-03
  • java获取压缩文件中的XML并解析保存到数据库

    java获取压缩文件中的XML并解析保存到数据库

    这篇文章主要为大家详细介绍了如何使用java实现获取压缩文件中的XML并解析保存到数据库,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下
    2025-06-06
  • Java文件基本操作总结

    Java文件基本操作总结

    今天给大家带来的是关于Java基础的相关知识,文章围绕着Java文件操作展开,文中有非常详细的介绍及代码示例,需要的朋友可以参考下
    2021-06-06
  • Spring Boot Starter中Bean 注册与属性绑定的两大机制最佳实践方案

    Spring Boot Starter中Bean 注册与属性绑定的两大机制最佳实践方案

    在 Spring Boot的世界里,Starter(启动器)是简化依赖管理和自动化配置的强大工具,本文将深入探讨 Spring Boot Starter 中 Bean 初始化的核心机制,并指出最佳实践,感兴趣的朋友跟随小编一起看看吧
    2025-10-10
  • SpringBoot中整合RabbitMQ(测试+部署上线最新完整)的过程

    SpringBoot中整合RabbitMQ(测试+部署上线最新完整)的过程

    本文详细介绍了如何在虚拟机和宝塔面板中安装RabbitMQ,并使用Java代码实现消息的发送和接收,通过异步通讯,可以优化性能,感兴趣的朋友一起看看吧
    2025-02-02
  • SpringBoot整合邮件发送的四种方法

    SpringBoot整合邮件发送的四种方法

    这篇文章主要介绍了SpringBoot整合邮件发送的四种方法,SpringBoot中集成了发送邮件的功能,本文做了进一步优化,需要的朋友可以参考下
    2023-03-03
  • SpringBoot 创建web项目并部署到外部Tomcat

    SpringBoot 创建web项目并部署到外部Tomcat

    本篇文章主要介绍了SpringBoot 创建web项目并部署到外部Tomcat,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-06-06
  • java中Object类4种方法详细介绍

    java中Object类4种方法详细介绍

    大家好,本篇文章主要讲的是java中Object类4种方法详细介绍,感兴趣的同学赶快来看一看吧,对你有帮助的话记得收藏一下,方便下次浏览
    2022-01-01
  • MyBatis-Plus Sequence主键的实现

    MyBatis-Plus Sequence主键的实现

    这篇文章主要介绍了MyBatis-Plus Sequence主键的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-12-12
  • SpringBoot注解生效原理全解析

    SpringBoot注解生效原理全解析

    Spring Boot注解自动生效依赖于Spring容器和处理机制,通过扫描、解析配置类生成BeanDefinition,再由容器实例化,实现@Autowired等注解的自动注入与组件注册,本文给大家介绍SpringBoot注解生效原理分析,感兴趣的朋友一起看看吧
    2025-09-09

最新评论