Spring 事务失效的常见八大场景,注意避坑

简介: Spring 事务失效的常见八大场景,注意避坑

 1. 抛出检查异常导致事务不能正确回滚

@Service
 public class Service1 {
     @Autowired
     private AccountMapper accountMapper;
     @Transactional
     public void transfer(int from, int to, int amount) throws FileNotFoundException {
         int fromBalance = accountMapper.findBalanceBy(from);
         if (fromBalance - amount >= 0) {
             accountMapper.update(from, -1 * amount);
             new FileInputStream("aaa");
             accountMapper.update(to, amount);
         }
     }
 }

image.gif

    • 原因:Spring 默认只会回滚非检查异常
    • 解法:配置 rollbackFor 属性
      • @Transactional(rollbackFor = Exception.class)

        2. 业务方法内自己 try-catch 异常导致事务不能正确回滚

        @Service
         public class Service2 {
             @Autowired
             private AccountMapper accountMapper;
             @Transactional(rollbackFor = Exception.class)
             public void transfer(int from, int to, int amount)  {
                 try {
                     int fromBalance = accountMapper.findBalanceBy(from);
                     if (fromBalance - amount >= 0) {
                         accountMapper.update(from, -1 * amount);
                         new FileInputStream("aaa");
                         accountMapper.update(to, amount);
                     }
                 } catch (FileNotFoundException e) {
                     e.printStackTrace();
                 }
             }
         }

        image.gif

          • 原因:事务通知只有捉到了目标抛出的异常,才能进行后续的回滚处理,如果目标自己处理掉异常,事务通知无法知悉
          • 解法1:异常原样抛出
            • 在 catch 块添加 throw new RuntimeException(e);
              • 解法2:手动设置 TransactionStatus.setRollbackOnly()
                • 在 catch 块添加 TransactionInterceptor.currentTransactionStatus().setRollbackOnly();

                  3. aop 切面顺序导致导致事务不能正确回滚

                  @Service
                   public class Service3 {
                       @Autowired
                       private AccountMapper accountMapper;
                       @Transactional(rollbackFor = Exception.class)
                       public void transfer(int from, int to, int amount) throws FileNotFoundException {
                           int fromBalance = accountMapper.findBalanceBy(from);
                           if (fromBalance - amount >= 0) {
                               accountMapper.update(from, -1 * amount);
                               new FileInputStream("aaa");
                               accountMapper.update(to, amount);
                           }
                       }
                   }

                  image.gif

                  @Aspect
                   public class MyAspect {
                       @Around("execution(* transfer(..))")
                       public Object around(ProceedingJoinPoint pjp) throws Throwable {
                           LoggerUtils.get().debug("log:{}", pjp.getTarget());
                           try {
                               return pjp.proceed();
                           } catch (Throwable e) {
                               e.printStackTrace();
                               return null;
                           }
                       }
                   }

                  image.gif

                    • 原因:事务切面优先级最低,但如果自定义的切面优先级和他一样,则还是自定义切面在内层,这时若自定义切面没有正确抛出异常…
                    • 解法1、2:同情况2 中的解法:1、2
                    • 解法3:调整切面顺序,在 MyAspect 上添加 @Order(Ordered.LOWEST_PRECEDENCE - 1) (不推荐)

                    4. 非 public 方法导致的事务失效

                    @Service
                     public class Service4 {
                         @Autowired
                         private AccountMapper accountMapper;
                         @Transactional
                         void transfer(int from, int to, int amount) throws FileNotFoundException {
                             int fromBalance = accountMapper.findBalanceBy(from);
                             if (fromBalance - amount >= 0) {
                                 accountMapper.update(from, -1 * amount);
                                 accountMapper.update(to, amount);
                             }
                         }
                     }

                    image.gif

                      • 原因:Spring 为方法创建代理、添加事务通知、前提条件都是该方法是 public 的
                      • 解法1:改为 public 方法
                      • 解法2:添加 bean 配置如下(不推荐)
                      @Bean
                       public TransactionAttributeSource transactionAttributeSource() {
                           return new AnnotationTransactionAttributeSource(false);
                       }

                      image.gif

                      5. 父子容器导致的事务失效

                      package day04.tx.app.service;
                       // ...
                       @Service
                       public class Service5 {
                           @Autowired
                           private AccountMapper accountMapper;
                           @Transactional(rollbackFor = Exception.class)
                           public void transfer(int from, int to, int amount) throws FileNotFoundException {
                               int fromBalance = accountMapper.findBalanceBy(from);
                               if (fromBalance - amount >= 0) {
                                   accountMapper.update(from, -1 * amount);
                                   accountMapper.update(to, amount);
                               }
                           }
                       }

                      image.gif

                      控制器类

                      package day04.tx.app.controller;
                       // ...
                       @Controller
                       public class AccountController {
                           @Autowired
                           public Service5 service;
                           public void transfer(int from, int to, int amount) throws FileNotFoundException {
                               service.transfer(from, to, amount);
                           }
                       }

                      image.gif

                      App 配置类

                      @Configuration
                       @ComponentScan("day04.tx.app.service")
                       @EnableTransactionManagement
                       // ...
                       public class AppConfig {
                           // ... 有事务相关配置
                       }

                      image.gif

                      Web 配置类

                      @Configuration
                       @ComponentScan("day04.tx.app")
                       // ...
                       public class WebConfig {
                           // ... 无事务配置
                       }

                      image.gif

                      现在配置了父子容器,WebConfig 对应子容器,AppConfig 对应父容器,发现事务依然失效

                        • 原因:子容器扫描范围过大,把未加事务配置的 service 扫描进来
                        • 解法1:各扫描各的,不要图简便
                        • 解法2:不要用父子容器,所有 bean 放在同一容器

                        6. 调用本类方法导致传播行为失效

                        @Service
                         public class Service6 {
                             @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
                             public void foo() throws FileNotFoundException {
                                 LoggerUtils.get().debug("foo");
                                 bar();
                             }
                             @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
                             public void bar() throws FileNotFoundException {
                                 LoggerUtils.get().debug("bar");
                             }
                         }

                        image.gif

                          • 原因:本类方法调用不经过代理,因此无法增强
                          • 解法1:依赖注入自己(代理)来调用
                          • 解法2:通过 AopContext 拿到代理对象,来调用
                          • 解法3:通过 CTW,LTW 实现功能增强

                          解法1

                          @Service
                           public class Service6 {
                               @Autowired
                               private Service6 proxy; // 本质上是一种循环依赖
                               @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
                               public void foo() throws FileNotFoundException {
                                   LoggerUtils.get().debug("foo");
                                   System.out.println(proxy.getClass());
                                   proxy.bar();
                               }
                               @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
                               public void bar() throws FileNotFoundException {
                                   LoggerUtils.get().debug("bar");
                               }
                           }

                          image.gif

                          解法2,还需要在 AppConfig 上添加 @EnableAspectJAutoProxy(exposeProxy = true)

                          @Service
                           public class Service6 {
                               @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
                               public void foo() throws FileNotFoundException {
                                   LoggerUtils.get().debug("foo");
                                   ((Service6) AopContext.currentProxy()).bar();
                               }
                               @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
                               public void bar() throws FileNotFoundException {
                                   LoggerUtils.get().debug("bar");
                               }
                           }

                          image.gif

                          7. @Transactional 没有保证原子行为

                          @Service
                           public class Service7 {
                               private static final Logger logger = LoggerFactory.getLogger(Service7.class);
                               @Autowired
                               private AccountMapper accountMapper;
                               @Transactional(rollbackFor = Exception.class)
                               public void transfer(int from, int to, int amount) {
                                   int fromBalance = accountMapper.findBalanceBy(from);
                                   logger.debug("更新前查询余额为: {}", fromBalance);
                                   if (fromBalance - amount >= 0) {
                                       accountMapper.update(from, -1 * amount);
                                       accountMapper.update(to, amount);
                                   }
                               }
                               public int findBalance(int accountNo) {
                                   return accountMapper.findBalanceBy(accountNo);
                               }
                           }

                          image.gif

                          上面的代码实际上是有 bug 的,假设 from 余额为 1000,两个线程都来转账 1000,可能会出现扣减为负数的情况

                            • 原因:事务的原子性仅涵盖 insert、update、delete、select … for update 语句,select 方法并不阻塞
                              • 如上图所示,红色线程和蓝色线程的查询都发生在扣减之前,都以为自己有足够的余额做扣减

                              8. @Transactional 方法导致的 synchronized 失效

                              针对上面的问题,能否在方法上加 synchronized 锁来解决呢?

                              @Service
                               public class Service7 {
                                   private static final Logger logger = LoggerFactory.getLogger(Service7.class);
                                   @Autowired
                                   private AccountMapper accountMapper;
                                   @Transactional(rollbackFor = Exception.class)
                                   public synchronized void transfer(int from, int to, int amount) {
                                       int fromBalance = accountMapper.findBalanceBy(from);
                                       logger.debug("更新前查询余额为: {}", fromBalance);
                                       if (fromBalance - amount >= 0) {
                                           accountMapper.update(from, -1 * amount);
                                           accountMapper.update(to, amount);
                                       }
                                   }
                                   public int findBalance(int accountNo) {
                                       return accountMapper.findBalanceBy(accountNo);
                                   }
                               }

                              image.gif

                              答案是不行,原因如下:

                                • synchronized 保证的仅是目标方法的原子性,环绕目标方法的还有 commit 等操作,它们并未处于 sync 块内
                                • 可以参考下图发现,蓝色线程的查询只要在红色线程提交之前执行,那么依然会查询到有 1000 足够余额来转账

                                image.gif编辑

                                  • 解法1:synchronized 范围应扩大至代理方法调用
                                  • 解法2:使用 select … for update 替换 select

                                  好了,本文就到这里了!如果觉得内容不错的话,希望大家可以帮忙点赞转发一波,这是对我最大的鼓励,感谢🙏🏻

                                  资料获取👇 最后面就是领取暗号,公众号回复即可!

                                  image.gif编辑


                                  相关文章
                                  |
                                  2月前
                                  |
                                  SQL Java 关系型数据库
                                  Spring事务传播机制:7种姿势教你玩转"事务接力赛"
                                  事务传播机制是Spring框架中用于管理事务行为的重要概念,它决定了在方法调用时事务如何传递与执行。通过7种传播行为,开发者可以灵活控制事务边界,适应不同业务场景。例如:REQUIRED默认加入或新建事务,REQUIRES_NEW独立开启新事务,NESTED支持嵌套回滚等。合理使用传播机制不仅能保障数据一致性,还能提升系统性能与健壮性。掌握这“七种人格”,才能在复杂业务中游刃有余。
                                  |
                                  2月前
                                  |
                                  缓存 Java 应用服务中间件
                                  Spring Boot配置优化:Tomcat+数据库+缓存+日志,全场景教程
                                  本文详解Spring Boot十大核心配置优化技巧,涵盖Tomcat连接池、数据库连接池、Jackson时区、日志管理、缓存策略、异步线程池等关键配置,结合代码示例与通俗解释,助你轻松掌握高并发场景下的性能调优方法,适用于实际项目落地。
                                  532 5
                                  |
                                  3月前
                                  |
                                  Java 关系型数据库 数据库
                                  深度剖析【Spring】事务:万字详解,彻底掌握传播机制与事务原理
                                  在Java开发中,Spring框架通过事务管理机制,帮我们轻松实现了这种“承诺”。它不仅封装了底层复杂的事务控制逻辑(比如手动开启、提交、回滚事务),还提供了灵活的配置方式,让开发者能专注于业务逻辑,而不用纠结于事务细节。
                                  |
                                  8月前
                                  |
                                  Java Spring
                                  Spring中事务失效的场景
                                  因为Spring事务是基于代理来实现的,所以某个加了@Transactional的⽅法只有是被代理对象调⽤时, 那么这个注解才会⽣效 , 如果使用的是被代理对象调用, 那么@Transactional会失效 同时如果某个⽅法是private的,那么@Transactional也会失效,因为底层cglib是基于⽗⼦类来实现 的,⼦类是不能重载⽗类的private⽅法的,所以⽆法很好的利⽤代理,也会导致@Transactianal失效 如果在业务中对异常进行了捕获处理 , 出现异常后Spring框架无法感知到异常, @Transactional也会失效
                                  |
                                  8月前
                                  |
                                  Java 关系型数据库 数据库
                                  微服务——SpringBoot使用归纳——Spring Boot事务配置管理——常见问题总结
                                  本文总结了Spring Boot中使用事务的常见问题,虽然通过`@Transactional`注解可以轻松实现事务管理,但在实际项目中仍有许多潜在坑点。文章详细分析了三个典型问题:1) 异常未被捕获导致事务未回滚,需明确指定`rollbackFor`属性;2) 异常被try-catch“吃掉”,应避免在事务方法中直接处理异常;3) 事务范围与锁范围不一致引发并发问题,建议调整锁策略以覆盖事务范围。这些问题看似简单,但一旦发生,排查难度较大,因此开发时需格外留意。最后,文章提供了课程源代码下载地址,供读者实践参考。
                                  208 0
                                  |
                                  8月前
                                  |
                                  Java 关系型数据库 数据库
                                  微服务——SpringBoot使用归纳——Spring Boot事务配置管理——Spring Boot 事务配置
                                  本文介绍了 Spring Boot 中的事务配置与使用方法。首先需要导入 MySQL 依赖,Spring Boot 会自动注入 `DataSourceTransactionManager`,无需额外配置即可通过 `@Transactional` 注解实现事务管理。接着通过创建一个用户插入功能的示例,展示了如何在 Service 层手动抛出异常以测试事务回滚机制。测试结果表明,数据库中未新增记录,证明事务已成功回滚。此过程简单高效,适合日常开发需求。
                                  1119 0
                                  |
                                  8月前
                                  |
                                  Java 数据库 微服务
                                  微服务——SpringBoot使用归纳——Spring Boot事务配置管理——事务相关
                                  本文介绍Spring Boot事务配置管理,阐述事务在企业应用开发中的重要性。事务确保数据操作可靠,任一异常均可回滚至初始状态,如转账、购票等场景需全流程执行成功才算完成。同时,事务管理在Spring Boot的service层广泛应用,但根据实际需求也可能存在无需事务的情况,例如独立数据插入操作。
                                  229 0
                                  |
                                  6月前
                                  |
                                  人工智能 Java 数据库连接
                                  Spring事务失效场景
                                  本文深入探讨了Spring框架中事务管理可能失效的几种常见场景及解决方案,包括事务方法访问级别不当、方法内部自调用、错误的异常处理、事务管理器或数据源配置错误、数据库不支持事务以及不合理的事务传播行为或隔离级别。通过合理配置和正确使用`@Transactional`注解,开发者可以有效避免这些问题,确保应用的数据一致性和完整性。
                                  355 10
                                  |
                                  5月前
                                  |
                                  Java 关系型数据库 MySQL
                                  【Spring】【事务】初学者直呼学会了的Spring事务入门
                                  本文深入解析了Spring事务的核心概念与使用方法。Spring事务是一种数据库事务管理机制,通过确保操作的原子性、一致性、隔离性和持久性(ACID),维护数据完整性。文章详细讲解了声明式事务(@Transactional注解)和编程式事务(TransactionTemplate、PlatformTransactionManager)的区别与用法,并探讨了事务传播行为(如REQUIRED、REQUIRES_NEW等)及隔离级别(如READ_COMMITTED、REPEATABLE_READ)。
                                  451 1
                                  |
                                  8月前
                                  |
                                  SQL Java 数据库连接
                                  Spring中的事务是如何实现的
                                  1. Spring事务底层是基于数据库事务和AOP机制的 2. ⾸先对于使⽤了@Transactional注解的Bean,Spring会创建⼀个代理对象作为Bean 3. 当调⽤代理对象的⽅法时,会先判断该⽅法上是否加了@Transactional注解 4. 如果加了,那么则利⽤事务管理器创建⼀个数据库连接 5. 并且修改数据库连接的autocommit属性为false,禁⽌此连接的⾃动提交,这是实现Spring事务⾮ 常重要的⼀步 6. 然后执⾏当前⽅法,⽅法中会执⾏sql 7. 执⾏完当前⽅法后,如果没有出现异常就直接提交事务 8. 如果出现了异常,并且这个异常是需要回滚的就会回滚事务

                                  热门文章

                                  最新文章

                                  下一篇
                                  oss云网关配置