SpringBoot+SpringCache+Redis整合

本文详细介绍了SpringBoot中SpringCache的使用,包括依赖、配置、@Cacheable、@CacheEvict、Redis集成及自定义序列化。通过实例展示了缓存的启用、key生成、条件过滤、缓存清除和更新,以及Redis作为缓存存储的配置。此外,还讨论了@CacheConfig、@Caching等高级用法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. SpringCache

Springboot中的缓存Cache和CacheManager原理介绍

Spring Cache学习

1.1 SpringCache 依赖

​ Spring 3.1 引入了激动人心的基于凝视(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(比如EHCache 或者 OSCache,Redis等),而是一个对缓存使用的抽象,通过在既有代码中加入少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果。

​ Spring 的缓存技术还具备相当的灵活性。不仅能够使用 SpEL(Spring Expression Language)来定义缓存的 key 和各种 condition,还提供开箱即用的缓存暂时存储方案,也支持和主流的专业缓存比如 EHCache,OSCache,Redis等集成。

​ Spring 的缓存技术还具备相当的灵活性。不仅能够使用 SpEL(Spring Expression Language)来定义缓存的 key 和各种 condition,还提供开箱即用的缓存暂时存储方案,也支持和主流的专业缓存比如 EHCache 集成。

其特点总结例如以下:

  • 通过少量的配置 annotation 凝视就可以使得既有代码支持缓存
  • 支持开箱即用 Out-Of-The-Box,即不用安装和部署额外第三方组件就可以使用缓存
  • 支持 Spring Express Language,能使用对象的不论什么属性或者方法来定义缓存的 key 和 condition
  • 支持 AspectJ,并通过事实上现不论什么方法的缓存支持
  • 支持自己定义 key 和自己定义缓存管理者,具有相当的灵活性和扩展性

使用Spring Cache首先引入依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

1.1.1 SpringCache的好处

​ SpringCache包含两个顶级接口,Cache(缓存)和CacheManager(缓存管理器)。顾名思义,用CacheManager去管理一堆Cache。

最最关键的地方:抱紧了Spring的大腿,可以使用注解就能完成数据进入缓存!!

2. SpringCache使用

2.1 @EnableCaching

​ 该注解是最基础的注解,用于启用Spring的缓存管理功能,类似于Spring XML配置中的cache:annotation-driven 标签。

​ 在启动类上开启 @EnableCaching。

@SpringBootApplication
@EnableCaching
public class SpringCacheApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringCacheApplication.class, args);
    }
}

2.2 @Cacheable

​ @Cacheable 注解在方法上,表示该方法的返回结果是可以缓存的。也就是说,该方法的返回结果会放在缓存中,以便于以后使用相同的参数调用该方法时,会返回缓存中的值,而不会实际执行该方法。

​ **注意,这里强调了一点:参数相同。**这一点应该是很容易理解的,因为缓存不关心方法的执行逻辑,它能确定的是:对于同一个方法,如果参数相同,那么返回结果也是相同的。但是如果参数不同,缓存只能假设结果是不同的,所以对于同一个方法,你的程序运行过程中,使用了多少种参数组合调用过该方法,理论上就会生成多少个缓存的 key(当然,这些组合的参数指的是与生成 key 相关的)。下面来了解一下 @Cacheable 的一些参数:

cacheNames & value

​ @Cacheable 提供两个参数来指定缓存名:value、cacheNames ,二者选其一即可。这是 @Cacheable 最简单的用法示例:

@Service("helloService")
@Log4j2
public class HelloServiceImpl {
    @Cacheable(value = "sayHello")
    public String sayHello(){
        System.out.println("call sayHello...");
        return "hello";
    }
}

在这个例子中,sayHello 方法与一个名为 sayHello 的缓存关联起来了。调用该方法时,会检查 sayHello缓存,如果缓存中有结果,就不会去执行方法了。

@Resource
private HelloServiceImpl helloService;

@Test
void springCacheTest01(){
    System.out.println(helloService.sayHello());
    System.out.println(helloService.sayHello());
    System.out.println(helloService.sayHello());
}

​ 结果为:

call sayHello...
hello
hello
hello

