动态数据源spring cache冲突处理

问题

使用动态数据源后,当我们使用注解进行缓存操作时,例如下面的写法。如果先缓存数据源1的值,然后数据源2访问该方法且参数相同,那么数据源2会直接获取到数据源1的结果,获取到错误的缓存值,出现冲突。

java 复制代码
    @Cacheable(value = "objCache", key = "(#root.targetClass.getSimpleName()).concat(#root.method.name).concat(':key:').concat(#key)", unless = "#result eq null")
    public String xxx(String key) {
        return demoMapper.xxx(key);
    }

分析

一、基本概念

Spring Cache是Spring框架提供的一个抽象层,用于简化缓存的使用。它允许开发者通过注解的方式轻松地实现缓存功能,而不需要关心具体的缓存实现细节。Spring Cache的原理主要基于以下几个基本概念:

  1. 缓存注解:Spring Cache提供了一系列注解,如@Cacheable、@CacheEvict、@CachePut等,用于标记需要进行缓存操作的方法。这些注解可以直接应用于方法上,告诉Spring框架在方法执行前后进行哪些缓存操作。
  1. 缓存解析器(CacheResolver):当方法上标记了缓存注解后,Spring Cache会根据配置的缓存解析器(Cache Resolver)来确定使用哪个缓存管理器(Cache Manager)进行缓存操作。缓存解析器可以根据方法的参数、返回值等信息来选择合适的缓存管理器。resolveCaches会根据传入的上下文,返回匹配的Cache。
  2. 缓存管理器:缓存管理器是Spring Cache的核心组件,它负责具体的缓存实现。Spring Cache支持多种缓存实现,如EhCache、Redis、Caffeine等。开发者可以根据实际需求选择合适的缓存实现,并通过配置将其与Spring Cache集成。
  3. 键生成器(Key Generator):根据方法信息生成key值。

二、注解缓存的执行部分逻辑

spring cache定义了缓存的执行流程,而开放了缓存的管理。

CacheInterceptor 拦截了注解缓存操作,下面是它的execute方法。

scss 复制代码
    protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) {
        if (this.initialized) {
            Class<?> targetClass = getTargetClass(target);
            CacheOperationSource cacheOperationSource = getCacheOperationSource();
            if (cacheOperationSource != null) {
                // 获取当前方法的CacheOperation
                Collection<CacheOperation> operations = cacheOperationSource.getCacheOperations(method, targetClass);
                if (!CollectionUtils.isEmpty(operations)) {
                  //  构建缓存操作的上下文,
                    return execute(invoker, method,
                            new CacheOperationContexts(operations, method, args, target, targetClass));
                }
            }
        }
        return invoker.invoke();
    }

CacheOperationContexts包含了不同cache操作的的上下文信息,内部由MultiValueMap维护。

在创建cache上下文CacheOperationContext对象时,会调用CacheResolver的resolveCaches方法,匹配该操作对应的Cache,所以我们可以从CacheResolver入手,例如给cacheNames拼接上当前访问数据源的信息,这么做相当于从cache Manager中根据新的cacheName(数据源信息 + "objCache")获取Cache,不同数据源命中的cache是不同的。

接着执行execute重载方法:

scss 复制代码
     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
        // 如果是@CacheEvict并配置了beforeInvocation是true,代表在执行实际方法前清除缓存
          processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
                    CacheOperationExpressionEvaluator.NO_RESULT);

          // Check if we have a cached item matching the conditions
        //  检查是否有@Cacheable命中缓存
          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
             // 如果没有@CachePut注解操作,直接获取缓存值
               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
        // 处理@CacheEvict操作
          processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);

          return returnValue;
     }

上面处理注解的操作都是根据key来运行的。

CacheOperationContext的generateKey方法:

