SpringBoot3中redis缓存如何支持自定义过期时间

471 阅读2分钟

配置

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>
spring:
  data:
    redis:
      host: "localhost"
      port: 6379
      database: 0
      # 用户名,如果有
      # username:
      # 密码,如果有
      # password:
      connect-timeout: 5s
      # 读超时
      timeout: 5s
      lettuce:
        pool:
          # 最小空闲连接
          min-idle: 0
          # 最大空闲连接
          max-idle: 8
          # 最大活跃连接
          max-active: 8
          # 从连接池获取连接 最大超时时间,小于等于0则表示不会超时
          max-wait: -1ms

redis主要使用是RedisTemplateRedisCacheManager,RedisTemplate实现了和Redis相关的直接操作,比如增删改查,RedisCacheManager在RedisTemplate基础上对spring cache进行进一步封装,提供缓存管理能力

RedisTemplate

RedisConfig.java

@Configuration
public class RedisConfig {

    /**
     * redis template.
     *
     * @param factory factory
     * @return RedisTemplate
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }
}

RedisController.java

@RestController
@RequestMapping("redis")
@AllArgsConstructor
public class RedisController {
    private final RedisTemplate<String, String> redisTemplate;

    @GetMapping("save")
    public void save(String key, String value){
        redisTemplate.opsForValue().set(key, value);
    }

    @GetMapping("get")
    public String get(String key){
        return redisTemplate.opsForValue().get(key);
    }
}

RedisCacheManager

Spring Boot 2.x

@Configuration
@EnableCaching
public class RedisCacheConfig extends CachingConfigurerSupport {

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofSeconds(60))
            .disableCachingNullValues();
        return RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(redisCacheConfiguration).build();
    }
}

Spring Boot 3.x

@Configuration
public class RedisConfig {

    private RedisCacheConfiguration determineConfiguration() {
        return RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofSeconds(30)) // 设置超时时长为30秒
            .disableCachingNullValues() // 防止写入空值
            .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
    }

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        RedisCacheManager.RedisCacheManagerBuilder builder = RedisCacheManager.builder(connectionFactory);
        builder.cacheDefaults(determineConfiguration());
        return builder.build();
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }
}

RedisController.java

@RestController
@RequestMapping("redis")
@AllArgsConstructor
@Slf4j
public class RedisController {

    private final RedisTemplate<String, String> redisTemplate;

    @GetMapping("save")
    public void save(String key, String value){
        redisTemplate.opsForValue().set(key, value);
    }

    @GetMapping("get")
    public String get(String key){
        return redisTemplate.opsForValue().get(key);
    }

    @GetMapping("getId")
    @Cacheable(cacheNames  = "test", key = "#key")
    public String getId(String key){
        log.info("重新获取");
        return "key";
    }
}

自定义过期时间

参考网上的文章,一般有三种:

  1. 自定义cacheNames方式,简单,不够语义化
  2. 自定义派生@Cacheable注解,这种太复杂了
  3. 注解,无侵入(推荐)

下面的代码是使用注解的方式:

config/redis/CacheTTL.java

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
public @interface CacheTTL {
    @AliasFor("ttl")
    long value() default 60;

    @AliasFor("value")
    long ttl() default 60;

    long preExpireRefresh() default 10;
}

config/redis/CustomCacheAspect.java

@Order(value = 1)
@Aspect
@Component
@Slf4j
@AllArgsConstructor
public class CustomCacheAspect {
    private final RedisTemplate<String, Object> redisTemplate;
    private final SimpleKeyGenerator keyGenerator = new SimpleKeyGenerator();
    private final AtomicInteger asyncLock = new AtomicInteger(0);
    
    // 修改为自己的包名
    @After("@annotation(com.xxx.xxx.xxx.config.redis.CacheTTL)")
    public void after(JoinPoint point) {
        Object target = point.getTarget();

        MethodSignature signature = (MethodSignature) point.getSignature();

        Method method = signature.getMethod();

        try {
            if (method.isAnnotationPresent(CacheTTL.class) && method.isAnnotationPresent(Cacheable.class)) {
                CacheTTL ttlData = AnnotationUtils.getAnnotation(method, CacheTTL.class);
                Cacheable cacheAbleData = AnnotationUtils.getAnnotation(method, Cacheable.class);

                long ttl = ttlData.ttl();
                long preExpireRefresh = ttlData.preExpireRefresh();

                String[] cacheNames = cacheAbleData.cacheNames();
                // 默认的keyGenerator生成,如果自定义了自己改一下
                Object key = keyGenerator.generate(target, method, point.getArgs());
                updateExpire(cacheNames, key, preExpireRefresh, ttl);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void updateExpire(String[] cacheNames, Object key, long preExpireRefresh, long ttl) {
        if (asyncLock.compareAndSet(0, 1)) {
            Arrays.stream(cacheNames).parallel().forEach(cacheName -> {
                cacheName = cacheName + "::" + key;
                long expire = redisTemplate.getExpire(cacheName, TimeUnit.SECONDS);

                log.info("cache ttl: {} | {} ", cacheName, expire);
                if (expire > 0 && expire <= preExpireRefresh || expire > ttl || expire == -1) {
                    redisTemplate.expire(cacheName, ttl, TimeUnit.SECONDS);
                }
            });
            asyncLock.set(0);
        }
    }
}

测试接口

@GetMapping("getIdTtl")
@CacheTTL(ttl = 60, preExpireRefresh = 10)
@Cacheable(cacheNames  = "test", key = "#key")
public String getIdTtl(String key){
    log.info("重新获取");
    return "666";
}

参考文章:
juejin.cn/post/715760…
juejin.cn/post/698987…