​ 其实,按照官方文档,@Cacheable 支持同一个方法关联多个缓存。这种情况下,当执行方法之前,这些关联的每一个缓存都会被检查,而且只要至少其中一个缓存命中了,那么这个缓存中的值就会被返回。如果缓存不存在则调用方法,并将结果同时缓存在每一个关联的缓存里面,示例:

@Service
public class HelloService {
    @Cacheable(value = {"sayHello","sayHello2",})
    public String sayHello(String name){
        System.out.println("call sayHello...");
        return "hello";
    }
}

key & keyGenerator

​ 一个缓存名对应一个被注解的方法,但是一个方法可能传入不同的参数,那么结果也就会不同,这应该如何区分呢?这就需要用到 key 。在 spring 中,key 的生成有两种方式:显式指定和使用 keyGenerator 自动生成。

最后生成的Key为cacheNames :: key,比如sayHello::sha256(自定义shaKeyGenerator 生成的256值)

2.2.1 KeyGenerator 自动生成

​ 当我们在声明 @Cacheable 时不指定 key 参数,则该缓存名下的所有 key 会使用 KeyGenerator 根据参数 自动生成。spring 有一个默认的 SimpleKeyGenerator ,在 spring boot 自动化配置中,这个会被默认注入。生成规则如下:

a. 如果该缓存方法没有参数,返回 SimpleKey.EMPTY ;

b. 如果该缓存方法有一个参数,返回该参数的实例 ;

c. 如果该缓存方法有多个参数,返回一个包含所有参数的 SimpleKey ;

​ 默认的 key 生成器要求参数具有有效的 hashCode() 和 equals() 方法实现。另外,keyGenerator 也支持自定义, 并通过 keyGenerator 来指定。

​ 不过我们也可以继承CachingConfigurerSupport来配置自定义KeyGenerator方法如下:

​ 首先导入依赖:

<!--  fastJson序列化依赖 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.75</version>
</dependency>
<!--  用于摘要运算、编码解码的包    -->
<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.11</version>
</dependency>
@Configuration
public class CacheConfig extends CachingConfigurerSupport {
    /**
     *  自定义缓存 key 生成策略
     */
    @Bean
    @Override
    public KeyGenerator keyGenerator() {
        return (target, method, params) -> {
            Map<String,Object> container = new HashMap<>(3);
            Class<?> targetClass = target.getClass();
            // 类地址
            container.put("class",targetClass.toGenericString());
            // 方法名称
            container.put("methodName",method.getName());
            // 包名称
            container.put("package",targetClass.getPackage());
            // 参数列表
            for(int i=0; i<params.length; i++){
                container.put(String.valueOf(i),params[i]);
            }
            // 转为 JSON 字符串
            String jsonString = JSON.toJSONString(container);
            // System.out.println(jsonString);
            // 做 SHA256 哈希得到一个 SHA256 摘要作为 key
            return DigestUtils.sha256Hex(jsonString);
        };
    }
}

2.2.2 显式指定 key

​ 相较于使用 KeyGenerator 生成,spring 官方更推荐显式指定 key 的方式,即指定 @Cacheable 的 key 参数。

即便是显式指定,但是 key 的值还是需要根据参数的不同来生成,那么如何实现动态拼接呢?SpEL(Spring Expression Language,Spring 表达式语言) 能做到这一点。

显示指定的好处在于,直观明了,看到代码就能想象生成的 key 是什么样。而且 SpEL 也很强大。关于 SpEL 的详细用法,这里不详述,可以参考官方文档:

(SpEL)

注意:官方说 key 和 keyGenerator 参数是互斥的,同时指定两个会导致异常

2.2.3 condition

SpEL 表达式,用于条件过滤,表示当满足该条件的情况下才进行缓存;true或者false,只有为true时才进行缓存;

2.3 @CacheEvict

​ 该注解用于缓存的清除,由于该注解的参数与Cacheable的参数大部分都是相同的,这里来简单介绍下CacheEvict注解独有的两个参数:

allEntries,是否清空所有的缓存内容,默认为false,如果指定为 true,则方法调用后将清空所有缓存;不过不允许在该值设置为true的情况下,再设置key的值;