kotlin 复制代码
		protected Object generateKey(@Nullable Object result) {
			if (StringUtils.hasText(this.metadata.operation.getKey())) {
				EvaluationContext evaluationContext = createEvaluationContext(result);
				return evaluator.key(this.metadata.operation.getKey(), this.metadata.methodKey, evaluationContext);
			}
			return this.metadata.keyGenerator.generate(this.target, this.metadata.method, this.args);
		}

如果@Cacheable配置了key,将会使用表达式解析器解析,如果未配置会调用keyGenerator。

我们可以不配置key,自定义keyGenerator,来解决冲突,相当于在同一个Cache中,不同数据源通过key区分了。

解决

方法一、自定义CacheResolver

自定义CacheResolver,把不同的数据源根据cacheName命中到不同的cache。

typescript 复制代码
public class CustomCacheResolver implements CacheResolver {

        private CacheManager cacheManager;

        public CustomCacheResolver(CacheManager cacheManager) {
            this.cacheManager = cacheManager;
        }

        @Override
        public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) {
            Collection<String> cacheNames = getCacheNames(context);
            if (cacheNames == null) {
                return Collections.emptyList();
            }
            Collection<Cache> result = new ArrayList<>(cacheNames.size());
            for (String cacheName : cacheNames) {
                // 使用数据源信息 + cacheName,将不同数据源区分开
                cacheName = DSHolder.getDSMsg() + "_" + cacheName;
                Cache cache = getCacheManager().getCache(cacheName);
                if (cache == null) {
                    throw new IllegalArgumentException("Cannot find cache named '" + cacheName + "' for " + context.getOperation());
                }
                result.add(cache);
            }
            return result;
        }

        protected Collection<String> getCacheNames(CacheOperationInvocationContext<?> context) {
            return context.getOperation().getCacheNames();
        }

        public CacheManager getCacheManager() {
            Assert.state(this.cacheManager != null, "No CacheManager set");
            return this.cacheManager;
        }

        public void setCacheManager(CacheManager cacheManager) {
            this.cacheManager = cacheManager;
        }

    }

上面的方法,基本复制的AbstractCacheResolver,唯一的不同处就是cacheName = DSHolder.getDSMsg() + "_" + cacheName重新给cacheName赋值了。

我们可以把自定义CacheResolver通过@bean注入到spring中,然后注解指定CacheResolver。

java 复制代码
    @Bean("customCacheResolver")
    public CacheResolver cacheResolver() {
        CustomCacheResolver resolver = new CustomCacheResolver(自己系统的cache manager);
        return resolver;
    }
typescript 复制代码
    @Cacheable(value = "objCache",  cacheResolver = "customCacheResolver",
key = "(#root.targetClass.getSimpleName()).concat(#root.method.name).concat(':key:').concat(#key)", unless = "#result eq null")
    public String xxx(String key) {
        return demoMapper.xxx(key);
    }

方法二、自定义KeyGenerator

自定义KeyGenerator,不同的数据源在同一个cache中根据key命中到不同的缓存值。

typescript 复制代码
 public class CustomKeyGenerator implements KeyGenerator {
        @Override
        public Object generate(Object target, Method method, Object... params) {
            StringBuilder keyBuilder = new StringBuilder();
            keyBuilder.append(target.getClass().getSimpleName()).append(":").append(method.getName()).append(":");
            for (Object param : params) {
                // 兼容null参数
                keyBuilder.append(Optional.ofNullable(param).map(Object::toString).orElse("null")).append(":");
            }
            keyBuilder.deleteCharAt(keyBuilder.length() - 1);
            // 添加前缀
            String key = DSHolder.getDSMsg() + ":" + keyBuilder;
            return key;
        }
    }

上面的生成规则就是拼接类名、方法名、参数值并用:隔开,最后添加上前缀。

我们可以把自定义KeyGenerator通过@bean注入到spring中,然后注解指定KeyGenerator。

typescript 复制代码
    @Bean
    public KeyGenerator keyGenerator() {
        return new CustomKeyGenerator();
    }
