java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Spring Cache用法

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

作者:何苏三月

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 提供了一组通用的缓存抽象接口,主要包括:

(2)主要注解

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

二、常见坑

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(面向切面编程)的实现方式导致的:

(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)原因分析

(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

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

//​​ 参数作为键
@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

@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用法内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文