关于SpringCache的一些认识

关于SpringCache的一些认识

项目中用到了redis,同时在一些高频的查询方法使用redis作为缓存。但是因为没使用过SpringCache,当时看着网上的教程做了配置后,确实缓存生效了,就没有再管它。

突然项目经理跟我说redis中的缓存数据没有设置过期时间。看了下ttl都是-1,确实有问题。

为了更好的说清楚过程,下面就从头到位按顺序记录一下整个SpringCache的使用过程。而我之前又没有使用过SpringCache,所以会有很多低级的错误。如果你是对SpringCache比较熟悉的大佬,这文章应该不用往下看了。

1. 网上教程

首先记录一下我找到的一些教程。

基本如下:

  1. 最简单的,引入spring-boot-starter-data-redis,启动类上增加@EnableCaching注解。在需要缓存的方法上使用@Cacheable注解。这里说一下,Cacheable注解的cacheNames必须配,否则报错。

  2. 复杂一些,在上面的基础上,增加对redis的配置,如CacheManager、key,value的序列化规则等等。

示例代码:

import org.springframework.cache.CacheManager;
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.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

@Configuration
public class RedisConfig {
    /**
     * 配置redis缓存管理器
     *
     * @param redisConnectionFactory Redis连接工厂
     * @return 缓存管理器
     */
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        //通过 Config 对象即可对缓存进行自定义配置
        RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
                // 禁止缓存 null 值
                .disableCachingNullValues()
                // 设置 key 序列化
                .serializeKeysWith(keyPair())
                // 设置 value 序列化
                .serializeValuesWith(valuePair())
                // 设置缓存前缀
                .prefixCacheNameWith("cache:scrd:")
                // 设置过期时间
                .entryTtl(Duration.ofMinutes(30L));

        // 返回 Redis 缓存管理器
       return RedisCacheManager.builder(redisConnectionFactory)
               .withCacheConfiguration("redis-test", cacheConfig)
               .build();
    }


    /**
     * 配置键序列化
     *
     * @return StringRedisSerializer
     */
    private RedisSerializationContext.SerializationPair<String> keyPair() {
        return RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer());
    }

    /**
     * 配置值序列化,使用 GenericJackson2JsonRedisSerializer 替换默认序列化
     *
     * @return GenericJackson2JsonRedisSerializer
     */
    private RedisSerializationContext.SerializationPair<Object> valuePair() {
        return RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer());
    }
}

2. 项目背景

我们的项目中会调用第三方的系统查询产品数据,这些数据可以说就是不会改变的。我们当时决定将数据缓存并且过期时间设置为12个小时,其他的一般缓存默认过期时间为30分钟。

因为找到的SpringCache教程基本就是上面的使用方法,基本没有介绍如何配置不同的ttl,所以只能自己寻找思路了。

通过上面的教程,我猜想:CacheManager可以配置cacheName和RedisCacheConfiguration,那么我配置了2个CacheManager,分别设置不同的cacheName和RedisCacheConfiguration,是不是可以支持不同的过期时间。然后在需要的方法上指定对应配置的cacheNames。
比如下面的代码就是使用@Cacheable(cacheNames = "product")

按照这个思路我写了这样的代码:

/**
    * 配置默认redis缓存管理器
    *
    * @param redisConnectionFactory Redis连接工厂
    * @return 缓存管理器
    */
@Bean("defaultCacheManger")
public CacheManager defaultCacheManger(RedisConnectionFactory redisConnectionFactory) {
    //通过 Config 对象即可对缓存进行自定义配置
    RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
            // 禁止缓存 null 值
            .disableCachingNullValues()
            // 设置 key 序列化
            .serializeKeysWith(keyPair())
            // 设置 value 序列化
            .serializeValuesWith(valuePair())
            // 设置缓存前缀
            .prefixCacheNameWith("cache:scrd:default:")
            // 设置过期时间
            .entryTtl(Duration.ofMinutes(10L));

    // 返回 Redis 缓存管理器
    return RedisCacheManager.builder(redisConnectionFactory)
            .withCacheConfiguration("redis-test", cacheConfig)
            .build();
}

/**
    * 配置产品信息redis缓存管理器
    *
    * @param redisConnectionFactory Redis连接工厂
    * @return 缓存管理器
    */
@Bean("productCacheManager")
public CacheManager productCacheManager(RedisConnectionFactory redisConnectionFactory) {
    //通过 Config 对象即可对缓存进行自定义配置
    RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
            // 禁止缓存 null 值
            .disableCachingNullValues()
            // 设置 key 序列化
            .serializeKeysWith(keyPair())
            // 设置 value 序列化
            .serializeValuesWith(valuePair())
            // 设置缓存前缀
            .prefixCacheNameWith("cache:scrd:product:")
            // 设置过期时间
            .entryTtl(Duration.ofMinutes(720L));

    // 返回 Redis 缓存管理器
    return RedisCacheManager.builder(redisConnectionFactory)
            .withCacheConfiguration("product", cacheConfig)
            .build();
}

