首页 增强RedisCache中@CacheEvict实现key的模糊匹配
文章
取消

增强RedisCache中@CacheEvict实现key的模糊匹配

@CacheEvict可以清除指定的 key,同时可以指定allEntries = true清空 namespace 下的所有元素,这也导致缺乏灵活性。

redis 的查询和删除都可以做模糊匹配,所以如何让@CacheEvict也支持模糊匹配清除?

@CacheEvict 实现原理

@CacheEvict是通过 AOP 实现的,其中核心的类是CacheAspectSupport,具体的调用路径如下:

CacheIntercepter 工作时调用超类方法

  • AOP Alliance MethodInterceptor for declarative cache management using the common Spring caching infrastructure (org.springframework.cache.Cache).

  • Derives from the CacheAspectSupport class which contains the integration with Spring’s underlying caching API.

  • CacheInterceptor simply calls the relevant superclass methods in the correct order.

  • CacheInterceptors are thread-safe.

1
2
3
4
5
6
7
8
9
10
11
public class CacheInterceptor extends CacheAspectSupport implements MethodInterceptor, Serializable {

  @Override
  @Nullable
  public Object invoke(final MethodInvocation invocation) throws Throwable {
    // ...
    try {
      return execute(aopAllianceInvoker, target, method, invocation.getArguments());
    }
  }
}

CacheAspectSupport 执行具体的增强方法

具体的执行过程如下:

  1. 同步执行逻辑:代码检查是否需要同步执行(Synchronized Invocation),简而言之就是如果@Cacheable中满足相应的条件,则会调用线程安全的方法来从缓存中取出缓存实例(cache),否则它将直接调用底层方法(invokeOperation);
  2. 处理提前失效的缓存@CacheEvict(beforeInvocation = true):类似于与Intercepter中的preHandler(),z直接调用相应的清楚缓存方法processCacheEvicts
  3. 检查是否存在@Cacheable(condition/unless = '...') @CachePut(),分为以下几种情况:
    • 有可用缓存,无需存储新的缓存:直接使用缓存中数据wrapCacheValue作为返回值;
    • 无可用缓存,需要存储新的缓存:调用底层方法(invokeOperation)获得数据作为返回值同时存入缓存;
  4. 处理延迟失效的缓存@CacheEvict
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
@Nullable
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, handleSynchronizedGet(invoker, key, cache));
      }
      catch (Cache.ValueRetrievalException ex) {
        // Directly propagate ThrowableWrapper from the invoker,
        // or potentially also an IllegalArgumentException etc.
        ReflectionUtils.rethrowRuntimeException(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 ArrayList<>();
  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 缓存清理

可以看到,除了一堆判断beforeInvocaitonisConditionPassing的逻辑来从上面论述的execute()不同位置跳出来完成对应的@Cacheable(conditon='...')beforInvocaiton操作,核心是通过调用doClear()doEvict()来完成清理工作。

allEntries = true时会执行doClear(),否则执行doEvict()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private void processCacheEvicts(
  Collection<CacheOperationContext> contexts, boolean beforeInvocation, @Nullable Object result) {

  for (CacheOperationContext context : contexts) {
    CacheEvictOperation operation = (CacheEvictOperation) context.metadata.operation;
    if (beforeInvocation == operation.isBeforeInvocation() && isConditionPassing(context, result)) {
      performCacheEvict(context, operation, result);
    }
  }
}

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

  Object key = null;
  for (Cache cache : context.getCaches()) {
    if (operation.isCacheWide()) {
      logInvalidating(context, operation, null);
      doClear(cache, operation.isBeforeInvocation());
    }
    else {
      if (key == null) {
        key = generateKey(context, result);
      }
      logInvalidating(context, operation, key);
      doEvict(cache, key, operation.isBeforeInvocation());
    }
  }
}

doEvict/doClear

抽象类AbstractCacheInvoker提供了相应的接口方法,最终RedisCache类中提供了相应的evictclear具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
protected void doEvict(Cache cache, Object key, boolean immediate) {
  try {
    if (immediate) {
      cache.evictIfPresent(key);
    }
    else {
      cache.evict(key);
    }
  }
  catch (RuntimeException ex) {
    getErrorHandler().handleCacheEvictError(ex, cache, key);
  }
}

/**
 * Execute {@link Cache#clear()} on the specified {@link Cache} and
 * invoke the error handler if an exception occurs.
 */
protected void doClear(Cache cache, boolean immediate) {
  try {
    if (immediate) {
      cache.invalidate();
    }
    else {
      cache.clear();
    }
  }
  catch (RuntimeException ex) {
    getErrorHandler().handleCacheClearError(ex, cache);
  }
}

可以看到,最终通过cacheWriter将相应的序列化后的key和删除操作一同写出。

我们看到如果clear()这个方法,其实也是模糊删除的,只是他的key规则是namespace:: *

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Override
public void evict(Object key) {
  cacheWriter.remove(name, createAndConvertCacheKey(key));
}

@Override
public void clear() {
  byte[] pattern = conversionService.convert(createCacheKey("*"), byte[].class);
  cacheWriter.clean(name, pattern);
}

// createCacheKey构造key
protected String createCacheKey(Object key) {

  String convertedKey = convertKey(key);

  if (!cacheConfig.usePrefix()) {
    return convertedKey;
  }

  return prefixCacheKey(convertedKey);
}
// 有前缀的情况下
private String prefixCacheKey(String key) {

  // allow contextual cache names by computing the key prefix on every call.
  return cacheConfig.getKeyPrefixFor(name) + key;
}

实现自定义的evict方法

重点需要重写RedisCacheevict方法,下面新建一个RedisCacheResolver继承RedisCache,并且重写evict方法。

这里的通配符参考了上述clear()中的convert(createCacheKey("*"), byte[].class),当然也可以实现其他诸如正则表达式之类的匹配逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class RedisCacheResolver extends RedisCache {

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

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

  }

  @Override
  public void evict(Object key) {

    if (key instanceof String) {
      String keyString = key.toString();
      // 后缀删除
      if (StringUtils.endsWithIgnoreCase(keyString, "*")) {
        evictLikeSuffix(keyString);
        return;
      }

      super.evict(key);
    }
  }

  private void evictLikeSuffix(String keyString) {
    // 后缀删除
    byte[] pattern = this.conversionService.convert(this.createCacheKey(keyString), byte[].class);
    if (pattern == null) {
      return;
    }
    this.cacheWriter.clean(this.name, pattern);
  }
}

