动态数据源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;
        }

    }
}

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

相关推荐
hERS EOUS1 小时前
SpringBoot 使用 spring.profiles.active 来区分不同环境配置
spring boot·后端·spring
超梦dasgg2 小时前
智慧充电系统设备管理服务对外接口实现方案
java·spring·微服务
xiaoye37083 小时前
Spring 事务传播机制 + 隔离级别
java·后端·spring
xuhaoyu_cpp_java4 小时前
Spring学习(一)
java·经验分享·笔记·学习·spring
苍煜6 小时前
SpringBoot AOP切面编程精讲:实现方式、Spring区别及与自定义注解生产实战
java·spring boot·spring
流年似水~9 小时前
Java新手5分钟接AI:Spring AI Alibaba实战
java·人工智能·spring
jnrjian9 小时前
Library Cache Load Lock library cache pins are replaced by mutexes
java·后端·spring
9523610 小时前
SpringAOP
java·后端·学习·spring
zx28596340011 小时前
Laravel6.x新特性全解析
java·后端·spring
未若君雅裁12 小时前
Spring Statemachine 实战入门:从零实现一个订单状态流转 Demo
java·spring·状态模式