状况是这样的:
因为某业务上的需求,须要数据库表T的数据发生变化时,通知给其它系统。
改造前,在各数据发生变化的业务节点,直接调用通知方法,将数据同步给其它系统;
改造后,使用了spring aop,基于抽象出的update方法,进行数据同步。java
因而代码改为了这样:mysql
##切面类 class NotifyXSystemAOP{ //切面定义 @Pointcut("execution(* com.xxx.updateMethod(com.xxx.XBean)) && args(record)") public void updatePointcut(XBean xBean){} //切面逻辑 @After(value = "updatePointcut(record)",argNames = "xBean") public void afterUpdate(XBean xBean){ //要将全量数据同步给兄弟系统,因此查了库 xBean = xBeanMapper.selectByPrimaryKey(xBean.getId()); //利用线程池,将数据同步至兄弟系统`XSystem` ThreadManager.executeTask(()-> { sendToXSystem(xBean); }); } } ##业务类关键方法 public void keyMethod(){ ... xBean.setPropertyA(999); com.xxx.updateMethod(xBean); //修改动做 ... }
看着一副还算合理的样子,测试时却发现推送的xBean
对象与数据库表T中的数据不一致(好比库中PropertyA字段=999,推送数据PropertyA=1)spring
发现问题后,先检查了切面中的逻辑,发现 切面中确实没有任何修改XBean
的痕迹。
并且从日志观察,业务中的修改方法确实已经成功执行了,才进入了切面。
因此愈加的以为这个问题很诡异!sql
众所周知,mysql默认的事务隔离级别是可重复读(Repeatable read),有没有多是这样的?
业务方法中执行了修改,因为事务还没提交,因此切面中查询时没有获取到最新的属性值,而把旧版本数据推送给兄弟系统了。数据库
有了思路,就进行求证喽。因而在切面方法中增长了日志:缓存
@After(value = "updatePointcut(record)",argNames = "xBean") public void afterUpdate(XBean xBean){ log.info("查询前xBean={}",xBean); //增长日志——查询前 xBean = xBeanMapper.selectByPrimaryKey(xBean.getId()); log.info("查询后xBean={}",xBean); //增长日志——查询后 ThreadManager.executeTask(()-> { sendToXSystem(xBean); }); }
若是确实如咱们猜想,问题是由事务引发的,那么查询后和查询前的PropertyA
属性值会不同。
从新执行,观察日志,结论是:查询先后,PropertyA的值同样!也就证实——推论错误,凶手不是事务。session
其实仔细想一想,也能想明白。
代码里是在切面主线程中作的数据库查询。而切面AOP的本质是动态代理,那么一个代理对象,又在同一个线程中,先执行业务方法逻辑,再执行切面中切入的逻辑,若是没有什么特殊的事务控制,这些逻辑理应在一个事务当中。mybatis
经过新增的日志,发现xBean确实已经修改过了——新增日志中的xBean和数据库表的数据彻底一致。
问题在于,只是到推送时(sendToXSystem(xBean)方法
处),某些属性,如PropertyA就发生了改变。app
因而有了第二种猜想,会不会切面在推送数据过程当中,业务方法中有修改逻辑?框架
先来复习一波java参数传递的问题:
# 参数传递demo void methodK(){ Person person = new Person(); person.setName("zhang3"); changeName(person); //传递person对象 System.out.println(person); //重写了person的toString方法 } void changeName(Person person){ person.setName("li4"); //方法内部修改了name属性 }
程序并不复杂,输出结果中:
name=li4
这不难解释,因为changeName方法
中的person和methodK方法
中的person指向同一个对象,因此changeName的修改会致使name属性的变化。
基于此,我猜想问题就出在这里,因而搜索了业务方法:
##业务类关键方法 public void keyMethod(){ ... xBean.setPropertyA(999); com.xxx.updateMethod(xBean); //修改动做 ... xBean.setPropertyA(1); //真的找到了这货 }
正所谓“成也参数传递,败也参数传递”。
思考一下好像有些不对,由于在切面方法中,一开始就从新修改了xBean对象的引用:
xBean = xBeanMapper.selectByPrimaryKey(xBean.getId()); //查库,修改了xBean的引用
还用前面的demo模拟,这至关于做了以下修改:
# 对象传递demo void methodK(){ Person person = new Person(); person.setName("zhang3"); changeName(person); System.out.println(person); } void changeName(Person person){ person = new Person(); //修改引用 person.setName("li4"); }
执行结果会变成:
name=zhang3
也就是说,随着changeName方法从新修改了引用指向,导致changeName方法
中的person和methodK方法
中的person指向不一样对象,所以changeName不能再改变methodK-person指向的对象了。
这个理论一样适用于咱们遇到的问题,因此这也不能解释为何推送数据发生变化了。
不死心的我再次翻看了业务方法:
##业务类关键方法 public void keyMethod(){ ... xBean.setPropertyA(999); com.xxx.updateMethod(xBean); //修改动做,触发切面 ... xBean.setPropertyA(1); //赋值 }
PropertyA最终被赋值为1,而切面中最终推送的数据里PropertyA也等于1。
这有些过于巧合了吧,因而耐着性子再仔细找找:
##业务类关键方法 public void keyMethod(){ ... xBean.setPropertyA(999); com.xxx.updateMethod(xBean); //修改动做,触发切面 ... xBean = xBeanMapper.selectByPrimaryKey(xBean.getId()); //又找到了这个 ... xBean.setPropertyA(1); }
业务方法中,调用了和切面中一样的查询方法。咱们用的orm框架是mybatis,而mybatis彷佛有个一级缓存?
科普下什么是mybatis的一级缓存:
结合spring以后,sqlsession默认会和事务绑定。
那么状况应该是这样的:
代码中切面先从db中查询告终果,放入一级缓存中;以后业务逻辑再次执行一样的sql,因为缓存中已有结果,直接从缓存中获取(这里获取的对象和切面中相同)。以后业务逻辑从新赋值PropertyA,再以后切面中用另外一个线程推送被修改的对象,完成绝杀。
解决方式简单粗暴,直接在切面中搞个副本就好:
@After(value = "updatePointcut(record)",argNames = "xBean") public void afterUpdate(XBean xBean){ xBean = xBeanMapper.selectByPrimaryKey(xBean.getId()); xBean = BeanUtils.cloneBean(xBean); //查询后建立副本 ThreadManager.executeTask(()-> { sendToXSystem(xBean); }); }
最后总结一下吧:这是一篇以aop勾引你进来,主打惊悚悬疑,中间夹琐事务隔离级别、对象参数传递和mybatis一级缓存的奇葩文章……