系统中集成了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下所有的缓存了