Spring @Cacheable的缓存数据手动清理问题

Spring Cache 本身完美支持缓存的CRUD. 可以通过注解来实现缓存的清理, 详见: org.springframework.cache.annotation.CacheEvict. 此处不赘述. 

 

假如, 我们希望手动清理@Cacheable的缓存数据呢? 为什么有这样low的需求, 这么变态的需求? 运行中的系统总有千奇百怪的问题, 总有时候你可能需要手动清理缓存. 

而手动清理缓存多半不能找系统管理员, 系统管理员总有各种出于安全的理由拒绝为你效劳. 既然我们是程序员, 那就用程序的方式解决. 

 

分两步:

  1. 记录cacheName. cacheName就是@Cacheable注解的value. 
    之所以要记录cacheName, 是出于性能考虑. 一般产品线的Redis是不允许执行keys命令的. 所以不知道哪些数据被真正被缓存在了redis中. 
  2. 提供一个cache console页面, 用于访问redis并清理指定的cacheName对应的缓存数据. 这个页面是需要登录, 可以用公司的SSO登录, 并有一定的权限控制.

 

本文只涉及第一步. 第二步就是写一个web页面, 很简单, 所以就不提了.

 

记录cacheName要用到Spring的AOP.

 

/**
 * Aspect to handle Cache
 */
@Aspect
@Slf4j
class CacheAspect implements Ordered {
 
    @Pointcut("@annotation(org.springframework.cache.annotation.Cacheable)")
    public void cacheable() {
    }
 
    @Before("cacheable()")
    public void handleCacheable(JoinPoint joinPoint) throws Throwable {
        Method method = ObjectUtil.getAopTargetMethod(joinPoint);
        saveCacheName(method.getDeclaredAnnotation(Cacheable.class).cacheNames());
    }
 
    static void saveCacheName(String[] cacheNames) {
        Flux.fromArray(cacheNames)
                .filter(StringUtils::hasText)
                .doOnError(e -> log.warn("failed to saveCacheName: " + e.getMessage()))
                .subscribe(cacheName -> {
                    HashOperations<String, String, String> opsForHash = SpringContextHelper.started().getBean(StringRedisTemplate.class).opsForHash();
                    opsForHash.putIfAbsent("__cacheName:" + value(SpringContextHelper.started().getApplicationName(), "unknown"), cacheName, "{}");
                });
    }
 
    @Override
    public int getOrder() {
        return -999;
    }
}

这个AOP拦截所有对@Cacheable注解了的方法的访问, 记录其cacheName, 异步写入redis的一个hash中. 用于后续cache console页面的查询.

异步写入用到了Spring 5的Reactor.  

filter(StringUtils::hasText), 用于过滤空的cacheName, 有些开发人员可能粗心把@Cacheable的value指定为了一个空字符串, 此处要绕过空串避免AOP出错.

doOnError(e -> log.warn("failed to saveCacheName: " + e.getMessage())), 用于错误事件的处理.

subscribe(cacheName -> {...}); 这一句是实际写入redis. 

最后, 还需要注册到bean Factory. 

@Bean(name = "cacheAspect")
public CacheAspect cacheAspect() {
    return new CacheAspect();
}

 

 

猜你喜欢

转载自rickgong.iteye.com/blog/2414338