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

记一次线上问题 → 事务去哪了

程序员文章站 2022-05-07 13:55:44
开心一刻 小羊:哎呀,前面有奶喝 狗妈:这谁呀,走开 小羊:我就喝点,能怎么的嘛 狗妈:你喝就喝,咋还上头了呢? 小羊:真香! 狗妈:这羊犊子,真硬核! 问题背景 一天早上,楼主兴致勃勃的逛着园子的时候,右下角的 QQ 头像嘀嘀嘀的闪了起来,定睛一看,哎我去,肾要开始疼了,不是,头要开始疼了 客服 ......

开心一刻

记一次线上问题 → 事务去哪了

  小羊:哎呀,前面有奶喝

  狗妈:这谁呀,走开

  小羊:我就喝点,能怎么的嘛

  狗妈:你喝就喝,咋还上头了呢?

  小羊:真香!

  狗妈:这羊犊子,真硬核!

问题背景

  一天早上,楼主兴致勃勃的逛着园子的时候,右下角的 qq 头像嘀嘀嘀的闪了起来,定睛一看,哎我去,肾要开始疼了,不是,头要开始疼了

  客服 mm:太躺,有个客户充值成功后,赠送的积分没有到账

  楼主:是不是客户等级不够,不满足资格 ?

  客服 mm:客户等级是够的,他之前的积分都正常到账了

  楼主:之前的积分都到账了 ? 哪个客户,我去看看

  客服 mm:客户名是:xxx,对应的单号是:xxx,找到原因了跟我说下

  楼主:好的,找到原因了第一时间通知你

泰坦是楼主在公司内的花名,也是楼主的 lol 本命英雄,慢慢的被传成太躺了,楼主也很无奈;
有小伙伴问楼主,你和客服 mm 什么关系,光看到头像闪动就肾疼了 ?
  这个问题问得好,改天楼主给你加鸡腿,其实楼主和客服确实挺熟悉的,工作交流挺多的,但是仅限于同事关系! 吾乃心系天下之人,岂能被儿女情长所困 ? 只可惜客服 mm 已名花有主,不然就,嘿嘿嘿,你们懂的(是那姓吾的小子心系天下,楼主不姓吾!)

问题解决

  积分赠送是最近新上的一个功能,上了也有一个多星期了,到目前为止,也就这个客户反馈了这个问题,另外这个客户之前的积分都是赠送到账了的,应该是触发了某些未考虑到的边界条件,产生了异常,导致积分未写入成功,照理来说,这应该是一个事务,要么都成功,要么都不成功呀

  由于这个功能不是楼主开发的,出于快速解决问题的考虑,楼主就找到了对应的开发同事小李,跟他说明了下情况,让他去排查下什么原因

  过了一会,小李找到了楼主,开始了他的排查分享

  小李:太躺,我看了下日志,由于 xxx 情况未考虑到,导致加积分记录的时候抛异常了

  楼主:xxx 情况确实比较特殊,一般很难考虑到,但是为什么存款成功了,积分却没加成功,你用了异步不 care 结果的处理 ?

  小李:我是同步处理的,照理来说,应该要回滚的

  楼主:那就奇了怪了,你把写入积分的方法给我下,我去看看代码

  几分钟过后,楼主找到了小李,跟他说了下怎么改,并且让他把边界限制的处理也加上,走紧急流程升到了线上

  问题解决后,小李又找到了楼主

  小李:太躺啊,为什么之前事务未回滚,而按你说的那么改之后事务就会回滚了 ?

  楼主:你去把你的椅子拿过来,我跟你好好讲讲!

记一次线上问题 → 事务去哪了

问题复现

  注意啊,这不是说升级了之后线上又出现了同样的问题,而是楼主为了让大家更好的了解这个问题,模拟下当时的场景

  数据库版本 5.7.21 、存储引擎 innodb 、隔离级别 rr 、spring的传播机制 required 、声明式事务 @transactional 

  完整代码:,里面的 transactionmisstest ,关键代码如下

/**
 * 存款
 * 引入积分之前的处理
 * @param loginname
 * @param amount
 * @return
 */
@override
@transactional(rollbackfor = exception.class)
public tranmisscredit deposit(string loginname, bigdecimal amount) {
    tranmisscredit credit = creditmapper.getbyloginname(loginname);
    bigdecimal creditafter = credit.getcredit().add(amount);

    tranmisscreditlog creditlog = new tranmisscreditlog(loginname, credit.getcredit(),
            amount, creditafter, "充值: " + amount);
    credit.setcredit(creditafter);

    creditmapper.update(credit);
    int count = creditlogmapper.insert(creditlog);

    return credit;
}

/**
 * 存款
 * 引入积分后的新增的方法
 * @param loginname
 * @param amount
 * @param integration
 * @return
 */