typescript 复制代码
    @Cacheable(value = "objCache", keyGenerator = "keyGenerator",  unless = "#result eq null")
    public String xxx(String key) {
        return demoMapper.xxx(key);
    }

扩展

如果想注解不指定keyGenerator或者cacheResolver,让我们的自定义类全局生效。

我们可以实现类CachingConfigurer,例如让KeyGenerator全局生效。

typescript 复制代码
@Configuration
public class MyCachingConfigurer implements CachingConfigurer {
    @Override
    public KeyGenerator keyGenerator() {
        return new CustomKeyGenerator();
    }

    public class CustomKeyGenerator implements KeyGenerator {
        @Override
        public Object generate(Object target, Method method, Object... params) {
            StringBuilder keyBuilder = new StringBuilder();
            keyBuilder.append(target.getClass().getSimpleName()).append(":").append(method.getName()).append(":");
            for (Object param : params) {
                // 兼容null参数
                keyBuilder.append(Optional.ofNullable(param).map(Object::toString).orElse("null")).append(":");
            }
            keyBuilder.deleteCharAt(keyBuilder.length() - 1);
            // 添加前缀
            String key = DSHolder.getDSMsg() + ":" + keyBuilder;
            return key;
        }
    }
}

或者让 CacheResolver 全局生效。

typescript 复制代码
@Configuration
public class MyCachingConfigurer implements CachingConfigurer {

    @Override
    public CacheResolver cacheResolver() {
        return new CustomCacheResolver(你自己的cacheManager);
    }

    public class CustomCacheResolver implements CacheResolver {

        private CacheManager cacheManager;

        public CustomCacheResolver(CacheManager cacheManager) {
            this.cacheManager = cacheManager;
        }

        @Override
        public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) {
            Collection<String> cacheNames = getCacheNames(context);
            if (cacheNames == null) {
                return Collections.emptyList();
            }
            Collection<Cache> result = new ArrayList<>(cacheNames.size());
            for (String cacheName : cacheNames) {
                // 使用数据源信息 + cacheName + cacheName,将不同数据源区分开
                cacheName = DSHolder.getDSMsg() + "_" + cacheName;
                Cache cache = getCacheManager().getCache(cacheName);
                if (cache == null) {
                    throw new IllegalArgumentException("Cannot find cache named '" + cacheName + "' for " + context.getOperation());
                }
                result.add(cache);
            }
            return result;
        }

        protected Collection<String> getCacheNames(CacheOperationInvocationContext<?> context) {
            return context.getOperation().getCacheNames();
        }

        public CacheManager getCacheManager() {
            Assert.state(this.cacheManager != null, "No CacheManager set");
            return this.cacheManager;
        }

        public void setCacheManager(CacheManager cacheManager) {
            this.cacheManager = cacheManager;
        }

    }
}

这两种方法提供了解决动态数据源缓存冲突问题的选择,可以根据项目需求来选取最适合的实现方案。

相关推荐
幽络源小助理10 小时前
SpringBoot基于Mysql的商业辅助决策系统设计与实现
java·vue.js·spring boot·后端·mysql·spring
wfsm14 小时前
spring事件使用
java·后端·spring
Exclusive_Cat17 小时前
SpringMVC参数接收与数据返回详解
spring·mvc
ChinaRainbowSea19 小时前
补充:问题:CORS ,前后端访问跨域问题
java·spring boot·后端·spring
hqxstudying21 小时前
java依赖注入方法
java·spring·log4j·ioc·依赖
春生野草21 小时前
关于SpringMVC的整理
spring
Bug退退退1231 天前
RabbitMQ 高级特性之重试机制
java·分布式·spring·rabbitmq
hello早上好1 天前
CGLIB代理核心原理
java·spring
先睡1 天前
Redis的缓存击穿和缓存雪崩
redis·spring·缓存
Bug退退退1231 天前
RabbitMQ 高级特性之死信队列
java·分布式·spring·rabbitmq