beforeInvocation,是否在调用该方法之前清空缓存,默认为false;如果为true,在该方法被调用前就清空缓存,不用考虑该方法的执行结果(即不考虑是否抛出异常);而默认情况下,如果方法执行时发生异常,则不会清除缓存;

​ 为了更好的查看效果,我们先引入 Redis,将SpringCache缓存在Redis,而不是使用内置的缓存。

2.4 使用Redis做缓存

2.4.1 Redis依赖

<!--  引入Redis依赖  -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--  Redis连接池依赖  -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

2.4.2 Redis 配置

​ application.properties:

#Redis
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.lettuce.pool.max-active=8
    # 超时设置 5000ms
spring.redis.lettuce.pool.max-wait=5000

​ 删除CacheConfig.java文件, 新建RedisConfig

@Configuration
public class RedisConfig extends CachingConfigurerSupport {

    @Bean(name = "redisTemplate")
    public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<Object,Object> template = new RedisTemplate<>();
        //Value序列化方法
        FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);
        //Value值采用 FastJsonRedisSerializer
        template.setValueSerializer(fastJsonRedisSerializer);
        template.setHashValueSerializer(fastJsonRedisSerializer);
        //Key序列化方法
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        //Key值采用 StringRedisSerializer
        template.setKeySerializer(stringRedisSerializer);
        template.setHashKeySerializer(stringRedisSerializer);
        // 全局开启AutoType,这里方便开发,使用全局模式
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        // 建议使用这种方式,小范围指定白名单
        //ParserConfig.getGlobalInstance().addAccept("com.xxx.xxx");
        template.setConnectionFactory(redisConnectionFactory);
        template.afterPropertiesSet();
        return template;
    }


    /**
     *  自定义缓存 key 生成策略
     */
    @Bean
    @Override
    public KeyGenerator keyGenerator() {
        return (target, method, params) -> {
            Map<String,Object> container = new HashMap<>(3);
            Class<?> targetClass = target.getClass();
            // 类地址
            container.put("class",targetClass.toGenericString());
            // 方法名称
            container.put("methodName",method.getName());
            // 包名称
            container.put("package",targetClass.getPackage());
            // 参数列表
            for(int i=0; i<params.length; i++){
                container.put(String.valueOf(i),params[i]);
            }
            // 转为 JSON 字符串
            String jsonString = JSON.toJSONString(container);
           // System.out.println(jsonString);
            // 做 SHA256 哈希得到一个 SHA256 摘要作为 key
            System.out.println(DigestUtils.sha256Hex(jsonString));
            return DigestUtils.sha256Hex(jsonString);
        };
    }



    /**
     *  Value序列化器
     */
    class FastJsonRedisSerializer<T> implements RedisSerializer<T> {
        private final Class<T> clazz;

        FastJsonRedisSerializer(Class<T> clazz) {
            super();
            this.clazz = clazz;
        }

        /**
         * Serialize the given object to binary data.
         *
         * @param t object to serialize. Can be {@literal null}.
         * @return the equivalent binary data. Can be {@literal null}.
         */
        @Override
        public byte[] serialize(T t) throws SerializationException {
           if(t == null){
               return new byte[0];
           }
           return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(StandardCharsets.UTF_8);
        }

        /**
         * Deserialize an object from the given binary data.
         *
         * @param bytes object binary representation. Can be {@literal null}.
         * @return the equivalent object instance. Can be {@literal null}.
         */
        @Override
        public T deserialize(byte[] bytes) throws SerializationException {
           if(bytes == null || bytes.length <=0){
               return null;
           }
           String str = new String(bytes,StandardCharsets.UTF_8);
           return JSON.parseObject(str,clazz);
        }
    }

    /**
     *  Key序列化器
     */
    class StringRedisSerializer implements RedisSerializer<Object>{
        private final Charset charset; // 编码
        StringRedisSerializer(){ this(StandardCharsets.UTF_8); }
        private StringRedisSerializer(Charset charset) {
            Assert.notNull(charset,"Charset must not be null!");
            this.charset = charset;
        }

        /**
         * Serialize the given object to binary data.
         *
         * @param object object to serialize. Can be {@literal null}.
         * @return the equivalent binary data. Can be {@literal null}.
         */
        @Override
        public byte[] serialize(Object object) throws SerializationException {
            String string = JSON.toJSONString(object);
            if(StringUtil.isNullOrEmpty(string)){
                return null;
            }
            string = string.replace("\"","");
            return string.getBytes(charset);
        }

        /**
         * Deserialize an object from the given binary data.
         *
         * @param bytes object binary representation. Can be {@literal null}.
         * @return the equivalent object instance. Can be {@literal null}.
         */
        @Override
        public Object deserialize(byte[] bytes) throws SerializationException {
            return (bytes == null ? null:new String(bytes,charset));
        }
    }
}

