一次排查@CacheEvict注解失效的经历及解决

网友投稿 1721 2022-11-12

一次排查@CacheEvict注解失效的经历及解决

一次排查@CacheEvict注解失效的经历及解决

目录排查@CacheEvict注解失效下面是我通过源码跟踪排查问题的过程小结一下说说spring全家桶中@CacheEvict无效情况举个例子

排查@CacheEvict注解失效

我简单看了一下《Spring实战》中的demo,然后就应用到业务代码中了,本以为如此简单的事情,竟然在代码提交后的1个周,被同事发现。selectByTaskId()方法查出来的数据总是过时的。

代码如下:

@Cacheable("taskParamsCache")

List selectByTaskId(Long taskId);

// ...

// ...

@CacheEvict("taskParamsCache")

int deleteByTaskId(Long taskId);

想要的效果是当程序调用selectByTaskId()方法时,把结果缓存下来,然后在调用deleteByTaskId()方法时,将缓存清空。

经过数据库数据对比之后,把问题排查的方向定位在@CacheEvict注解失效了。

下面是我通过源码跟踪排查问题的过程

在deleteByTaskId()方法的调用出打断点,跟进代码到spring生成的代理层。

@Override

@Nullable

public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {

Object oldProxy = null;

boolean setProxyContext = false;

Object target = null;

TargetSource targetSource = this.advised.getTargetSource();

try {

if (this.advised.exposeProxy) {

// Make invocation available if necessary.

oldProxy = AopContext.setCurrentProxy(proxy);

setProxyContext = true;

}

// Get as late as possible to minimize the time we "own" the target, in case it comes from a pool...

target = targetSource.getTarget();

Class> targetClass = (target != null ? target.getClass() : null);

List chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

Object retVal;

// Check whether we only have one InvokerInterceptor: that is,

// no real advice, but just reflective invocation of the target.

if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {

// We can skip creating a MethodInvocation: just invoke the target directly.

// Note that the final invoker must be an InvokerInterceptor, so we know

// it does nothing but a reflective operation on the target, and no hot

// swapping or fancy proxying.

Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);

retVal = methodProxy.invoke(target, argsToUse);

}

else {

// We need to create a method invocation...

retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();

}

retVal = processReturnType(proxy, target, method, retVal);

return retVal;

}

finally {

if (target != null && !targetSource.isStatic()) {

targetSource.releaseTarget(target);

}

if (setProxyContext) {

// Restore old proxy.

AopContext.setCurrentProxy(oldProxy);

}

}

}

通过getInterceptorsAndDynamicInterceptionAdvice获取到当前方法的-,里面包含了CacheIneterceptor,说明注解被spring检测到了。

进入CglibMethodInvocation(proxy, target, methodFqvkTCQ, args, targetClass, chain, methodProxy).proceed()方法内部

org.springframework.aop.framework.ReflectiveMethodInvocation#proceed

@Override

@Nullable

pFqvkTCQublic Object proceed() throws Throwable {

// We start with an index of -1 and increment early.

if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {

return invokeJoinpoint();

}

Object interceptorOrInterceptionAdvice =

this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);

if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {

// Evaluate dynamic method matcher here: static part will already have

// been evaluated and found to match.

InterceptorAndDynamicMethodMatcher dm =

(InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;

if (dm.methodMatcher.matches(this.method, this.targetClass, this.arguments)) {

return dm.interceptor.invoke(this);

}

else {

// Dynamic matching failed.

// Skip this interceptor and invoke the next in the chain.

return proceed();

}

}

else {

// It's an interceptor, so we just invoke it: The pointcut will have

// been evaluated statically before this object was constructed.

return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);

}

}

this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex)方法取第一个-,正是我们要关注的CacheIneterceptor,然后调用((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this)方法,继续跟进

org.springframework.cache.interceptor.CacheInterceptor#invoke

@Override

@Nullable

public Object invoke(final MethodInvocation invocation) throws Throwable {

Method method = invocation.getMethod();

CacheOperationInvoker aopAllianceInvoker = () -> {

try {

return invocation.proceed();

}

catch (Throwable ex) {

throw new CacheOperationInvoker.ThrowableWrapper(ex);

}

};

try {

return execute(aopAllianceInvoker, invocation.getThis(), method, invocation.getArguments());

}

catch (CacheOperationInvoker.ThrowableWrapper th) {

throw th.getOriginal();

}

}

进入execute方法

protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) {

// Check whether aspect is enabled (to cope with cases where the AJ is pulled in automatically)

if (this.initialized) {

Class> targetClass = getTargetClass(target);

CacheOperationSource cacheOperationSource = getCacheOperationSource();

if (cacheOperationSource != null) {

Collection operations = cacheOperationSource.getCacheOperations(method, targetClass);

if (!CollectionUtils.isEmpty(operations)) {

return execute(invoker, method,

new CacheOperationContexts(operations, method, args, target, targetClass));

}

}

}

return invoker.invoke();

}

