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微服务之Hystrix组件实现服务熔断的方法

    SpringCloud微服务之Hystrix组件实现服务熔断的方法

    微服务架构特点就是多服务,多数据源,支撑系统应用。这样导致微服务之间存在依赖关系。这篇文章主要介绍了SpringCloud微服务之Hystrix组件实现服务熔断的方法,需要的朋友可以参考下
    2019-08-08
  • SpringBoot中服务消费的实现

    SpringBoot中服务消费的实现

    本文主要介绍了SpringBoot中服务消费的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-07-07
  • Java I/O技术之文件操作详解

    Java I/O技术之文件操作详解

    这篇文章主要介绍了Java I/O技术之文件操作详解,需要的朋友可以参考下
    2014-07-07
  • logback配置中变量和include的应用方式

    logback配置中变量和include的应用方式

    这篇文章主要介绍了logback配置中变量和include的应用方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-08-08
  • MyBatis-Plus通用CRUD操作的实现

    MyBatis-Plus通用CRUD操作的实现

    MyBatis-Plus是基于MyBatis的增强工具,主要目的是简化MyBatis的使用并提升开发效率,它提供了通可以用CRUD操作、分页插件、多种插件支持、自动代码生成器等功能,感兴趣的可以了解一下
    2024-10-10
  • java 中JFinal getModel方法和数据库使用出现问题解决办法

    java 中JFinal getModel方法和数据库使用出现问题解决办法

    这篇文章主要介绍了java 中JFinal getModel方法和数据库使用出现问题解决办法的相关资料,需要的朋友可以参考下
    2017-04-04
  • Java基础之static的用法

    Java基础之static的用法

    这篇文章主要介绍了Java基础之static的用法,文中有非常详细的代码示例,对正在学习java基础的小伙伴们有很大的帮助,需要的朋友可以参考下
    2021-05-05
  • JDK动态代理提高代码可维护性和复用性利器

    JDK动态代理提高代码可维护性和复用性利器

    这篇文章主要为大家介绍了JDK动态代理提高代码可维护性和复用性利器,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-10-10
  • springboot如何获取request请求的原始url与post参数

    springboot如何获取request请求的原始url与post参数

    这篇文章主要介绍了springboot如何获取request请求的原始url与post参数问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-12-12
  • Java实现的打印螺旋矩阵算法示例

    Java实现的打印螺旋矩阵算法示例

    这篇文章主要介绍了Java实现的打印螺旋矩阵算法,结合完整实例形式详细分析了java打印螺旋矩阵的算法原理与实现技巧,需要的朋友可以参考下
    2019-10-10

最新评论