一次排查@CacheEvict注解失效的经历及解决
排查@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检测到了。
进入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的缓存
然后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<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
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(); } }
以上为个人经验,希望能给大家一个参考,也希望大家多多支持。