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 关系型数据库
                                  【SpringFramework】Spring事务
                                  本文简述Spring中数据库及事务相关衍伸知识点。
                                  51 9
                                  |
                                  3月前
                                  |
                                  Java 开发者 Spring
                                  理解和解决Spring框架中的事务自调用问题
                                  事务自调用问题是由于 Spring AOP 代理机制引起的,当方法在同一个类内部自调用时,事务注解将失效。通过使用代理对象调用、将事务逻辑分离到不同类中或使用 AspectJ 模式,可以有效解决这一问题。理解和解决这一问题,对于保证 Spring 应用中的事务管理正确性至关重要。掌握这些技巧,可以提高开发效率和代码的健壮性。
                                  156 13
                                  |
                                  3月前
                                  |
                                  缓存 安全 Java
                                  Spring高手之路26——全方位掌握事务监听器
                                  本文深入探讨了Spring事务监听器的设计与实现,包括通过TransactionSynchronization接口和@TransactionalEventListener注解实现事务监听器的方法,并通过实例详细展示了如何在事务生命周期的不同阶段执行自定义逻辑,提供了实际应用场景中的最佳实践。
                                  93 2
                                  Spring高手之路26——全方位掌握事务监听器
                                  |
                                  3月前
                                  |
                                  Java 关系型数据库 数据库
                                  京东面试:聊聊Spring事务?Spring事务的10种失效场景?加入型传播和嵌套型传播有什么区别?
                                  45岁老架构师尼恩分享了Spring事务的核心知识点,包括事务的两种管理方式(编程式和声明式)、@Transactional注解的五大属性(transactionManager、propagation、isolation、timeout、readOnly、rollbackFor)、事务的七种传播行为、事务隔离级别及其与数据库隔离级别的关系,以及Spring事务的10种失效场景。尼恩还强调了面试中如何给出高质量答案,推荐阅读《尼恩Java面试宝典PDF》以提升面试表现。更多技术资料可在公众号【技术自由圈】获取。
                                  |
                                  4月前
                                  |
                                  Java 开发者 Spring
                                  Spring高手之路24——事务类型及传播行为实战指南
                                  本篇文章深入探讨了Spring中的事务管理,特别是事务传播行为(如REQUIRES_NEW和NESTED)的应用与区别。通过详实的示例和优化的时序图,全面解析如何在实际项目中使用这些高级事务控制技巧,以提升开发者的Spring事务管理能力。
                                  86 1
                                  Spring高手之路24——事务类型及传播行为实战指南
                                  |
                                  4月前
                                  |
                                  JavaScript Java 关系型数据库
                                  Spring事务失效的8种场景
                                  本文总结了使用 @Transactional 注解时事务可能失效的几种情况,包括数据库引擎不支持事务、类未被 Spring 管理、方法非 public、自身调用、未配置事务管理器、设置为不支持事务、异常未抛出及异常类型不匹配等。针对这些情况,文章提供了相应的解决建议,帮助开发者排查和解决事务不生效的问题。
                                  142 1
                                  |
                                  4月前
                                  |
                                  XML Java 数据库连接
                                  Spring中的事务是如何实现的
                                  Spring中的事务管理机制通过一系列强大的功能和灵活的配置选项,为开发者提供了高效且可靠的事务处理手段。无论是通过注解还是AOP配置,Spring都能轻松实现复杂的事务管理需求。掌握这些工具和最佳实践,能
                                  135 3
                                  |
                                  5月前
                                  |
                                  Java API Spring
                                  springboot学习七:Spring Boot2.x 拦截器基础入门&实战项目场景实现
                                  这篇文章是关于Spring Boot 2.x中拦截器的入门教程和实战项目场景实现的详细指南。
                                  60 0
                                  springboot学习七:Spring Boot2.x 拦截器基础入门&实战项目场景实现
                                  |
                                  5月前
                                  |
                                  Java API Spring
                                  springboot学习六:Spring Boot2.x 过滤器基础入门&实战项目场景实现
                                  这篇文章是关于Spring Boot 2.x中过滤器的基础知识和实战项目应用的教程。
                                  67 0
                                  springboot学习六:Spring Boot2.x 过滤器基础入门&实战项目场景实现
                                  |
                                  5月前
                                  |
                                  Java 关系型数据库 MySQL
                                  Spring事务失效,我总结了这7个主要原因
                                  本文详细探讨了Spring事务在日常开发中常见的七个失效原因,包括数据库不支持事务、类不受Spring管理、事务方法非public、异常被捕获、`rollbackFor`属性配置错误、方法内部调用事务方法及事务传播属性使用不当。通过具体示例和源码分析,帮助开发者更好地理解和应用Spring事务机制,避免线上事故。适合所有使用Spring进行业务开发的工程师参考。
                                  69 2