如何解决基于Cacheable的redis缓存过期时间都一样的问题

1,610 阅读3分钟

今天要讨论的疑问如题

原理

先通过源码验证下,请看源码RedisCacheConfiguration#determineConfiguration 这里做了个事情就是将this.cacheProperties.getRedis();的配置塞到RedisCacheConfiguration中。在cachePropertiescacheNames是数组,也就是可以设置多个缓存域,但是注解并沒有为每个缓存域设置过期时间,这就导致所有缓存域共用一个过期时间。我也不知道写这段代码的人是出于什么考虑而这么做的,可能他认为的常用业务场景是一个key有可能存在不同缓存域下,或者说他认为一个应用只应该让你使用一块缓存,所以过期时间得设置成一样?但是实际场景中往往我们会在一个应用下使用到多个业务场景的缓存。例如:用户中心,你可能需要缓存菜单,需要缓存用户登入信息,显然两个缓存不能使用同一种策略。

class RedisCacheConfiguration {
//...省略无关代码
   private org.springframework.data.redis.cache.RedisCacheConfiguration determineConfiguration(
         ClassLoader classLoader) {
      if (this.redisCacheConfiguration != null) {
         return this.redisCacheConfiguration;
      }
      Redis redisProperties = this.cacheProperties.getRedis();
      org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration
            .defaultCacheConfig();
      config = config.serializeValuesWith(SerializationPair
            .fromSerializer(new JdkSerializationRedisSerializer(classLoader)));
      if (redisProperties.getTimeToLive() != null) {
         config = config.entryTtl(redisProperties.getTimeToLive());
      }
      if (redisProperties.getKeyPrefix() != null) {
         config = config.prefixKeysWith(redisProperties.getKeyPrefix());
      }
      if (!redisProperties.isCacheNullValues()) {
         config = config.disableCachingNullValues();
      }
      if (!redisProperties.isUseKeyPrefix()) {
         config = config.disableKeyPrefix();
      }
      return config;
   }

}

以上代码我们可以推断出所有缓存域映射到同一个Redis配置实例。那么我们如果需要修改成不同缓存域使用不同的过期时间,我们只需要构造如下数据结构即可。

@ConfigurationProperties(prefix = "spring.cache.custom.redis")
public class CustomRedisProperties {
    //spring.cache.custom.redis.user.time-to-live=5000ms
    //spring.cache.custom.redis.order.time-to-live=10000ms
    //map结构为
    //1、("user",new Redis().setTimeToLive(5000ms)
    //2、("order",new Redis().setTimeToLive(10000ms)
    private Map<String, Redis> cacheNamesMap;

    public Map<String, Redis> getCacheNamesMap() {
        return cacheNamesMap;
    }

    public void setCacheNamesMap(Map<String, Redis> cacheNamesMap) {
        this.cacheNamesMap = cacheNamesMap;
    }
}

接着我们实现一个配置类配置CacheManager

@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
@EnableConfigurationProperties({ CacheProperties.class,CustomRedisProperties.class})
public class CacheMapRedisCacheConfiguration {

    private CacheProperties cacheProperties;
    private CustomRedisProperties customRedisProperties;

    public CacheMapRedisCacheConfiguration(CacheProperties cacheProperties,
        CustomRedisProperties customRedisProperties) {
        this.cacheProperties = cacheProperties;
        this.customRedisProperties = customRedisProperties;
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory,
        ResourceLoader resourceLoader, RedisTemplate redisTemplate) {
        RedisSerializer valueSerializer = redisTemplate.getValueSerializer();
        Map<String, RedisCacheConfiguration> map = getMap(resourceLoader.getClassLoader(), valueSerializer);
        //配置构造器,使用redisConnectionFactory作为连接工厂
        //cacheDefaults默认配置请看determineConfiguration,首先从原本的配置获取配置,如果原配置沒有配,那么就使用defaultCacheConfig来配置,
        //withInitialCacheConfigurations使用我们扩展的配置作为初始化配置,如果有初始化配置则不用上面的默认配置。这样就达到了我们自定义配置的目的。对不同缓存域做了个过期时间区分。
        RedisCacheManagerBuilder builder = RedisCacheManager.builder(redisConnectionFactory) .cacheDefaults(determineConfiguration(resourceLoader.getClassLoader(),valueSerializer))
            .withInitialCacheConfigurations(map);
        return builder.build();
    }
    
