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编辑


                                  相关文章
                                  |
                                  6天前
                                  |
                                  Java 数据库 开发者
                                  |
                                  6天前
                                  |
                                  Java 关系型数据库 MySQL
                                  【JavaEE】Spring事务-@Transactional参数介绍-事务的隔离级别以及传播机制
                                  【JavaEE】Spring事务-@Transactional参数介绍-事务的隔离级别以及传播机制
                                  9 0
                                  |
                                  6天前
                                  |
                                  消息中间件 Java 关系型数据库
                                  【JavaEE】Spring事务-事务的基本介绍-事务的实现-@Transactional基本介绍和使用
                                  【JavaEE】Spring事务-事务的基本介绍-事务的实现-@Transactional基本介绍和使用
                                  12 0
                                  |
                                  6天前
                                  |
                                  SQL Java 关系型数据库
                                  Spring 事务
                                  Spring 事务
                                  14 1
                                  |
                                  6天前
                                  |
                                  Java 数据库连接 数据库
                                  Spring事务简介,事务角色,事务属性
                                  Spring事务简介,事务角色,事务属性
                                  20 2
                                  |
                                  6天前
                                  |
                                  Java 数据库连接 数据库
                                  16:事务-Java Spring
                                  16:事务-Java Spring
                                  30 5
                                  |
                                  6天前
                                  |
                                  消息中间件 Java 关系型数据库
                                  Spring事务与分布式事务
                                  这篇文档介绍了事务的概念和数据库事务的ACID特性:原子性、一致性、隔离性和持久性。在并发环境下,事务可能出现更新丢失、脏读和不可重复读等问题,这些问题通过设置事务隔离级别(如读未提交、读已提交、可重复读和序列化)来解决。Spring事务传播行为有七种模式,影响嵌套事务的执行方式。`@Transactional`注解用于管理事务,其属性包括传播行为、隔离级别、超时和只读等。最后提到了分布式事务,分为跨库和跨服务两种情况,跨服务的分布式事务通常通过最终一致性策略,如消息队列实现。
                                  |
                                  6天前
                                  |
                                  监控 Java 测试技术
                                  Spring Boot与事务钩子函数:概念与实战
                                  【4月更文挑战第29天】在复杂的业务逻辑中,事务管理是确保数据一致性和完整性的关键。Spring Boot提供了强大的事务管理机制,其中事务钩子函数(Transaction Hooks)允许开发者在事务的不同阶段插入自定义逻辑。本篇博客将详细探讨事务钩子函数的概念及其在Spring Boot中的应用。
                                  38 1
                                  |
                                  6天前
                                  |
                                  XML Java 数据库连接
                                  精妙绝伦:玩转Spring事务编程的艺术
                                  【4月更文挑战第20天】
                                  34 0
                                  |
                                  6天前
                                  |
                                  XML Java 数据库
                                  在Spring框架中,XML配置事务
                                  在Spring框架中,XML配置事务