问题
使用动态数据源后,当我们使用注解进行缓存操作时,例如下面的写法。如果先缓存数据源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的原理主要基于以下几个基本概念:
- 缓存注解:Spring Cache提供了一系列注解,如@Cacheable、@CacheEvict、@CachePut等,用于标记需要进行缓存操作的方法。这些注解可以直接应用于方法上,告诉Spring框架在方法执行前后进行哪些缓存操作。
- 缓存解析器(CacheResolver):当方法上标记了缓存注解后,Spring Cache会根据配置的缓存解析器(Cache Resolver)来确定使用哪个缓存管理器(Cache Manager)进行缓存操作。缓存解析器可以根据方法的参数、返回值等信息来选择合适的缓存管理器。resolveCaches会根据传入的上下文,返回匹配的Cache。
- 缓存管理器:缓存管理器是Spring Cache的核心组件,它负责具体的缓存实现。Spring Cache支持多种缓存实现,如EhCache、Redis、Caffeine等。开发者可以根据实际需求选择合适的缓存实现,并通过配置将其与Spring Cache集成。
- 键生成器(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;
}
}
}
这两种方法提供了解决动态数据源缓存冲突问题的选择,可以根据项目需求来选取最适合的实现方案。