因为使用Redis后,RedisCacheConfiguration类注册了RedisCacheManager, 因此SpringCache内置的被替换成了使用RedisCacheManager, 即可以使用Redis做缓存了,非常方便。但是会发现默认的RedisCacheManager并没有使用我们自定义的序列化器,因此要使用我们自定义的序列化器,还需要自定义RedisCacheManager。

在这里插入图片描述

在这里插入图片描述

2.4.3 SpringCache使用自定义序列化器

​ 重写自己的 RedisCacheConfiguration 如下:(使用默认的序列化方法会存储字节码,不便于查看)

@Configuration
public class RedisConfig extends CachingConfigurerSupport {
/**
 * 设置 Redis数据默认过期时间, 默认6小时(不会影响正常 Redis 的过期时间)
 * 设置 @cacheable 序列化方式 (spring cache 序列化方式)
 */
@Bean
public RedisCacheConfiguration redisCacheConfiguration(){
    // Value 序列化器
    FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);
    RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig();
    configuration = configuration.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(fastJsonRedisSerializer));
    // 默认 Spring Cache 使用 Redis 的过期时间是 6小时 (不会影响正常 Redis 的过期时间)
    configuration = configuration.entryTtl(Duration.ofHours(6));
    return configuration;
}
...
}

​ 测试:

@Test
void redisTemplateTest01(){
    redisTemplate.opsForValue().set("hello","hello"); //普通的Redis操作
}

​ 结果:

127.0.0.1:6379> KEYS *
1) "hello"
127.0.0.1:6379> get hello
"\"hello\""
127.0.0.1:6379> TTL hello  
(integer) -1  //不会设置6小时过期

​ 测试Spring Cache:

@Cacheable(value = "sayHello", key = "#text")
public String sayHello(String text){
    System.out.println("call sayHello...");
    return text;
}

​ 调用后结果:

127.0.0.1:6379> KEYS *
1) "hello"
2) "sayHello::hello"
127.0.0.1:6379> ttl sayHello::hello  //@Cacheable 默认过期时间 6 小时  
(integer) 21079
127.0.0.1:6379> GET sayHello::hello //查看不会是字节乱码
"\"hello\""
127.0.0.1:6379>

3. 继续SpringCache的使用

3.1 @CacheEvict

​ 该注解用于缓存的清除,由于该注解的参数与Cacheable的参数大部分都是相同的,这里来简单介绍下CacheEvict注解独有的两个参数:

allEntries,是否清空所有的缓存内容,默认为false,如果指定为 true,则方法调用后将清空所有缓存;不过不允许在该值设置为true的情况下,再设置key的值;

beforeInvocation,是否在调用该方法之前清空缓存,默认为false;如果为true,在该方法被调用前就清空缓存,不用考虑该方法的执行结果(即不考虑是否抛出异常);而默认情况下,如果方法执行时发生异常,则不会清除缓存;
在这里插入图片描述

​ 多个缓存使用同一个value,可以发现缓存的Key 是不一样的,是根据最后生成的Key为cacheNames :: key,比如sayHello::sha256(自定义shaKeyGenerator 生成的256值)的。

​ 如上采用自定义 KeyGenerator 发现无法删除成功,因为此时的方法名不一致,当然产生的sha256不一样,因此删除时产生Redis的key并不在Redis中存在。因此需要采用显示指定key来覆盖自定义 KeyGenerator ,只要显示指定key自定义的KeyGenerator就会失效。

​ 重定义方法如下:

