当前位置: 首页 > 科技观察

一段包含在Try-Catch中的代码差点让我失去工作!

时间:2023-03-14 23:23:02 科技观察

一段try-catch包裹的代码在产线稳定运行200天后突然出现异常,该异常导致产线事务回滚。图片来自Pexels。这期间发生了什么?日常项目过程中如何避免交易异常?就在这时,老大过来了《XX 公司关于三十岁员工优化通知》……01生产线的部分数据丢失了,因为一个奇怪的事务回滚了。导致事务回滚的竟然是try-cath包裹的一段代码,一段在产线上稳定运行了200天的代码,稳定到我们早忘记了。谁也没想到,它以这样的方式回到了我们的视野,宣告着它的存在!小九九是永远的19岁程序员,和所有的程序员一样阳光帅气(这句话信不信由你,反正我自己也不信。今天的文章能开篇就这么编,总比从“一个没有头发的程序员”开始)。当他告诉我一段try-catch代码导致生产线回滚时,我温柔耐心地对他说:“走开,你没看到我很忙吗?”,然后他扔给我一个一段代码,用猥琐而真诚的眼神告诉我,他说的是真的。02我们来看这段导致产线事务回滚的代码,类似如下:@Transactionalpublicvoidmain(){//假设有多个用户操作,需要事务控制方法A();try{orderService.methodB();}catch(Exceptione){//订单失败不能影响这个方法,不回滚。//异常处理,省略}userOtherProcess();}methodA方法需要事务控制,methodB方法无论遇到什么异常都不能影响A事务,所以加入try-catch。有些人第一反应可能和我一样。上次userOtherProcess方法执行异常是不是导致methodA的事务回滚了?小九九告诉我,真的是因为方法B。这段代码一开始是经过严格测试的,到现在已经200天没人碰了。也有可能已经有人猜到了问题的原因,所以还是先保密吧,因为在这件事情上,最重要的是这个坑是怎么一步步产生的。为了更形象地描述这件事,我画了一张图。红色背景表示该方法有事务控制,白色背景表示该方法没有事务:一开始,在代码中可以看到,methodA方法有事务,而methodB没有事务,并被try-包裹抓住,它完美地工作。过了一段时间,就到了第二阶段。由于一些需求变化,添加了methodC。业务也依赖了methodB,依然完美上线。过了一段时间,到了阶段3,依赖于methodC的相关业务又变了,需要在methodB中增加一些逻辑,需要进行事务控制。经过评估,并没有影响到methodA,所以在全测后完美上线,不过此时埋下了隐弹。小伙伴们此时应该已经猜到原因了,是的,你猜对了。有一天,methodA调用methodB时,methodB出现异常。因为是继承事务,虽然methodB发生了异常,被try-catched了,但还是导致methodA事务回滚。还不了解的朋友,可以看下图:我们可以把事务控制机制理解成上图这样一个长长的红色房间。这个房间有人看守。他负责事务的启动和提交,还有一个重要的任务就是异常监控。一旦发现RuntimeException,直接回滚整个事务,我们给他起个称呼,叫“Supervisor”。再次查看第三阶段和第一阶段开头的代码。方法开头有个@Transactional注解,于是他打开红色房间的门,把methodA放了进去。然后methodB过来了,开始业务——继承业务,于是主管也把methodB安排在这个房间里。虽然methodB发生了异常,被try-catch包裹了,但是还是逃不过主管的眼睛,于是按下了事务回滚按钮。这样理解了之后,我们再来简单看一下源码:org.springframework.transaction.UnexpectedRollbackException:Transactionrolledbackbecauseithasbeenmarkedasrollback-onlyatorg.springframework.transaction.support.AbstractPlatformTransactionManager.processRollback(AbstractPlatformTransactionManager.java:873)atorg.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:710)atatorg.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:534)根据异常提示可以看到错误发生在AbstractPlatformTransactionManager的873行processRollback方法中。通过FindUsages找到调用者的提交方法,显然这是一个事务提交的逻辑。@Overridepublicfinalvoidcommit(TransactionStatusstatus)throwsTransactionException{//为了可读性删除一些代码......if(!shouldCommitOnGlobalRollbackOnly()&&defStatus.isGlobalRollbackOnly()){//为了可读性删除一些代码processRollback(defStatus,true);return;}processCommit(defStatus);}shouldCommitOnGlobalRollbackOnly:默认实现为false,意思是如果发现事务被标记为全局回滚并且标记不需要提交事务,那么就会回滚。defStatus.isGlobalRollbackOnly():判断是否读取DefaultTransactionStatus中事务对象的ConnectionHolder的rollbackOnly标志。继续回溯到TransactionAspectSupport.invokeWithinTransaction方法:@NullableprotectedObjectinvokeWithinTransaction(Methodmethod,@NullableClasstargetClass,finalInvocationCallbackinvocation)throwsThrowable{//删除部分代码方便阅读...//如果是声明式事务if(txAttr==null||!(tminstanceofCallbackPreferringPlatformTransactionManager)){//StandardtransactiondemarcationwithgetTransactionandcommit/rollbackcalls.TransactionInfotxInfo=createTransactionIfNecessary(tm,txAttr,joinpointIdentification);ObjectretVal;try{//Thisisanaroundadvice:Invokethenextinterceptorinthechain.//Thiswillnormallyresultinatargetobjectbeinginvoked.//执行事务方法retVal=invocation.proceedWithInvocation();}catch(Throwableex){//捕获异常,并将事务设置为回滚状态。completeTransactionAfterThrowing(txInfo,ex);throwex;}finally{cleanupTransactionInfo(txInfo);}//提交事务commitTransactionAfterReturning(txInfo);returnretVal;}else{//声明式事务,简称}}整个执行过程参考注释,其他源码我就不一一列举了。Spring捕捉到异常后,正如我们猜测的那样,事务将被设置为全局回滚。最外层的事务方法执行提交操作。此时由于事务状态为rollback,Spring认为事务不应该提交,而应该回滚,所以抛出rollback-only异常。03还有一个典型的事务问题:在同一个类中,mehtodA没有事务,mehtodB开启了(声明式)事务。此时mehtodA调用mehtodB时,事务不会生效:如上图所示,我们还是把AOP想象成一个长方形的房间。由于mehtodA没有交易,这个房间已经被标记为没有交易无人值守,mehtodB虽然标记了交易,但是显然是无效的。接下来我们再回顾一下事务的几个配置:REQUIRED:支持当前事务,如果没有当前事务,则新建一个事务。这是最常见的选择。REQUIRES_NEW:创建一个新事务,如果有当前事务,则暂停当前事务。SUPPORTS:支持当前事务,如果没有当前事务,则以非事务方式执行。MANDATORY:支持当前事务,如果没有当前事务则抛出异常。NEVER:以非事务方式执行,如果有当前事务则抛出异常。NOT_SUPPORTED:以非事务方式执行操作。如果有当前事务,则暂停当前事务。NESTED:支持当前事务。如果当前事务存在,则执行嵌套事务。如果没有当前事务,则创建一个新事务。这方面的文章很多,这里就不赘述了。04交易问题本身很难通过测试发现。让我们谈谈如何在项目过程中防止交易问题。比如笔者之前一直负责支付和资金处理相关系统。产品单笔交易金额较大,每笔交易至少10000+,正常金额100000+。很多时候单笔付款就是300万,所以一笔钱是容不得有差错的。好在我们的资金交易从0到3000亿,资金还是0错误。针对业务可能出现的问题,我们采取了以下措施:通过开发规范、产线坑收集等文档、培训等方式,让开发人员对业务有足够的理解和敏感度。在设计系统的时候,对于关键的业务场景,需要说明是否启用了事务,一个事务中包装了哪些方法,并进行审核。代码审核环节有很多专项审核,比如基金审核,多线程审核等,还有一个专项交易审核:是否需要添加交易?交易配置是否正确?是否处理异常等。开发者构建交易异常场景进行自测和交叉验证。测试团队参与系统设计评审,进行交易相关测试。比如通过防火墙阻断请求,手动锁表等,模拟可能出现的事务异常。笔者在之前公司工作的另一种方式是通过开发规范约束:所有交易方式都以tx开头。比如methodB方法需要启动一个事务,添加一个txMethodB方法,在这个方法中调用methodB。这样一来,就可以完全避免上面的问题,但是显然这个方法还是比较“丑陋”的。05正和小九九谈事情,老大拿着A4纸走了过来。作为公司里唯一一个30岁的程序员,我提高嗓门对小酒酒说:你有没有注意到@Transactional中也有一个readOnly的配置项。如果你需要使用这个参数,你必须启动一个事务。但是如果是读数据,就完全不需要事务了?为什么会有这么矛盾的配置项呢?萧九九茫然的摇了摇头。老板冲我点点头,转身回到办公室,坐下来想了想,然后把手里的A4纸《XX 公司关于三十岁员工优化通知》放在抽屉里那一摞材料的最下面,然后拉了出来并将其放在材料的中间。看来我的编程生涯还能再继续一段时间!作者:剑圣编辑:陶家龙来源:转载自微信公众号码大叔(ID:ma_dashu)