增强Spring @CacheEvict实现key模糊匹配清除

          系统中集成了Spring cache 使用@CacheEvict进行缓存清除,@CacheEvict可以清除指定的key,同时可以指定allEntries = true清空namespace下的所有元素,现在遇到一个问题使用allEntries = true清空namespace的值只能是常量,但是我现在需要将缓存根据租户的唯一TelnetID进行分离,这就导致allEntries = true不能使用了,否则一旦触发清除缓存,将会导致全部的缓存清空,而我只想清空当前租户的缓存,熟悉redis命令的人都知道,查询和删除都可以做模糊匹配,所以我就想让SpringCache的@CacheEvict也支持模糊匹配清除。

         先去搞清楚@CacheEvict是怎么实现缓存清理的,因为之前看过redis的源码,知道@CacheEvict是通过AOP实现的,其中核心的类是CacheAspectSupport,具体的源码分析细节大家可以自行Google,我只简单的分析一下CacheAspectSupport这个类里面重点的几个方法

private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
		// Special handling of synchronized invocation
		if (contexts.isSynchronized()) {
			CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next();
			if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
				Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
				Cache cache = context.getCaches().iterator().next();
				try {
					return wrapCacheValue(method, cache.get(key, () -> unwrapReturnValue(invokeOperation(invoker))));
				}
				catch (Cache.ValueRetrievalException ex) {
					// The invoker wraps any Throwable in a ThrowableWrapper instance so we
					// can just make sure that one bubbles up the stack.
					throw (CacheOperationInvoker.ThrowableWrapper) ex.getCause();
				}
			}
			else {
				// No caching required, only call the underlying method
				return invokeOperation(invoker);
			}
		}


		// Process any early evictions
		processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
				CacheOperationExpressionEvaluator.NO_RESULT);

		// Check if we have a cached item matching the conditions
		Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));

		// Collect puts from any @Cacheable miss, if no cached item is found
		List<CachePutRequest> cachePutRequests = new LinkedList<>();
		if (cacheHit == null) {
			collectPutRequests(contexts.get(CacheableOperation.class),
					CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
		}

		Object cacheValue;
		Object returnValue;

		if (cacheHit != null && !hasCachePut(contexts)) {
			// If there are no put requests, just use the cache hit
			cacheValue = cacheHit.get();
			returnValue = wrapCacheValue(method, cacheValue);
		}
		else {
			// Invoke the method if we don't have a cache hit
			returnValue = invokeOperation(invoker);
			cacheValue = unwrapReturnValue(returnValue);
		}

		// Collect any explicit @CachePuts
		collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);

		// Process any collected put requests, either from @CachePut or a @Cacheable miss
		for (CachePutRequest cachePutRequest : cachePutRequests) {
			cachePutRequest.apply(cacheValue);
		}

		// Process any late evictions
		processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);

		return returnValue;
	}

这个方法是缓存控制入口核心方法processCacheEvicts会根据CacheOperationContext进行缓存清理处理,我们可以看到调用的一个performCacheEvict方法

private void performCacheEvict(
			CacheOperationContext context, CacheEvictOperation operation, @Nullable Object result) {

		Object key = null;
		for (Cache cache : context.getCaches()) {
			if (operation.isCacheWide()) { // 如果allEntries为ture就执行这个逻辑
				logInvalidating(context, operation, null);
				doClear(cache);
			}
			else {// 否则执行删除指定的key
				if (key == null) {
					key = generateKey(context, result);
				}
				logInvalidating(context, operation, key);
				doEvict(cache, key);
			}
		}
	}

看到这里,我们就知道怎么回事了,继续debug跟进去,看到doClear和doEvict最终分别会调用RedisCache中的evict和clear方法

@Override
	public void evict(Object key) {
		cacheWriter.remove(name, createAndConvertCacheKey(key));
	}

	/*
	 * (non-Javadoc)
	 * @see org.springframework.cache.Cache#clear()
	 */
	@Override
	public void clear() {
    // 支持模糊删除
		byte[] pattern = conversionService.convert(createCacheKey("*"), byte[].class);
		cacheWriter.clean(name, pattern);
	}

我们看到如果allEntries为ture的时候最终执行的是clear()这个方法,其实他也是模糊删除的,只是他的key规则是namespace:: *,看到这里就看到希望了我们只需要想办法在namespace *中插入我们的telnetID就可以变成namespace ::telnetID:*这种格式,也就达到了我们的目的了。

   重点需要重写RedisCache的evict方法,新建一个RedisCacheResolver集成RedisCache,重写evict方法

public class RedisCacheResolver extends RedisCache {

    private final String name;
    private final RedisCacheWriter cacheWriter;
    private final ConversionService conversionService;

    protected RedisCacheResolver(String name, RedisCacheWriter cacheWriter, RedisCacheConfiguration cacheConfig) {
        super(name, cacheWriter, cacheConfig);
        this.name = name;
        this.cacheWriter = cacheWriter;
        this.conversionService = cacheConfig.getConversionService();
    }