    //自定义配置
    private Map<String, RedisCacheConfiguration> getMap(ClassLoader classLoader,
        RedisSerializer valueSerializer) {
        Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
        customRedisProperties.getCacheNamesMap().forEach((key, value) -> {
            RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
            config = config.serializeValuesWith(SerializationPair.fromSerializer(valueSerializer));
            if (value.getTimeToLive() != null) {
                config = config.entryTtl(value.getTimeToLive());
            }
            if (value.getKeyPrefix() != null) {
                config = config.prefixKeysWith(value.getKeyPrefix());
            }
            if (!value.isCacheNullValues()) {
                config = config.disableCachingNullValues();
            }
            if (!value.isUseKeyPrefix()) {
                config = config.disableKeyPrefix();
            }
            redisCacheConfigurationMap.put(key, config);
        });
        return redisCacheConfigurationMap;
    }
    //推断默认配置,如果有配置cacheProperties则使用它,如果沒有则使用RedisCacheConfiguration.defaultCacheConfig();
    private RedisCacheConfiguration determineConfiguration(ClassLoader classLoader,RedisSerializer redisSerializer) {
        Redis redisProperties = this.cacheProperties则使用它,如果沒有则使用.getRedis();
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        config = config.serializeValuesWith(
            SerializationPair.fromSerializer(redisSerializer));
        if (redisProperties.getTimeToLive() != null) {
            config = config.entryTtl(redisProperties.getTimeToLive());
        }
        if (redisProperties.getKeyPrefix() != null) {
            config = config.prefixKeysWith(redisProperties.getKeyPrefix());
        }
        if (!redisProperties.isCacheNullValues()) {
            config = config.disableCachingNullValues();
        }
        if (!redisProperties.isUseKeyPrefix()) {
            config = config.disableKeyPrefix();
        }
        return config;
    }
}

这样就解决了基于Cacheableredis缓存过期时间都一样的问题,此时我们就可以根据业务使用不同的缓存域,配置不同的缓存过期时间了。当然这里没办法做到像自动调用api那样灵活,想对什么key加过期时间就加,但是它同样做到了规范化缓存管理。灵活虽然不意味着混乱,但是灵活普遍会照成混乱。

示例

新建springboot项目,引入redis-starter

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.qimo</groupId>
    <artifactId>omsa-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>omsa-demo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

application.properties配置

server.port=7777
logging.level.root=info
spring.redis.port=6379
spring.redis.host=localhost
spring.cache.cache-names[0]=user
spring.cache.cache-names[1]=order
spring.cache.custom.redis.cache-names-map.user.time-to-live=20000ms
spring.cache.custom.redis.cache-names-map.order.time-to-live=30000ms

编写用例

/**
 * @Description TODO
 * @Author 姚仲杰#80998699
 * @Date 2021/8/3 11:17
 */
@RestController
@EnableCaching
public class CacheTest {
    @Value("${spring.cache.custom.redis.cache-names-map.user.time-to-live}")
    private String userTTL;
    @Value("${spring.cache.custom.redis.cache-names-map.order.time-to-live}")
    private String orderTTL;
    
    
    @GetMapping("/cacheUser")
    @Cacheable(cacheNames = "user")
    public String userCache(String s) {
        System.out.println("user域缓存时间:"+userTTL);
        return s;
    }
    
    @GetMapping("/cacheOrder")
    @Cacheable(cacheNames = "order")
    public String orderCache(String s) {
        System.out.println("order域缓存时间:"+orderTTL);
        return s;
    }
    
}

开始前先验证下redis如下

# redis-cli
127.0.0.1:6379> TTL user::111
(integer) -2
127.0.0.1:6379> TTL order::111
(integer) -2
127.0.0.1:6379>

启动,执行,访问两个接口,然后查看过期时间如下

127.0.0.1:6379> TTL user::111
(integer) 14
127.0.0.1:6379> TTL order::111
(integer) 24
127.0.0.1:6379>

大功告成。