将自定义的RedisCache加入到RedisCacheManager中

通过继承RedisCacheManager后重写createRedisCache()将之前自定义的RedisCacheResolver加入管理,这样实例化的RedisCache就是最后调用前文中自定义的evict()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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);
  }


  @Override
  protected RedisCache createRedisCache(String name, 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(name -> {
      RedisCache cache = RedisCacheResolver.class.cast(lookupCache(name));
      configurationMap.put(name, cache != null ? cache.getCacheConfiguration() : null);
    });
    return Collections.unmodifiableMap(configurationMap);
  }
}

最后加入到IOC容器中随配置文件一同加载

这里构造的cacheManager中所使用的都是默认的配置文件,如有其他自定义配置也可以在实例化redisCacheManager中加入,比如设置ttl之类的,这里先不赘述了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Configuration
public class RedisConfig {

	// ...

  @Bean
  public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory,
      RedisCacheConfiguration defaultCacheConfiguration,
      Map<String, RedisCacheConfiguration> cacheConfigurationMap) {
    // Create and configure a custom RedisCacheManagerResolver
    RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(
        redisConnectionFactory);

    return new RedisCacheManagerResolver(cacheWriter,
        defaultCacheConfiguration, cacheConfigurationMap);
  }

  @Bean
  public RedisCacheConfiguration defaultCacheConfiguration() {
    return RedisCacheConfiguration.defaultCacheConfig();
  }


  @Bean
  public Map<String, RedisCacheConfiguration> cacheConfigurationMap() {
    return new LinkedHashMap<>();
  }
}

使用

如下代码所示,该方法调用时就会将Redis中user.name开头的key删除,不至于影响其他的缓存。

1
2
3
4
5
6
@PutMapping
@CacheEvict(value = "userCache", key = "#user.name + '*'")
public User update(User user) {
  userService.updateById(user);
  return user;
}
本文由作者按照 CC BY 4.0 进行授权