欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-03-04 15:29:09
目录排查@cacheevict注解失效下面是我通过源码跟踪排查问题的过程小结一下说说spring全家桶中@cacheevict无效情况举个例子排查@cacheevict注解失效我简单看了一下《spri...

排查@cacheevict注解失效

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

代码如下:

@cacheable("taskparamscache")
list<taskparams> 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<object> 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检测到了。

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

进入cglibmethodinvocation(proxy, target, method, args, targetclass, chain, methodproxy).proceed()方法内部

org.springframework.aop.framework.reflectivemethodinvocation#proceed

@override
	@nullable
	public 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<cacheoperation> 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<cacheputrequest> 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的缓存

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

然后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的注释:

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

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

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

@cacheable(value = "taskparamscache", key = "#taskid")
list<taskparams> 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();
  }
}

以上为个人经验,希望能给大家一个参考,也希望大家多多支持。