cacheOperationSource记录系统中所有使用了缓存的方法,cacheOperationSource.getCacheOperations(method, targetClass)能获取deleteByTaskId()方法缓存元数据,然后执行execute()方法

@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, 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 cachePutRequests = new LinkedList<>();

if (cacheHit == null) {

collectPutRequests(contexts.get(CacheableOperation.class),

CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);

}

Object cacheValue;

Object returnValue;

if (cacheHit != null && cachePutRequests.isEmpty() && !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;

}

这里大致过程是:

先执行beforInvokeEvict ---- 执行数据库delete操作 --- 执行CachePut操作 ---- 执行afterInvokeEvict

我们的注解是方法调用后再使缓存失效,直接所以有效的操作应在倒数第2行

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);

}

else {

if (key == null) {

key = generateKey(context, result);

}

logInvalidating(context, operation, key);

doEvict(cache, key);

}

}

}

这里通过context.getCaches()获取到name为taskParamsCache的缓存

然后generateKey生成key,注意这里,发现生成的key是com.xxx.xxx.atomic.impl.xxxxdeleteByTaskId982,但是缓存中的key却是com.xxx.xxx.atomic.impl.xxxxselectByTaskId982,下面调用的doEvict(cache, key)方法不再跟进了,就是从cache中移除key对应值。明显这里key对应不上的,这也是导致@CacheEvict没有生效的原因。

小结一下

我还是太大意了,当时看了注解@CacheEvict的对key的注释:

大意就是如果没有指定key,那就会使用方法所有参数生成一个key,明显com.xxx.xxx.atomic.impl.xxxxselectByTaskId982是方法名 + 参数,可是你没说把方法名还加上了啊,说好的只用参数呢,哈哈,这个bug是我使用不当引出的,很多人不会犯这种低级错误。

解决办法就是使用SpEL明确定义key

@Cacheable(value = "taskParamsCache", key = "#taskId")

List selectByTaskId(Long taskId);

// ...

// ...

@CacheEvict(value = "taskParamsCache", key = "#taskId")

int deleteByTaskId(Long taskId);

说说spring全家桶中@CacheEvict无效情况

@CacheEvict(value =“test”, allEntries = true)

1、使用@CacheEvict注解的方法必须是controller层直接调用,service里间接调用不生效。

2、原因是因为key值跟你查询方法的key值不统一,所以导致缓存并没有清除

3、把@CacheEvict的方法和@Cache的方法放到一个java文件中写,他俩在两个java文件的话,会导致@CacheEvict失效。

4、返回值必须设置为void

@CacheEvict annotation

It is important to note that void methods can be used with @CacheEvict

5、@CacheEvict必须作用在走代理的方法上

在使用Spring @CacheEvict注解的时候,要注意,如果类A的方法f1()被标注了 @CacheEvict注解,那么当类A的其他方法,例如:f2(),去直接调用f1()的时候, @CacheEvict是不起作用的,原因是 @CacheEvict是基于Spring AOP代理类,f2()属于内部方法,直接调用f1()时,是不走代理的。

举个例子

不生效:

@Override

public void saveEntity(Menu menu) {

try {

mapper.insert(menu);

//Cacheable 不生效

this.test();

}catch(Exception e){

e.printStackTrace();

}

}

@CacheEvict(value = "test" , allEntries = true)

public void test() {

}

正确使用:

@Override

@CacheEvict(value = "test" , allEntries = true)

public void saveEntity(Menu menu) {

try {

mapper.insert(menu);

}catch(Exception e){

e.printStackTrace();

}

}

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:Directx 向量
下一篇:导出 CSDN 博客至 Markdown 或 PDF 格式 (近乎完美)
相关文章

 发表评论

暂时没有评论,来抢沙发吧~