记一次AOP问题排查

背景和问题

状况是这样的:
因为某业务上的需求,须要数据库表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=1spring

分析

发现问题后,先检查了切面中的逻辑,发现 切面中确实没有任何修改XBean的痕迹
并且从日志观察,业务中的修改方法确实已经成功执行了,才进入了切面。
因此愈加的以为这个问题很诡异!sql

推理1:事务设置

众所周知,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

推理2:对象传递

因而有了第二种猜想,会不会切面在推送数据过程当中,业务方法中有修改逻辑框架

先来复习一波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属性的变化。
clipboard.png

基于此,我猜想问题就出在这里,因而搜索了业务方法:

##业务类关键方法
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指向的对象了

clipboard.png

这个理论一样适用于咱们遇到的问题,因此这也不能解释为何推送数据发生变化了。

推理3:真正的凶手

不死心的我再次翻看了业务方法:

##业务类关键方法
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的一级缓存:

  • 在同一个sqlsession中,若是某查询sql已经执行过了,会将结果缓存,再次执行此查询sql时直接从缓存中获取结果,以提高性能。(固然,update等操做会做废一级缓存)

结合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一级缓存的奇葩文章……

相关文章
相关标签/搜索