@Service("helloService")
@Log4j2
public class HelloServiceImpl {
    @Cacheable(value = "sayHello", key = "#text")
    public String sayHello(String text){
        System.out.println("call sayHello...");
        return text;
    }

    @Cacheable(value = "sayHello",key = "#text")
    public String sayHello2(String text){
        System.out.println("call sayHello2...");
        return text;
    }

    @CacheEvict(value = "sayHello",key = "#delText",allEntries = false)
    public void delHello(String delText){
        System.out.println("call delHello...");
    }
}

测试:

@Test
void springCacheTest01(){
    System.out.println(helloService.sayHello("hello"));
    System.out.println(helloService.sayHello("hello"));
    System.out.println(helloService.sayHello("hello"));
    System.out.println(helloService.sayHello2("hello2"));
    System.out.println(helloService.sayHello2("hello2"));
    System.out.println(helloService.sayHello2("hello2"));
}

结果:

127.0.0.1:6379> KEYS *
1) "sayHello::hello"
2) "sayHello::hello2"

再测试删除:

@Test
void springCacheTest02(){
    helloService.delHello("hello2");
}

结果:

127.0.0.1:6379> KEYS *
1) "sayHello::hello"      //"sayHello::hello2"被删除

3.1.1 allEntries属性

再重新恢复缓存,设置allEntries:true:

@CacheEvict(value = "sayHello",key = "#delText",allEntries = true)
public void delHello(String delText){
    System.out.println("call delHello...");
}

执行清除缓存,结果:

127.0.0.1:6379> KEYS *
1) "sayHello::hello"
2) "sayHello::hello2"
127.0.0.1:6379> KEYS *
(empty list or set)	//清空所有的缓存内容,即所有sayHello::*
127.0.0.1:6379>

3.1.2 beforeInvocation属性

​ 清除操作默认是在对应方法成功执行之后触发的,即方法如果因为抛出异常而未能成功返回时也不会触发清除操作。使用beforeInvocation可以改变触发清除操作的时间,当我们指定该属性值为true时,Spring会在调用该方法之前清除缓存中的指定元素。

3.2 @CachePut

​ 配置于函数上,能够根据参数定义条件进行缓存,与@Cacheable不同的是,每次都会真实调用函数,所以主要用于数据新增和修改操作上。

在这里插入图片描述

@CachePut(value = "sayHello",key = "#changeText")
public String changeHello(String changeText){
    System.out.println("call changeHello...");
   return changeText+changeText;
}

​ 测试:

@Test
void springCacheTest03(){
    helloService.changeHello("hello");
}

​ 结果:

127.0.0.1:6379> GET sayHello::hello   // 发生更新
"\"hellohello\""
127.0.0.1:6379> ttl sayHello::hello  // ttl 重置为 6小时
(integer) 21589

3.3 @CacheConfig

​ 所有的@Cacheable()里面都有一个value=“xxx”的属性,这显然如果方法多了,写起来也是挺累的,如果可以一次性声明完 那就省事了, 所以,有了@CacheConfig这个配置。

​ @CacheConfig是一个类级别的注解。

@Service("helloService")
@Log4j2
@CacheConfig(cacheNames = "sayHello")
public class HelloServiceImpl {
    ...
}

​ 这样就不需要在每个类方法上申明cacheNames或者value了。但是如果在方法上继续又声明了cacheNames或者value则方法注解覆盖类注解。

3.4 @Caching

​ 有时候我们可能组合多个Cache注解使用;比如用户新增成功后,我们要添加id–>user;username—>user;email—>user的缓存;此时就需要@Caching组合多个注解标签了。

@Caching(put = {
@CachePut(value = "user", key = "#user.id"),
@CachePut(value = "user", key = "#user.username"),
@CachePut(value = "user", key = "#user.email")
})
public User save(User user) {
}

3.5 cacheManager属性

​ 当同时使用了多种缓存技术,需要定义多个cacheManager, 同时使用@Primary指定一个主Bean。 此时 spring cache注解可以使用此字段决定缓存到哪种缓存中。

在这里插入图片描述

​ 常规的SpringBoot已经为我们自动配置了EhCacheCollectionGuavaConcurrentMap等缓存,默认使用ConcurrentMapCacheManager

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值