@override
public tranmisscredit deposit(string loginname, bigdecimal amount, int integration) {
    tranmisscredit credit = deposit(loginname, amount);             // 复用之前的存款逻辑

    // 下面是新增的积分业务
    int integrationafter = credit.getintegration() + integration;
    tranmissintegrationlog log = new tranmissintegrationlog(loginname, credit.getintegration(),
            integration, integrationafter, "充值赠送积分: " + integration);
    credit.setintegration(integrationafter);

    creditmapper.update(credit);
    integrationlogmapper.insert(log);
    return credit;
}


// 调用的地方,相当于controller
@autowired
private idepositservice depositservice;

@test
public void deposit() {

    // 积分引入前的调用
    // tranmisscredit credit = depositservice.deposit("zhangsan", new bigdecimal(100));

    // 积分引入后的调用
    tranmisscredit credit = depositservice.deposit("zhangsan", new bigdecimal(100), 10);

}

  看上去好像没毛病吧,楼主你不是蒙我了把 ? 蒙没蒙你,咱们找焦点访谈

记一次线上问题 → 事务去哪了

  我们先看下初始状态,目前只有客户 zhangsan ,其额度 100 ,积分 10 记一次线上问题 → 事务去哪了

  我们来手动造个异常,模拟边界条件的触发,修改新增的 deposit 方法

/**
 * 存款
 * 引入积分后的新增的方法
 * @param loginname
 * @param amount
 * @param integration
 * @return
 */
@override
public tranmisscredit deposit(string loginname, bigdecimal amount, int integration) {
    tranmisscredit credit = deposit(loginname, amount);             // 复用之前的存款逻辑

    // 下面是新增的积分业务
    int integrationafter = credit.getintegration() + integration;
    tranmissintegrationlog log = new tranmissintegrationlog(loginname, credit.getintegration(),
            integration, integrationafter, "充值赠送积分: " + integration);
    credit.setintegration(integrationafter);

    // 模拟异常抛出
    if ("zhangsan".equals(loginname)) {
        throw new runtimeexception("触发边界条件");
    }
    creditmapper.update(credit);
    integrationlogmapper.insert(log);
    return credit;
}

  我们来看看结果记一次线上问题 → 事务去哪了

  哟嚯,额度加成功了,积分却没加成功,事务没生效!是不是有点懵 ?

问题分析

  我们仔细观察下 deposit 方法,一个有 @transactional 修饰,一个没有,就这一个差别;虽说只有这一个差别,但 spring 却在幕后替我们完成了很多事情

  spring 事务原理

    关于这个,我相信大家都能答上来一点,底层实现就是动态代理(你还不知道动态代理 ?那还不赶紧去看:)

    当 spring 检查到 @transactional ,会给目标对象创建一个代理对象,然后在代理对象中给目标对象中被 @transactional 修饰的方法织入事务增强处理,类似这样记一次线上问题 → 事务去哪了

    如果目标对象中没有被 @transactional 修饰的方法,在代理类中是怎样的了 ? 既然没有被 @transactional ,说明不需要事务增强处理嘛,那就直调呗记一次线上问题 → 事务去哪了

    回到我们的案例,代理对象与被代理对象之间的调用如下

记一次线上问题 → 事务去哪了

    可以看出来,目标对象新增的方法 tranmisscredit deposit(string loginname, bigdecimal amount, int integration) 在代理对象内是没有织入事务的,也就是默认的自动提交,那么异常抛出之前的数据库操作都是自动提交的,不会因后面的异常而回滚

    其实不是事务丢失了,而是根本就不在一个事务中

  再次校验

    不只是 spring 事务,很多的 aop 也都一样,代码中直接操作的往往不是目标对象,而是目标对象的代理,通过代理对象来间接操作目标对象,而在代理对象中我们可以做一些前置或者后置的增强处理,不信 ? 我们再次找焦点访谈

    打个断点,看看就知道了记一次线上问题 → 事务去哪了

    注入到 transactionmisstest 的确实是代理对象

    我们在 tranmisscredit deposit(string loginname, bigdecimal amount) 上打个断点,然后两种方式各调用一次,来看看调用链有什么不一样

    以 depositservice.deposit("zhangsan", new bigdecimal(100)); 方式调用时

记一次线上问题 → 事务去哪了

    此时调用链中有事务拦截器,有事务的调用链

    以 depositservice.deposit("zhangsan", new bigdecimal(100), 10) 方式调用时

记一次线上问题 → 事务去哪了

    此时调用链中没有事务拦截器,没有事务的调用链

    是不是很明了了,so easy

总结

  1、正常上线流程

    线上问题 → 问题定位 → 问题复现 → 问题修复 → 转测试 → 测试通过升线上

    而不是像文中说的那么轻描淡写

  2、事务去哪了

    spring 事务的底层实现就是动态代理,是通过代理的方式对目标对象做前后的增强处理,前置开启事务、后置提交(回滚)事务;

    增强处理在代理对象内,而不是在目标对象内,若目标对象的方法没有被 @transactional 修饰,则在代理对象的代理方法内不会有关于事务的增强处理,而是直接调用目标对象的方法,那么后续的数据库操作就不是在一个事务中了

    不是事务消失了,而是不在同一个事务了