很遗憾,启动报错。

java.lang.IllegalStateException: No CacheResolver specified, and no unique bean of type CacheManager found. Mark one as primary or declare a specific CacheManager to use.

大意是:找到多个CacheManager,不知道注入哪一个,需要使用@Primary指定一下默认使用哪一个。

果断给defaultCacheManger加上@Primary注解,正常启动。测试调用后,确实可以在redis看到数据。

3. 问题分析

上面看到的redis中数据其实有问题的,剧透一下:redis中key按照配置应该是以“cache:scrd:product:”开头,但是实际redis中缓存相关的key都是以“product开头”。因为“缓存”功能一直正常,所以我以为这是前缀的配置失效了。直到项目经理告诉我ttl有问题,我才会想起这个不一样的地方。

通过看源码,打断点,发现SpringCache在写数据进redis时,并没有使用我在productCacheManager中的相关配置,不仅是ttl的配置没有了,前缀的也没有了。

scrd-0.png

这让我不禁怀疑@Cacheable注解的cacheNames参数是不是没办法关联到productCacheManager。查看@Cacheable注解的源码,发现可以直接配置cacheManager。然后就试了下直接配置cacheManager = "productCacheManager"

重启测试,发现这次可以正常使用我在productCacheManager中的配置了。

scrd-1.png

4. 优化总结

这里重申一下:配置了cacheManager后,cacheNames还是要配置。因为我不配置就报错了。这个报错让我想到了,我一开始的思路或许是错误的。

从上面的过程来看,每个缓存必须指定对应的cacheName,但是可以不指定cacheManager。所以cacheManager应该可以由框架默认注入,而默认的cacheManager就是加了@Primary的那个Bean。这也是为什么手动指定cacheManager后可以正常。

再次观察CacheManager的配置代码。

查看配置代码中RedisCacheConfiguration是在withCacheConfiguration方法中使用的,参数还有cacheName。一开始我以为cacheName是给CacheManager设置的属性。查看源码发现该方法将RedisCacheConfiguration存在一个key为cacheName的Map中,所以RedisCacheConfiguration是和cacheName对应的。一个CacheManager可以有多个cacheName和RedisCacheConfiguration的组合。也就是说withCacheConfiguration方法可以多次调用。从而实现不同缓存,不同配置。使用的地方,通过cacheName来获取对应的配置。

withCacheConfiguration源码:

/**
 * @param cacheName
 * @param cacheConfiguration
 * @return this {@link RedisCacheManagerBuilder}.
 * @since 2.2
 */
public RedisCacheManagerBuilder withCacheConfiguration(String cacheName,
        RedisCacheConfiguration cacheConfiguration) {

    Assert.notNull(cacheName, "CacheName must not be null!");
    Assert.notNull(cacheConfiguration, "CacheConfiguration must not be null!");

    this.initialCaches.put(cacheName, cacheConfiguration);
    return this;
}

总结一下:SpringCache中一般只需要配置一个CacheManager,通过配置cacheName来指定特殊配置。除非是对接了多种缓存实现,比如即用redis,也用Ehcache。这个时候,就需要配置多个CacheManager,在使用的地方指定cacheManager。

调整后的代码:

/**
 * 配置redis缓存管理器
 *
 * @param redisConnectionFactory Redis连接工厂
 * @return 缓存管理器
 */
@Bean
public CacheManager cacheManger(RedisConnectionFactory redisConnectionFactory) {
    //通过 Config 对象即可对缓存进行自定义配置
    RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
            // 禁止缓存 null 值
            .disableCachingNullValues()
            // 设置 key 序列化
            .serializeKeysWith(keyPair())
            // 设置 value 序列化
            .serializeValuesWith(valuePair())
            // 设置缓存前缀
            .prefixCacheNameWith("cache:scrd:")
            // 设置过期时间
            .entryTtl(Duration.ofMinutes(10L));
    RedisCacheManager.RedisCacheManagerBuilder cacheManagerBuilder = RedisCacheManager.builder(redisConnectionFactory);
    //设置默认的配置,当设置测cacheName没有配置的时候,使用默认配置
    cacheManagerBuilder.cacheDefaults(cacheConfig);

    //entryTtl方法不是在原有对象中修改配置
    //而是会返回一个新的RedisCacheConfiguration对象,需要用cacheConfig接收。否则设置无效。
    cacheConfig = cacheConfig.entryTtl(Duration.ofMinutes(720L));

    //设置cacheName对呀的配置
    cacheManagerBuilder.withCacheConfiguration("product", cacheConfig);

    // 返回 Redis 缓存管理器
    return cacheManagerBuilder.build();
}
原文地址:https://www.cnblogs.com/jimmyfan/p/14412363.html