导读:​一段被try-catch包裹后的代码在产线稳定运行了200天后忽然发生了异常,而这个异常竟然导致了产线事务回滚。这期间究竟发生了什么?日常在项目过程中该如何避免事务异常?就在这个时候,老板拿着《XX公司关于三十岁员工优化通知》走了过来......

# 01

产线部分数据丢失了,因为一个蹊跷的事务回滚。而造成事务回滚的,竟然是一段被try-cath包裹后的代码,一段已经在产线稳定运行了200天的代码,稳定到我们已经把它遗忘了。谁也没想到的是,它竟然以这样一种方式重新回到了我们的视野,宣告着它的存在!

小九九是一个永远19岁的程序员,和所有程序员一样地阳光、帅气(这句话不管你信不信,反正我自己也不信。为了能够开始今天的文章,就这么瞎编吧,总比以“一个没有头发的程序员”开头的好)。当他告诉我一段try-catch的代码造成产线事务回滚后,我温柔、耐心地对他说:“滚一边去,没看我正忙着吗?”,然后他给我甩出了一段代码,用猥琐又真诚的眼睛告诉我,他说的是真的。

02

我们来看一下这段导致了产线事务回滚的代码,类似于下面这样的:

@Transactional
public void main() {
// 假设有多个user的操作,需要事务控制
methodA(); try {
orderService.methodB();
} catch (Exception e) {
// order失败了不能影响该方法,不回滚。
// 异常处理,略
}
userOtherProcess();
}

methodA方法需要事务控制,methodB方法不管遇到什么异常都不能影响A事务,所以加了try-catch。可能有的人和我的第一反应一样,是不是最后的userOtherProcess方法执行异常造成了methodA的事务回滚?小九九告诉我真的是因为methodB,这段代码当初经过严格的测试,而且已经200天没人碰过了。也可能已经有人猜出了问题的原因了,这里先卖个关子,因为这件事情里,最重要的是这个坑是如何一步步产生的。

为了更形象地描述这个事情我画一个图,红色背景表示该方法是有事务控制的,白色背景表示该方法没有事务

一开始的时候,正如大家所看到的代码,methodA方法有事务,methodB无事务且被try-catch包裹了,运行得很完美。过了一段时间后来到了阶段二,因为一些需求变更新增了methodC,该业务也依赖了methodB,依然很完美地上线了。

过了一段时间来到了阶段3,依赖methodC相关业务再次发生了变更,需要在methodB里增加一些逻辑且需要事务控制,经过评估确实对methodA没有影响,于是经过充分测试后再次完美地上线了,然而隐藏的炸弹就在这个时候埋下了。小伙伴们这个时候应该已经猜到原因了,是的,你猜的没错。某一天methodA调用methodBmethodB发生了异常,由于是继承性事务,虽然methodB发生了异常被try-catch了,依然造成了methodA事务回滚。还没有理解的小伙伴,可以看下面这张图:

我们可以把事务控制机制理解为上图这样一个红色的长长的房间,这个房间是有人看守的,他负责事务的开始、提交,还有一项重要的任务就是监控异常,一旦发现RuntimeException异常直接回滚整个事务,我们给他一个title,称之为“监事”吧。再来看阶段三和一开始的代码,方法的开头有一个@Transactional注解,于是他打开了这个红色房间的门,把methodA放了进去,接着methodB过来了,也开启了事务--继承性事务,于是监事把methodB也安排到了这个房间,methodB虽然发生了异常且被try-catch包裹,但逃不过监事的火眼金睛,于是他按下了事务回滚的按钮。

这样理解了之后,我们再来简单看一下源码:

org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processRollback(AbstractPlatformTransactionManager.java:873)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:710)
at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:534)

根据异常提示,可以看到错误发生在AbstractPlatformTransactionManager的873行processRollback方法,通过Find Usages找到调用方commit方法,显然这是一段事务提交的逻辑。

@Override
public final void commit(TransactionStatus status) throws TransactionException {
// 为便于阅读,删除部分代码
......
if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
// 为便于阅读,删除部分代码
processRollback(defStatus, true);
return;
}
processCommit(defStatus);
}
  • shouldCommitOnGlobalRollbackOnly:默认实现是false,意思是如果发现事务被标记全局回滚并且该标记不需要提交事务的话,那么则进行回滚。
  • defStatus.isGlobalRollbackOnly():判断是否是读取DefaultTransactionStatus中transaction对象的ConnectionHolder的rollbackOnly标志位

继续往上追溯,来到TransactionAspectSupport.invokeWithinTransaction方法:

@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {
// 为便于阅读,删除部分代码
......
// 如果是声明式事务
if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
// Standard transaction demarcation with getTransaction and commit/rollback calls.
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification); Object retVal;
try {
// This is an around advice: Invoke the next interceptor in the chain.
// This will normally result in a target object being invoked.
// 执行事务方法
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// 捕获异常,并将会把事务设置为Rollback回滚状态。
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}
// 提交事务
commitTransactionAfterReturning(txInfo);
return retVal;
} else {
// 声明式事务,略
}
}

整个执行过程参见注释说明,其它源码就不罗列了。Spring捕获异常后,正如我们所猜测的,事务将会被设置全局rollback,而最外层的事务方法执行commit操作,这时由于事务状态为rollback,Spring认为不应该commit提交事务,而应该回滚事务,所以抛出rollback-only异常。

03

还有一个比较典型的事务问题就是:在同一个类中,mehtodA没有事务,mehtodB开启了(声明式)事务,此时mehtodA调用mehtodB时事务是不生效

如上面这张图所示,我们还是把AOP想像成一个长方形的房间,由于mehtodA没有事务,这个房间已经被标志为没有事务无人值守了,mehtodB虽然标记了事务,但很显然是不生效的。

接下来我们重新回顾一下事务的几种配置:

  • REQUIRED:支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。
  • REQUIRES_NEW:新建事务,如果当前存在事务,把当前事务挂起。
  • SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行。
  • MANDATORY:支持当前事务,如果当前没有事务,就抛出异常。
  • NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
  • NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
  • NESTED:支持当前事务,如果当前事务存在,则执行一个嵌套事务,如果当前没有事务,就新建一个事务。

这方面的文章很多,这里就不做描述了。

04

事务问题本身是比较难通过测试发现的,我们再来聊一聊项目过程中如何防止事务问题的发生。比如笔者之前曾负责过支付及资金处理相关系统,产品的单笔交易额比较大,每笔至少1万+,正常10万+,很多时候一笔支付就是300万,所以容不得出现一笔资金差错。好在我们资金交易从0做到了3000亿,依然资金0差错。针对可能的事务问题,我们采取的措施有:

  1. 通过开发规范、产线坑集等文档、培训等让开发人员对事务有足够的了解、敏感度。
  2. 系统设计时,对于关键的业务场景需要写明是否启用了事务,哪些方法包裹在一个事务中,并进行评审。
  3. 代码Review环节有很多专项Review,比如资金review、多线程Review等等,也有一项专门的事务Review:需不需要加事务?事务配置是否正确?异常是否处理等。
  4. 开发人员构造事务异常场景进行自测、交叉验证。
  5. 测试团队参与系统设计评审,并进行事务相关测试。比如通过防火墙阻断请求、手动锁表等方式来模拟可能的事务异常。

笔者在之前一家公司还有一种做法就是通过开发规范约束:所有事务的方法全部以tx开头。比如methodB方法需要开启事务,则新增一个txMethodB方法,在该方法中调用methodB。通过这种方式完全可以避免上面问题的发生,但很显然这种方式相当地“丑陋”。

05

正和小九九聊着事务问题,老板手里拿着几张A4纸走了过来。

作为公司唯一的30岁程序员,我提高了声音对小九九说:你有没有发现@Transactional中还有一个配置项readOnly,如果需要使用这个参数,必须启动一个事务。但如果是读取数据,根本就不需要事务啊?为什么会有这么一个自相矛盾的配置项呢?小九九一脸茫然地摇了摇头。

老板冲我点了点头,转身回到了办公室,坐下思考了一会,然后把手里的A4纸《XX公司​关于三十岁员工优化通知》放到了抽屉一叠资料的最下面,接着又抽出来放到了资料的中间。

看来我的程序生涯,又可以持续一段时间了!

推荐阅读

Redis 6.0 新特性-多线程连环13问!

报告老板,微服务高可用神器已祭出,您花巨资营销的高流量来了没?

我成功攻击了Tomcat服务器,大佬们的反应亮了

公众号:码大叔

资深程序员、架构师技术社区。

微服务 | 大数据 | 架构设计 | 技术管理。

