【JavaEE32-后端部分】Spring事务进阶:@Transactional三大利器,把事务玩明白【AI辅助理解】

张开发
2026/4/6 11:15:14 15 分钟阅读

分享文章

【JavaEE32-后端部分】Spring事务进阶:@Transactional三大利器,把事务玩明白【AI辅助理解】
老铁们上回咱们学会了用Transactional这个注解搞定事务再也不用自己写那一坨开启、提交、回滚的代码了爽不爽但是咱们真的会用Transactional吗为啥我抛了个IOException读作“ai-o-exception”就是输入输出异常比如文件找不到、网络断了事务它就不回滚呢啥叫脏读啥叫不可重复读啥叫幻读隔离级别又是个啥玩意儿一个事务方法调另一个事务方法它们到底是“穿一条裤子”还是“各过各的”别慌今天这篇咱们就把Transactional身上的三个核心属性——rollbackFor读作“肉贝壳佛”意思是“回滚条件”、isolation读作“爱骚雷神”意思是“隔离级别”、propagation读作“普罗帕给神”意思是“传播机制”向大家介绍清楚声明本文AI辅助理解本人整理阅读的老铁别忘了重点看 第五点 小贴士 帮你避坑哦。开场一个让哥们儿社死的翻车现场先给老铁们讲个趣事。有一哥们儿刚入职一家公司写支付系统。有一天他写了个退款功能逻辑大概是这样的从用户账户里把钱扣回来退款嘛记录一条退款日志调用第三方接口通知商家“钱退了啊”他觉得万无一失给方法上加了个Transactional。结果测试的时候第三方接口挂了抛了个IOException就是网络异常。他心想“没事事务会回滚钱不会少。”结果一查数据库——钱扣了日志也记了但第三方通知失败了。用户炸了老板也炸了。为啥因为IOException这玩意儿在 Java 里属于非运行时异常也叫检查型异常。Transactional默认只回滚运行时异常比如除以零、空指针对这种“外部问题”它不管。这哥们儿当场社死。所以今天这篇咱们就从rollbackFor开始把事务的底裤都扒干净。一、rollbackFor回滚条件—— 让“所有异常”都回滚1.1 默认只回滚运行时异常这是个坑咱先搞清楚Java 里的异常分两类。运行时异常RuntimeException编译的时候不强制你处理。比如除以零10 / 0、数组越界、空指针。这种通常是代码写错了。非运行时异常也叫检查型异常Checked Exception编译的时候必须处理要么try-catch要么throws。比如文件找不到、网络断了、数据库连不上IOException、SQLException。Transactional默认的规则是只对运行时异常和Error回滚对非运行时异常不回滚。Spring 为啥这么设计它觉得非运行时异常通常是外部环境问题网络、IO、数据库不一定代表你的业务数据错了所以它就不帮你回滚。来咱们验证一下。写一个注册用户的方法注册成功后手动抛一个IOException假装网络断了。ServicepublicclassUserService{Transactionalpublicvoidregister(Stringname,Stringpassword)throwsIOException{// 插入用户userMapper.insertUser(name,password);log.info(用户插入成功);// 故意抛个 IO 异常thrownewIOException(网络异常假装连不上第三方);}}运行你会发现用户数据成功插进数据库了事务没回滚这就是那个哥们儿翻车的原因。1.2 rollbackFor Exception.class把所有异常都拉回来那怎么办用rollbackFor属性。rollbackFor的意思就是“回滚条件”。你给它指定一个异常类型告诉 Spring“只要遇到这种异常就给我回滚。”Exception是所有异常的爹。你写rollbackFor Exception.class意思就是“甭管啥异常运行时还是非运行时统统给我回滚”Transactional(rollbackForException.class)publicvoidregister(Stringname,Stringpassword)throwsIOException{userMapper.insertUser(name,password);thrownewIOException(网络异常);}再运行抛出IOException后事务乖乖回滚数据库干干净净。你还可以指定多个异常比如Transactional(rollbackFor{IOException.class,SQLException.class})意思是只有遇到IO异常或SQL异常才回滚别的异常不管。1.3 总结一下 rollbackFor写法效果Transactional只回滚运行时异常RuntimeException和ErrorTransactional(rollbackFor Exception.class)所有异常都回滚Transactional(rollbackFor {IOException.class})只有指定的异常才回滚老铁们记住如果你的方法里调了文件上传、网络请求、第三方接口一定加上rollbackFor Exception.class否则钱飞了你都不知道喔。二、isolation隔离级别—— 解决多人抢数据的“打架”问题2.1 三个读数据的问题用生活例子讲多个事务同时操作同一张表就像多个人同时改同一个 Excel 文件会出幺蛾子。主要有三个问题脏读你读到别人还没提交的数据。人家万一反悔了回滚你读到的就是垃圾。不可重复读你在同一个事务里两次读同一行数据结果不一样。因为中间被别人改了并提交了。幻读你在同一个事务里两次查询记录条数不一样。因为中间被别人插入了新数据。举个例子用你和你老婆同时登录网银来理解脏读你老婆转了 100 元给你还没点“确认”。这时候你查余额发现多了 100 元。结果她点了“取消”钱根本没转。你刚才看到的是假数据。不可重复读你先查余额 1000 元然后你老婆转了 100 元并确认。你再查余额变成 900 元。两次结果不一样。幻读你先查订单列表有 10 条然后你老婆下了一个新订单并确认。你再查变成 11 条。多出来一条像幻觉一样。2.2 MySQL 的四种隔离级别从松到严数据库用“隔离级别”来控制这些问题。级别越高越安全但性能越低因为要加锁。隔离级别英文中文名脏读不可重复读幻读性能read uncommitted读未提交可能可能可能最高read committed读已提交不会可能可能较高repeatable read可重复读不会不会不会一般MySQL默认serializable串行化不会不会不会最低注意MySQL InnoDB 在repeatable read级别下通过MVCC 间隙锁Next-Key Lock解决了当前读的幻读问题普通快照读不存在幻读。日常业务开发中我们可以认为不会出现幻读。2.3 Spring 里的隔离级别怎么用Spring 提供了 5 种选项Transactional(isolationIsolation.READ_COMMITTED)publicvoidtransferMoney(){// 业务代码}各种隔离级别的中文意思Isolation.DEFAULT用数据库自己的MySQL 默认是repeatable readIsolation.READ_UNCOMMITTED读未提交最不安全基本不用Isolation.READ_COMMITTED读已提交常用性能好Isolation.REPEATABLE_READ可重复读MySQL 默认安全Isolation.SERIALIZABLE串行化最安全但最慢基本不用老铁们实际开发怎么选普通业务比如论坛、评论用DEFAULT就完事了。高并发、读多写少的场景用READ_COMMITTED性能更好。金融、支付、库存这种对数据一致性要求高的用REPEATABLE_READ。SERIALIZABLE千万慎用慢到你怀疑人生。三、propagation传播机制—— 方法调用时事务咋传递3.1 啥是传播机制举个例子你就懂了先看一段代码TransactionalpublicvoidmethodA(){// 干点啥methodB();// 调用 methodB}TransactionalpublicvoidmethodB(){// 干点啥}当methodA调用methodB时methodB是跟着methodA共用同一个事务还是methodB自己单独开一个新事务这就是事务传播机制——它决定了事务方法之间调用时事务怎么传递。生活例子你和同事都在写报告你们都有各自的“工作单”。你写了部分内容后需要同事帮忙。同事是直接在你的工作单上继续写加入你的事务还是自己另开一张新工作单新建独立事务这就是传播机制。重要前提必须是不同类之间的方法调用AOP 才能生效。同一个类里方法 A 调方法 B事务是不生效的这是 Spring 代理的坑。3.2 Spring 的 7 种传播行为重点记 2 个Spring 提供了 7 种传播行为但咱普通人只需要记住两个最常用的其他的了解就行。传播行为英文中文意思干啥用的required必需的有事务就加入没有就新建默认requires_new需要新的不管有没有事务都新建一个独立事务supports支持有事务就加入没有就算了mandatory强制的必须有事务没有就报错not_supported不支持不用事务有事务就先挂起never绝不不能用事务有事务就报错nested嵌套嵌套事务可以局部回滚用生活例子帮你记结婚买房版required必须有房。你有房我就跟你住你没房咱俩一起买。requires_new必须买新房。你有房我也不住必须两人一起买一套新的。supports有房就住没房就租房。随缘。mandatory必须有房没房就不结婚。霸道。not_supported不要房。你有房我也不住必须租房。never绝对不能有房。你有房咱就分手。nested有房就住但允许你在房子里搞点副业局部回滚不影响整体。3.3 重点演示required vs requires_new咱用“用户注册 记录日志”这个经典场景来演示。场景用户注册成功了要记一条日志。但是记录日志的方法可能会失败比如抛异常。情况1两个方法都用 required默认穿一条裤子ServicepublicclassUserService{Transactional(propagationPropagation.REQUIRED)publicvoidregister(){userMapper.insert();}}ServicepublicclassLogService{Transactional(propagationPropagation.REQUIRED)publicvoidaddLog(){inti1/0;// 故意除零抛异常logMapper.insert();}}ServicepublicclassBizService{TransactionalpublicvoiddoRegister(){userService.register();// 成功logService.addLog();// 抛异常}}运行结果addLog抛异常 → 整个事务回滚 →用户也没注册成功。这就是“穿一条裤子”一荣俱荣一损俱损。情况2LogService 用 requires_new各过各的把LogService改成Transactional(propagationPropagation.REQUIRES_NEW)publicvoidaddLog(){inti1/0;// 还是抛异常}运行结果addLog抛异常但它自己开了一个新事务只回滚自己的操作不影响外层事务。用户注册成功日志没记上。这就是“各过各的”你炸了跟我没关系。3.4 实际开发怎么选90% 的场景用required就够了。什么时候用requires_new当内层操作允许失败且不应该影响主业务的时候。比如记录日志、发送通知、写消息队列。这些操作失败了不应该让用户注册失败。nested很少用知道有这么个东西就行。四、一张表记住三大属性属性英文中文意思干啥的常用值rollbackFor回滚条件控制哪些异常触发回滚Exception.class所有异常isolation隔离级别控制并发读的问题DEFAULT或READ_COMMITTEDpropagation传播机制控制事务方法调用时咋传递REQUIRED默认或REQUIRES_NEW五、血泪实战小贴士必看一定加rollbackFor Exception.class只要你的方法里调了网络、文件、第三方接口就别偷懒。否则一个IOException就能让你的钱飞走。事务只加在 Service 层Controller 不要加事务职责乱而且容易失效。异常被 catch 住不抛出 事务不回滚如果你用try-catch把异常吃了Spring 感知不到事务照常提交。要么重新抛出要么手动回滚TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();同类内部方法调用事务不生效因为 Spring 事务靠代理自己调自己绕过了代理。必须跨类调用。requires_new会挂起事务有性能开销别滥用只在真正需要独立事务的地方用。根据您的要求我对第2点进行了详细扩充解释了为什么事务应该加在 Service 层而不能加在 Controller 层或 Mapper 层。同时保留了其他小贴士整体语言风格保持口语化、接地气。以下是修正后的五、血泪实战小贴士必看完整内容您可以直接替换原文章中的对应部分。五、血泪实战小贴士必看1. 一定加rollbackFor Exception.class只要你的方法里调了网络、文件、第三方接口就别偷懒。否则一个IOException就能让你的钱飞走。2. 事务只加在 Service 层Controller 和 Mapper 都不行很多老铁刚学的时候喜欢在 Controller 的方法上直接加Transactional或者在 Mapper 接口的方法上加。这两种都是错误用法咱一个一个说。为什么不能加在 Controller 层职责问题Controller 的职责是接收请求、参数校验、调用 Service、返回响应。它不应该关心“数据库事务怎么开怎么关”这种业务逻辑层的事情。如果你在 Controller 上加事务会带来几个问题一个 Controller 方法可能调用多个 Service这些 Service 可能各自有自己的事务你强行在 Controller 上开一个事务会把多个不相关的业务操作绑在一起导致不该回滚的也回滚了。事务会延长数据库连接的持有时间从 Controller 进入开始一直到方法结束连接一直被占用在高并发下容易造成连接池耗尽。违反分层架构表现层Controller不应该包含业务逻辑事务属于业务逻辑的范畴应该放在业务层Service。举个翻车例子RestControllerpublicclassOrderController{AutowiredprivateOrderServiceorderService;AutowiredprivateLogServicelogService;Transactional// ❌ 错误事务加在 ControllerPostMapping(/create)publicResultcreateOrder(RequestBodyOrderDtodto){orderService.create(dto);// 下单logService.saveLog(创建订单);// 记日志returnResult.ok();}}如果logService.saveLog()抛异常整个事务回滚订单也会被回滚。但记日志失败真的应该导致下单失败吗显然不合理。日志应该用REQUIRES_NEW独立事务而 Controller 上的Transactional把所有操作强行绑在了一起。正确做法Controller 不加事务让 Service 自己控制。RestControllerpublicclassOrderController{AutowiredprivateOrderServiceorderService;PostMapping(/create)publicResultcreateOrder(RequestBodyOrderDtodto){orderService.createOrder(dto);// Service 内部自己管理事务returnResult.ok();}}为什么不能加在 Mapper 层Mapper 只是执行单条 SQL 的接口。事务的本质是将多个数据库操作捆绑成一个原子操作如果你只在 Mapper 的单个方法上加Transactional那相当于每个 SQL 自己开一个事务跟没开一样。而且Mapper 是数据访问层它不应该知道“哪些操作要放在一起”。这是业务层Service的职责。举个反例MapperpublicinterfaceUserMapper{Transactional// ❌ 完全没用而且没有意义Insert(insert into user...)intinsert(Useruser);}这个事务只包裹了一条 SQL根本起不到“多个操作一起成功或失败”的作用。事务应该加在哪一层答案Service 层业务逻辑层。因为一个业务往往由多个数据访问操作组成比如扣库存 创建订单 减余额。把这些操作放在一个 Service 方法里然后在这个 Service 方法上加Transactional才是正确姿势。ServicepublicclassOrderService{AutowiredprivateUserMapperuserMapper;AutowiredprivateOrderMapperorderMapper;AutowiredprivateStockMapperstockMapper;Transactional(rollbackForException.class)// ✅ 正确加在 Service 方法上publicvoidcreateOrder(OrderDtodto){// 1. 扣库存stockMapper.deduct(dto.getProductId(),dto.getCount());// 2. 创建订单orderMapper.insert(dto);// 3. 减用户余额userMapper.reduceBalance(dto.getUserId(),dto.getAmount());}}总结层级能否加事务原因Controller❌ 不能职责是接收请求不应包含业务事务Service✅ 应该业务逻辑层一个业务包含多个数据操作Mapper❌ 不能单条 SQL 不需要事务且分层不合理3. 异常被 catch 住不抛出 事务不回滚如果你用try-catch把异常吃了Spring 感知不到事务照常提交。要么重新抛出要么手动回滚TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();4. 同类内部方法调用事务不生效这个坑很多人踩先说原因因为 Spring 事务靠代理自己调自己绕过了代理。必须跨类调用。老铁这个问题一定要搞清楚是事务本身不生效不是传播机制不生效。Spring 的事务是靠AOP 代理实现的。从外部调用你类的方法时调用的是 Spring 生成的代理对象代理对象会帮你开启、提交、回滚事务。但是当你在类内部通过this.methodB()调用自己的另一个方法时用的是原始对象不是代理对象所以事务增强的代码根本不会执行。举个例子ServicepublicclassUserService{TransactionalpublicvoidmethodA(){this.methodB();// ❌ 内部调用事务不生效}Transactional(propagationPropagation.REQUIRES_NEW)publicvoidmethodB(){// 这个事务不会开启因为是通过 this 直接调用的}}从外面调用userService.methodA()时methodA本身的事务是生效的因为是外部调用。但methodA里面通过this.methodB()调用methodBmethodB上的Transactional完全不生效不会开启新事务也不会加入现有事务。那怎么解决让 Spring 帮你拿到代理对象然后用代理对象调用内部方法。有两种常见方式方式一自己注入自己推荐简单ServicepublicclassUserService{AutowiredprivateUserServiceself;// 注入自己的代理对象TransactionalpublicvoidmethodA(){self.methodB();// ✅ 通过代理调用事务生效}Transactional(propagationPropagation.REQUIRES_NEW)publicvoidmethodB(){}}方式二使用AopContext.currentProxy()需要开启配置先在 Spring Boot 启动类或任意Configuration配置类上加EnableAspectJAutoProxy(exposeProxy true)然后ServicepublicclassUserService{TransactionalpublicvoidmethodA(){UserServiceproxy(UserService)AopContext.currentProxy();proxy.methodB();// ✅ 通过代理调用}Transactional(propagationPropagation.REQUIRES_NEW)publicvoidmethodB(){}}一句话总结同类内部调用事务不生效是因为直接调用了原始对象绕过了 Spring 代理。不是传播机制不生效而是事务压根儿就没开。遇到这种情况用上面两种方式改成代理调用就行了。5.requires_new会挂起事务有性能开销别滥用只在真正需要独立事务的地方用比如日志、消息通知。结尾老铁们到这里Spring 事务的核心用法就简单介绍完了。上一篇咱们学会了声明式事务会用Transactional了。这一篇咱们把rollbackFor回滚条件、isolation隔离级别、propagation传播机制这三个属性彻底吃透了。觉得有用别忘了点赞、收藏、关注老铁们下期见

更多文章