@Transactional 事务失效排查,try-catch 吞异常导致回滚失败(附源码分析)
老炮踩坑录 · F09 · 翻车现场系列·基于「企业融合评估平台」真实源码复盘 Transactional 被 try-catch 架空的经典翻车·关键词Transactional 失效 · 事务不回滚 · try-catch 吞异常 · rollbackFor 欢迎阅读个人主页知守观我的专栏老炮踩坑录当前内容Transactional 事务失效文章目录引子案发现场一个删除失败的接口为什么没回滚一句话讲透这不是个例同一个项目同一个坑第二处saveSelectPaper这个项目的 Transactional 问题全景正确写法三条铁律铁律一Transactional 放 Service 层不要放 Controller铁律二事务方法里异常必须往外抛铁律三永远写 rollbackFor Exception.class自查清单老炮点评引子如果一个 Java 开发者只能记住一条 Spring 注解大部分人会选Transactional。加上去事务就有了数据一致性也稳了。真的稳了吗2022年的老项目里有个Service接口加了Transactional(rollbackFor Exception.class)——你看连rollbackFor都写了多规范。但它的事务一次都没成功回滚过。不是 Spring 的锅是代码里一个 try-catch把异常吞得干干净净。Transactional以为一切正常事务正常提交。但数据库里的数据已经半截 delete掉了、半截还存在——数据不一致了。今天这篇我从项目里一个真实的 “删除失败” 接口出发拆透这个 90% 的 Java 开发者都踩过的坑。这个坑不是个例它藏在项目的很多角落。整个排查和复盘的过程我整理成了下面这张图。你可以对照看看你项目里有没有类似的隐患。案发现场一个删除失败的接口这个接口叫deleteReport功能是删除诊断报告——用户想换一套诊断模型系统需要先删掉旧数据再重建。删除涉及 5 张关联表必须原子操作要么全删要么全不删所以开发者加了Transactional。看起来考虑得很周到来看代码// ApplyInfoController.java L353-381GetMapping(/deleteReport)Transactional(rollbackForException.class)publicObjectdeleteReport(HttpServletRequestrequest)throwsException{StringenterpriseIdSessionCacheUtils.getEnterpriseid();// ...前置校验...try{applyInfoService.deleteReport(enterpriseId);// ① 删申报信息diaService.delete(enterpriseId);// ② 删诊断试卷maService.delete(enterpriseId);// ③ 删成熟度考卷caService.delete(enterpriseId);// ④ 删能力试卷otherService.delete(enterpriseId);// ⑤ 删其他试卷applyInfoService.updateApplyId(enterpriseId);// ⑥ 清空关联ID}catch(Exceptione){log.error(删除失败:{} ,e);// ⑦ 记录错误日志returnResultInfo.setResultInfo(true,500,error,删除失败,重新尝试);// ⑧ 返回响应}returnResultInfo.setResultInfo(true,200,ok,删除成功);}你看出问题了吗如果没看出来我把这段代码翻译成人话①②③ 三步删除执行成功数据库里三张表的数据已经没了如果④ 第四步抛异常了——比如数据库连接超时catch 接住了异常记录日志方法 return 了一个删除失败的响应正常结束Spring 事务管理器一看方法正常返回没有异常提交事务结果前三张表的数据删了后两张表的数据还在。说好的要么全删要么全不删呢没有兑现呀为什么没回滚一句话讲透Spring 的Transactional注解靠的是AOP 代理。原理很简单关键点代理只能看到方法有没有抛异常。如果你的方法内部用 try-catch 把异常吃掉了然后正常 return——代理看到的是方法正常返回于是提交事务。打个比方Transactional就像大楼的消防系统——它能感知火灾异常然后自动喷水回滚。但你的 try-catch 相当于有人在火灾报警器响之前把报警器关了。消防队以为一切正常但楼已经烧了一半。Spring 事务的核心逻辑在TransactionInterceptor这个类里。它的工作流程极其简单核心代码逻辑大致如下// TransactionAspectSupport.java 核心逻辑try{// 执行业务方法retValinvocation.proceed();}catch(Throwableex){// 一旦捕获到异常立刻标记回滚completeTransactionAfterThrowing(txInfo,ex);throwex;}// 如果方法正常返回提交事务commitTransactionAfterReturning(txInfo);问题就出在这里因为你的代码在业务方法内部自己try-catch了异常没有“冒泡”到invocation.proceed()外面。Spring 以为业务方法“正常返回”了于是正常的地执行了commitTransactionAfterReturning方法提交事务。一句话总结Spring 监听的是“异常抛出”而不是“数据异常”。这就是deleteReport的问题Transactional配了但 try-catch 把异常吞了Spring事务永远看不到异常永远不回滚。这不是个例同一个项目同一个坑我以为只有这一个接口这么写的搜了一下发现至少有两处。第二处saveSelectPaper// ApplyInfoController.java L176-192GetMapping(/createSelectType)Transactional(rollbackForException.class)publicResultcreateSelectType(Integerpaperid){// ...try{returnapplyInfoService.addApplyInfo(paperid);}catch(Exceptione){// 吞了异常e.printStackTrace();// 记录日志returnresult.fail(ResultEnum.FAIL);// 正常返回}}这个方法中调用了ApplyInfoServiceImpl.addApplyInfo()而addApplyInfo里面又做了什么// ApplyInfoServiceImpl.java L232-295publicResultaddApplyInfo(Integer...papers)throwsException{// ① 创建申报信息mapper.addApplyInfo(info);// ② 更新企业基本信息表的 applyIdmapper.updateApplyId(applyid,enterpriseId,userAccount);// ③ 循环创建试卷4 种模型每张都要建for(Integerstr:papers){switch(str){case0:diagnosisMapper.save(...);break;case1:maMapper.save(...);break;case2:caMapper.save(...);break;case3:cloudMapper.save(...);cloudMapper.saveExtend(...);break;}}// ④ 初始化申报附件表applyFileMapper.insert(applyFile);returnresult.success(retData);}这个方法里做了4 步写操作创建申报、更新企业表、建 4 张试卷、初始化附件表。任何一步失败都应该整体回滚。它确实加了Transactional(rollbackFor Exception.class)而且也抛出了异常。但调用它的 Controller 用 try-catch 把异常吞了。异常传不到 Service 层的事务边界Spring事务以为一切正常提交。如果第 ③ 步建第 3 张试卷时数据库报错——申报信息创建了、企业表更新了、前 2 张试卷建好了——这些全部提交不会回滚。这个项目的 Transactional 问题全景我全局扫了一遍整个项目发现事务管理的问题远不止 try-catch 吞异常而是三个层次全踩了第三个问题值得单独说说。项目里有 10 个 Service 类用了裸的Transactional没写rollbackForTransactional// 没有 rollbackForpublicclassEnterpriseRegistServiceImplimplementsEnterpriseRegistService{...}Transactional// 没有 rollbackForpublicclassDagInfoServiceImplimplementsDiagnosisInfoService{...}Transactional// 没有 rollbackForpublicclassHwFileServiceImplextendsAbstractFileService{...}// ... 还有 7 个全是同样的写法Spring 的默认行为只对RuntimeException和Error回滚对受检异常checked exception不回滚。也就是说如果EnterpriseRegistServiceImpl里的方法抛了IOException或SQLException——事务不回滚正常提交。你以为加了Transactional就万事大吉但 Spring 默认只保护了一半。更离谱的是HwFileServiceImpl等——它们把Transactional加在了类级别。这意味着所有 public 方法都被事务包裹包括那些根本不需要事务的查询方法。每个 SELECT 都开一个事务白白浪费数据库连接资源。正确写法三条铁律铁律一Transactional 放 Service 层不要放 Controller// 错误Controller 层加事务Transactional(rollbackForException.class)publicObjectdeleteReport(...){...}// 正确Controller 只调 Service事务由 Service 管publicObjectdeleteReport(...){returnreportService.deleteReport(enterpriseId);}Controller 的职责是接请求、返响应。事务是业务逻辑的事交给 Service。这也符合单一职责设计原则。铁律二事务方法里异常必须往外抛// 错误try-catch 吞异常事务永远提交try{service.deleteA();service.deleteB();}catch(Exceptione){e.printStackTrace();// 吞了异常异常止步于此spring事务看不到了return删除失败;}// 正确异常抛出去让 Transactional 看到try{service.deleteA();service.deleteB();}catch(Exceptione){log.error(删除失败: enterpriseId{},enterpriseId,e);thrownewBusinessException(删除失败请稍后重试,e);// 抛出去}如果你一定要在 catch 里做点事情比如记日志、发告警做完之后必须 throw。可以抛原异常也可以抛新异常但不能 return。但如果在Controller层捕获了异常事务会生效吗先说结论会的。看个例子。如果事务边界在 Service 方法不在 Controller.Service 里写Transactional(rollbackForException.class)publicResultaddApplyInfo(Integer...papers)throwsException{//抛出异常// 创建申报信息mapper.addApplyInfo(info);....}Controller 里写try{returnapplyInfoService.addApplyInfo(paperid);}catch(Exceptione){log.error(删除失败: paperid{},paperid,e);returnresult.fail(ResultEnum.FAIL);}这个try-catch是在事务代理之外。调用顺序大致是Controller - Spring 事务代理 - 开启事务 - 执行 addApplyInfo() - 方法抛出异常 - 事务代理捕获异常 - 根据 rollbackFor 执行 rollback - 重新抛出异常 - Controller 的 catch 捕获到异常 - 返回 result.fail所以异常先被 Spring 事务拦截器感知并回滚然后才被 Controller 捕获。Controller 即使把异常“吞掉”了也不会影响已经发生的回滚也不会让事务提交。记住Service 里的 catch 里可以做事但做完必须让异常继续传播。Transactional只认异常不认 return。铁律三永远写 rollbackFor Exception.class// 裸 Transactional受检异常不回滚TransactionalpublicvoiddoSomething(){...}// 明确指定回滚条件Transactional(rollbackForException.class)publicvoiddoSomething(){...}这一条没有例外。不管你觉不觉得我的方法不会抛受检异常——写上它零成本但关键时刻能救命。终极方案使用TransactionTemplate手动控制。如果你觉得声明式事务太容易踩坑那么最安全的做法就是显式编程。在代码里直接注入TransactionTemplate在execute方法里写业务逻辑一旦有异常直接抛出绝对可控。适用场景逻辑极其复杂、有大量中间状态处理的逻辑用声明式事务难以控制边界时。有个细节我没完全确认TransactionTemplate 在嵌套事务比如外层有事务、内层也手动开事务时的行为和声明式事务到底有没有差异我本地验证的是简单场景嵌套没试过。有踩过坑的兄弟评论区指点一下。自查清单在项目里搜到三个东西这三条随便哪一条搜出结果你的项目就存在事务假死不回滚的风险。自查打卡搜一下你的项目grep -r “Transactional” src/ | wc -l再看看有多少没带rollbackFor。两个数字打在评论区我看看谁的项目事务最“规范”。老炮补充除了本文讲的三种还有两个经典的事务失效场景同类内部调用同一个Service类里方法A加事务调用了方法B加事务因为不走代理事务会失效。方法不是publicSpring AOP无法代理 private、protected 或包级私有方法。这两个坑以后在别的文章里我会单独拆解。老炮点评这个坑的本质是形式正确和实际有效之间的幻觉。Transactional加了 → 形式正确rollbackFor写了 → 形式正确try-catch 兜住了 → 看起来健壮但组合在一起事务保护就静默失效了。最可怕的是这种失效不会报错、不会告警、不会在测试环境暴露——它只会在生产环境某个并发高峰悄悄留下半截数据。我见过太多团队花大力气设计分布式事务、搞最终一致性方案但最基本的Transactional try-catch这关都没过。事务管理的第一课不是分布式是别让 try-catch 把异常吞了。事务回滚的关键异常必须让事务拦截器感知到。要么抛出事务方法外要么内层事务已标记 rollback-only要么手动 setRollbackOnly()。吞掉异常且无内层标记事务就不会回滚。最后问一句你们项目里catch到异常是抛出去还是吞掉有没有人真被事务不回滚坑过评论区聊聊我看看有多少项目在用“形式正确”的事务。下期预告《Spring Boot 2.1.0 已经“死”了你的项目还在裸奔吗》Spring事务的坑填完了下期把眼光从代码抬到框架——pom.xml里那个 Spring Boot 2.1.0已经停维护了。没有安全补丁、没有社区支持升级又怕踩坑。老项目用停维护的版本到底有多危险下期见。我是老炮18年Java老兵仍在一线。关注「Java老炮踩坑录」不错过每一篇真实案例少踩坑。