③优雅的缓存框架:SpringCache增强支持自定义过期时间TTL

306 阅读2分钟

默认SpringCache是不支持自定义设置TTL的,redis的只支持配置一个统一的TTL,但是这可能并不符合业务的需求。

下面开始增强SpringCache让它支持配置化TTL

定义CachceTTL注解


/**
 * 缓存有效时长, 单位: 秒, 若写在类上则类中所有方法都继承此时间
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface CacheTTL {

    /**
     * 缓存有效时长, 单位: 秒
     */
    long value() default -1;
}

实战

缓存

/**
 * 生成验证码图片, 返回验证码文字, 并把验证码文字缓存
 */
@CacheTTL(120L)
@CachePut(key = "#token", unless = "#result == null")
public String generateCaptchaImage(String token, OutputStream os) throws IOException {
    String captchaText = ImageCaptchaGenerator.generateCaptchaText(candidateString, 4);
    ImageCaptchaGenerator.writeCaptchaImageToOutputStream(captchaText, width, height, os);
    return captchaText;
}

移除缓存

/**
 * 移除缓存
 */
@CacheTTL(120L)
@CacheEvict(key = "#token")
public void removeCaptchaCodeCache(String token) { }

实现原理

  1. 使用AOP拦截CacheTTL注解的方法或类里的所有方法
  2. 在写入redis之前把TTL信息保存到ThreadLocal中
  3. 在redis写入时取出ThreadLocal中的TTL并设置

Aop拦截并保存TTL

@Component
@Aspect
@Order(-1) // 需要比redis serializer 提前执行
public class CacheTTLAop {
    @Around("@annotation(org.cloud.cache.CacheTTL) || @within(org.cloud.cache.CacheTTL)")
    public Object run(JoinPoint joinPoint) throws Throwable {
        try {
            Signature signature = joinPoint.getSignature();
            if (signature instanceof MethodSignature methodSignature) {
                Method method = methodSignature.getMethod();
                CacheTTL expireAt = method.getAnnotation(CacheTTL.class);
                if (expireAt == null) {
                    Class<?> declaringType = methodSignature.getDeclaringType();
                    expireAt = declaringType.getAnnotation(CacheTTL.class);
                }
                long ttl = expireAt.value();
                CacheTTLContext.setTTL(ttl);
            }
            return ((MethodInvocationProceedingJoinPoint) joinPoint).proceed(joinPoint.getArgs());
        } finally {
            CacheTTLContext.remove();
        }
    }
}

redis写入

public class CustomRedisCache extends RedisCache {

    private final RedisCacheWriter cacheWriter;
    private final String name;
    private final RedisCacheConfiguration cacheConfig;

    // ... 省略其他无关方法完整源码请看https://github.com/L1yp/van-template-api/blob/main/src/main/java/org/cloud/cache/custom/CustomRedisCache.java

    @Override
    public void put(@NotNull Object key, Object value) {

        Object cacheValue = preProcessCacheValue(value);

        if (!isAllowNullValues() && cacheValue == null) {

            throw new IllegalArgumentException(String.format(
                    "Cache '%s' does not allow 'null' values. Avoid storing null via '@Cacheable(unless=\"#result == null\")' or configure RedisCache to allow 'null' via RedisCacheConfiguration.",
                    name));
        }

        // add expire ttl
        Duration ttl = cacheConfig.getTtl();
        // 获取ThreadLocal上下文中的TTL
        Long ctxTTL = CacheTTLContext.getTTL();
        if (ctxTTL != null && ctxTTL > 0) {
            ttl = Duration.ofSeconds(ctxTTL);
        }

        this.cacheWriter.put(name, createAndConvertCacheKey(key), serializeCacheValue(cacheValue), ttl);
    }

    @Override
    public ValueWrapper putIfAbsent(Object key, @Nullable Object value) {

        Object cacheValue = preProcessCacheValue(value);

        if (!isAllowNullValues() && cacheValue == null) {
            return get(key);
        }

        // add expire ttl
        Duration ttl = cacheConfig.getTtl();
        // 获取ThreadLocal上下文中的TTL
        Long ctxTTL = CacheTTLContext.getTTL();
        if (ctxTTL != null && ctxTTL > 0) {
            ttl = Duration.ofSeconds(ctxTTL);
        }

        byte[] result = cacheWriter.putIfAbsent(name, createAndConvertCacheKey(key), serializeCacheValue(cacheValue),
                ttl);

        if (result == null) {
            return null;
        }

        return new SimpleValueWrapper(fromStoreValue(deserializeCacheValue(result)));
    }
}

配置CustomRedisCache

自定义RedisCacheManager

/**
 * 支持可配置化的本地缓存
 */
public class CustomRedisCacheManager extends RedisCacheManager {

    private final RedisCacheWriter cacheWriter;
    private final RedisCacheConfiguration defaultCacheConfiguration;

    public CustomRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
        super(cacheWriter, defaultCacheConfiguration);
        this.cacheWriter = cacheWriter;
        this.defaultCacheConfiguration = defaultCacheConfiguration;
    }

    @Override
    @NotNull
    protected RedisCache createRedisCache(@NotNull String name, RedisCacheConfiguration cacheConfig) {
        return new CustomRedisCache(name, this.cacheWriter, cacheConfig != null ? cacheConfig : this.defaultCacheConfiguration);
    }
}

配置自定义 RedisCacheManager

    @Bean
    @Primary
    public CustomRedisCacheManager redisCacheManager(RedisConnectionFactory connectionFactory, RedisCacheConfiguration redisCacheConfiguration) {
        // 使用分布式锁
        return new CustomRedisCacheManager(RedisCacheWriter.lockingRedisCacheWriter(connectionFactory), redisCacheConfiguration);
    }

下一篇: ④优雅的缓存框架:SpringCache之多级缓存

以上代码来源: 后端代码:github.com/L1yp/van-te…

前端代码:github.com/L1yp/van-te…

点击链接加入群聊:【Van交流群】