Try-Catch包裹的代码异常后,竟然导致了产线事务回滚!的更多相关文章

  1. (转)spring异常抛出触发事务回滚策略

    背景:在面试时候问到事务方法在调用过程中出现异常,是否会传递的问题,平时接触的比较少,有些懵逼. spring异常抛出触发事务回滚策略 Spring.EJB的声明式事务默认情况下都是在抛出unchec ...

  2. Spring异常抛出触发事务回滚

    Spring.EJB的声明式事务默认情况下都是在抛出unchecked exception后才会触发事务的回滚 /** * 如果在spring事务配置中不为切入点(如这里的切入点可以定义成test*) ...

  3. Spring事务回滚和异常类

    1.异常的一些基本知识 异常的架构 异常的继承结构:Throwable为基类,Error和Exception继承Throwable.Error和RuntimeException及其子类成为未检查异常( ...

  4. 为什么mysql事务回滚后, 自增ID依然自增

    事务回滚后,自增ID仍然增加,回滚后,自增ID仍然增加.比如当前ID是7,插入一条数据后,又回滚了.然后你再插入一条数据,此时插入成功,这时候你的ID不是8,而是9.因为虽然你之前插入回滚,但是ID还 ...

  5. ThinkPHP实现事务回滚示例代码(附加:PDO的事务处理)

    ThinkPHP的事务回滚示例如下: $m=D('YourModel');//或者是M(); $m2=D('YouModel2'); $m->startTrans();//在第一个模型里启用就可 ...

  6. ThinkPHP 实现数据库事务回滚示例代码

    ThinkPHP提供了数据库的事务支持,如果要在应用逻辑中使用事务,可以参考下面的方法:   启动事务: $User->startTrans(); 提交事务: $User->commit( ...

  7. 哪些异常是RuntimeException?Sql异常属于RuntimeException吗?Spring下SQL异常事务回滚

    一,为什么框架中根本没有对Exception的一般子类进行回滚配置,异常发生时,事务都进行了回滚 ,说好的只会对RuntimeException(Unchecked 非受检异常)回滚呢? 此时,我们就 ...

  8. 事务回滚后,自增ID仍然增加

    回滚后,自增ID仍然增加. 比如当前ID是7,插入一条数据后,又回滚了.然后你再插入一条数据,此时插入成功,这时候你的ID不是8,而是9.因为虽然你之前插入回滚,但是ID还是自增了. 如果你认为自增I ...

  9. Spring 事务回滚代码

    在事务中实行的方法:org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransacti ...

随机推荐

  1. 三个步骤就能让你轻松掌握Python爬虫

    前言 文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理. 作者:NicePython PS:如有需要Python学习资料的小伙伴可以加 ...

  2. 哈密顿绕行世界问题 HDU2181

    题目大意都比较简单,用vector存一下图,然后爆搜就可以了. #include<bits/stdc++.h> using namespace std; ; vector<]; bo ...

  3. Shell脚本日志关键字监控+告警

    最近小张的爬虫程序越来越多,可当爬虫程序报错,不能及时的发现,从而造成某些重要信息不能及时获取的问题,更有甚者,遭到领导的批评.于是就在想有没有一种方法,当爬取信息报错的时候,可以通过邮件或者短信的方 ...

  4. 数据挖掘入门系列教程(十)之k-means算法

    简介 这一次我们来讲一下比较轻松简单的数据挖掘的算法--K-Means算法.K-Means算法是一种无监督的聚类算法.什么叫无监督呢?就是对于训练集的数据,在训练的过程中,并没有告诉训练算法某一个数据 ...

  5. C#反射(二)

    长时间没有回顾反射知识了,今天就讲解一下反射的一般第二个用法. 二.对方法,属性等的反射 首先需要写一个测试类,生成.exe或.dll文件. class Test {   public Test()/ ...

  6. hadoop 伪分布配置

    配置 Hadoop 伪分布式 任务配置说明: VMware 15 Centos 6.5 java -jdk 1.8 hadoop-2.6.0-cdh5.14.0.tar.gz 第一步 自行安装虚拟机 ...

  7. 最通俗易懂的Redis发布订阅及代码实战

    发布订阅简介 除了使用List实现简单的消息队列功能以外,Redis还提供了发布订阅的消息机制.在这种机制下,消息发布者向指定频道(channel)发布消息,消息订阅者可以收到指定频道的消息,同一个频 ...

  8. Java 解析 xml 常见的4中方式:DOM SAX JDOM DOM4J

    Java 四种解析 XML 的特点 1.DOM 解析: 形成了树结构,有助于更好的理解.掌握,且代码容易编写. 解析过程中,树结构保存在内存中,方便修改. 2.SAX 解析: 采用事件驱动模式,对内存 ...

  9. 一些软件的 Basic Auth 行为

    一个 WBEM 在2003年的bug I'm trying to access the WBEM service of the CIMOM on the ESX Server 3i and all m ...

  10. c语言解一元二次方程

    C语言解一元二次方程,输入系数a,b,c; #include <stdio.h> #include <math.h> int main(int argc, char *argv ...