    /**
     * 
      * @Title: evict 
      * @Description: 重写删除的方法
      * @param  @param key 
      * @throws 
      *
     */
    @Override
    public void evict(Object key) {
        // 如果key中包含"noCacheable:"关键字的,就不进行缓存处理
        if (key.toString().contains(RedisConstant.NO_CACHEABLE)) {
            return;
        }
        
        if (key instanceof String) {
            String keyString = key.toString();
            // 后缀删除
            if (StringUtils.endsWith(keyString, "*")) {
                evictLikeSuffix(keyString);
                return;
            }
        }
        // 删除指定的key
        super.evict(key);
    }

    /**
     * 后缀匹配匹配
     * 
     * @param key
     */
    private void evictLikeSuffix(String key) {
        byte[] pattern = this.conversionService.convert(this.createCacheKey(key), byte[].class);
        this.cacheWriter.clean(this.name, pattern);
    }
}

     现在我们需要让我们的这个RedisCacheResolver 生效,所以需要将我们的RedisCacheResolver 注入到RedisCacheManager中,因此我们需要定义一个我们自己的RedisCacheManagerResolver集成RedisCacheManager

public class RedisCacheManagerResolver extends RedisCacheManager {
    private final RedisCacheWriter cacheWriter;
    private final RedisCacheConfiguration defaultCacheConfig;

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

    public RedisCacheManagerResolver(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, String... initialCacheNames) {
        super(cacheWriter, defaultCacheConfiguration, initialCacheNames);
        this.cacheWriter = cacheWriter;
        this.defaultCacheConfig = defaultCacheConfiguration;
    }

    public RedisCacheManagerResolver(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, boolean allowInFlightCacheCreation, String... initialCacheNames) {
        super(cacheWriter, defaultCacheConfiguration, allowInFlightCacheCreation, initialCacheNames);
        this.cacheWriter = cacheWriter;
        this.defaultCacheConfig = defaultCacheConfiguration;
    }

    public RedisCacheManagerResolver(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, Map<String, RedisCacheConfiguration> initialCacheConfigurations) {
        super(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations);
        this.cacheWriter = cacheWriter;
        this.defaultCacheConfig = defaultCacheConfiguration;
    }

    public RedisCacheManagerResolver(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, Map<String, RedisCacheConfiguration> initialCacheConfigurations, boolean allowInFlightCacheCreation) {
        super(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations, allowInFlightCacheCreation);
        this.cacheWriter = cacheWriter;
        this.defaultCacheConfig = defaultCacheConfiguration;
    }

    public RedisCacheManagerResolver(RedisConnectionFactory redisConnectionFactory, RedisCacheConfiguration cacheConfiguration) {
        this(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory),cacheConfiguration);
    }

    /**
     * 覆盖父类创建RedisCache,采用自定义的RedisCacheResolver
      * @Title: createRedisCache 
      * @Description: TODO
      * @param  @param name
      * @param  @param cacheConfig
      * @param  @return 
      * @throws 
      *
     */
    @Override
    protected RedisCache createRedisCache(String name, @Nullable RedisCacheConfiguration cacheConfig) {
        return new RedisCacheResolver(name, cacheWriter, cacheConfig != null ? cacheConfig : defaultCacheConfig);
    }

    @Override
    public Map<String, RedisCacheConfiguration> getCacheConfigurations() {
        Map<String, RedisCacheConfiguration> configurationMap = new HashMap<>(getCacheNames().size());
        getCacheNames().forEach(it -> {
            RedisCache cache = RedisCacheResolver.class.cast(lookupCache(it));
            configurationMap.put(it, cache != null ? cache.getCacheConfiguration() : null);
        });
        return Collections.unmodifiableMap(configurationMap);
    }
}

        至此我们就完成了关键的步骤,最后只需要RedisConfig中管理我们自己的RedisCacheManagerResolver即可

	public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
		// 设置全局过期时间,单位为秒
		Duration timeToLive = Duration.ZERO;
		timeToLive = Duration.ofSeconds(timeOut);
//		RedisCacheManager cacheManager = new RedisCacheManager(
//				RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory),
//				this.getRedisCacheConfigurationWithTtl(timeToLive), // 默认策略,未配置的 value 会使用这个
//				this.getRedisCacheConfigurationMap() // 指定 value策略
//		);
		RedisCacheManagerResolver cacheManager =
            new RedisCacheManagerResolver(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory),
                this.getRedisCacheConfigurationWithTtl(timeToLive), // 默认策略,未配置的 value 会使用这个
                this.getRedisCacheConfigurationMap() // 指定 value策略
            );

//		RedisCacheManager cacheManager = RedisCacheManager.builder(factory).cacheDefaults(config).build();
		return cacheManager;
	}

      经过上面的步骤,我们已经实现了重写evict方法,用来模糊删除缓存了

@CacheEvict(value = "BASE_CACHE, key = "#modelClassName + #telenetId+ '*'", allEntries = false)

只需要用上面这个注解,我们就可以删除telnetID下所有的缓存了


版权声明:本文为Crystalqy原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。