文章

Spring事务失效和异常回滚总结


一 Spring事务传播机制

七大传播机制:

传播属性

含义

Propagation.REQUIRED

如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。

Propagation.SUPPORTS

如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式执行。

Propagation.MANDATORY

如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。

Propagation.REQUIRES_NEW

无论当前是否存在事务,都创建一个新的事务。如果当前存在事务,则将其挂起。

Propagation.NOT_SUPPORTED

以非事务方式执行操作,如果当前存在事务,则将其挂起。

Propagation.NEVER

以非事务方式执行操作,如果当前存在事务,则抛出异常。

Propagation.NESTED

如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则创建一个新事务。嵌套事务是外部事务的一部分,但也有独立的保存点,可以回滚到该保存点。

二 Spring事务不生效场景

1 非公共方法上使用事务注解

Java的访问权限主要有四种:private、default、protected、public,它们的权限从左到右,依次变大。

Spring事务是通过AOP(面向切面编程)实现的,只有公共方法上的事务注解才会生效。如果在非公共方法上使用事务注解,事务将不会被管理,导致失效。

 // This will not work, as privateMethod() is not public
 @Transactional
 private void privateMethod() {
     // ...
 }

2 Final方法上使用事务注解

Spring事务是通过AOP(面向切面编程)实现的,在它的代理类中,无法重写final方法,而添加事务功能,导致失效。

 @Transactional
 public final void finalMethod() {
     // ...
 }

3 自调用问题

当在同一个类中的一个@Transactional方法调用另一个@Transactional方法时,事务可能会失效。这是因为Spring使用代理机制来实现事务,自调用方法绕过了代理,导致事务管理器无法介入。

 @Transactional
 public void methodA() {
     // ...
     methodB(); // This call may not be transactional
 }
 ​
 @Transactional
 public void methodB() {
     // ...
 }

三个解决思路: 1)methodB放到另外一个类中,然后注入进当前类

2)该Service类中注入自己(不推荐)

3)通过AopContext.currentProxy()获取代理对象

4 对象未被Spring管理

即使用spring事务的前提是:对象要被spring管理,需要创建bean实例。

 //@Service
 public class UserService {
 ​
     @Transactional
     public void add(UserModel userModel) {
          saveData(userModel);
          updateData(userModel);
     }    
 }

5 多线程调用

如果在一个带有@Transactional注解的方法中启动一个新线程,新线程的操作将在一个新的独立事务中运行,而不是与原始事务关联。如果在新线程中发生异常,它将独立于原始事务的异常处理,原始事务可能不会回滚。 以下是一个简单的示例,说明在多线程情况下Spring事务可能失效:

 @Service
 public class MyService {
 ​
     @Autowired
     private MyRepository repository;
 ​
     @Transactional
     public void performTransactionalOperation() {
         // 一些业务逻辑
 ​
         // 在事务中启动新线程
         new Thread(() -> {
             // 新线程中的操作,这不会与原始事务关联
             repository.updateDataInNewThread();
 ​
             // 可能发生异常,但原始事务不受影响
             throw new RuntimeException("Exception in new thread");
         }).start();
 ​
         // 主线程中的一些其他操作
         // ...
     }
 }
 ​
 @Repository
 public class MyRepository {
 ​
     @PersistenceContext
     private EntityManager entityManager;
 ​
     @Transactional
     public void updateDataInNewThread() {
         // 更新数据库操作
         // ...
     }
 }
 ​

6 表不支持事务

如MySQL的数据库引擎myisam

7 没有启用事务管理器

检查应用的配置文件,确保@EnableTransactionManagement注解被正确添加到配置类中。如果没有启用事务管理,事务将不会生效。

 @Configuration
 @EnableTransactionManagement
 public class AppConfig {
     // 配置类
 }

如果你是SpringBoot项目,那么 TransactionAutoConfiguration 是Spring Boot中的一个自动配置类,用于配置和启用Spring事务管理。这个自动配置类通常会自动生效,无需手动配置,它通过类路径下的相关依赖来判断是否需要启用事务管理。

以下是TransactionAutoConfiguration的一些关键点:

  1. 条件判断: TransactionAutoConfiguration类使用@ConditionalOnClass注解,该注解表示只有在类路径下存在指定的类时,自动配置才会生效。具体来说,它通常会检查是否存在PlatformTransactionManager类。

     @Configuration(proxyBeanMethods = false)
     @ConditionalOnClass(PlatformTransactionManager.class)
     @EnableTransactionManagement
     @ConditionalOnMissingBean(TransactionManagementConfigurationSelector.class)
     public class TransactionAutoConfiguration {
         // ...
     }
  2. @EnableTransactionManagement: 这个注解用于启用Spring的注解驱动的事务管理。它会自动注册TransactionManagementConfigurationSelector,该选择器在符合条件的情况下会注册ProxyTransactionManagementConfiguration

  3. 条件注解: TransactionAutoConfiguration类还使用了@ConditionalOnMissingBean注解,这表示只有在容器中不存在某个特定类型的bean时,自动配置才会生效。这有助于确保不会覆盖用户自定义的事务管理配置。

  4. 其他相关配置: 在这个自动配置类中,还包括其他一些相关的配置,例如TransactionInterceptor的配置和TransactionAttributeSource的配置等。

总体而言,TransactionAutoConfiguration的作用是根据类路径下的条件自动配置Spring事务管理。一旦在应用中添加了相关的事务依赖,这个自动配置类就会生效,使得应用具备默认的事务管理功能。如果需要进行定制,可以通过自定义事务管理配置来覆盖或扩展默认的配置。

三 Spring事务不回滚场景

1 未抛出受检异常

spring事务,默认情况下只会回滚RuntimeException(运行时异常)和Error(错误),对于普通的Exception(非运行时异常),它不会回滚。 解决方法是使用@Transactional注解的rollbackFor属性,明确指定需要回滚的异常类型。

 @Transactional(rollbackFor = Exception.class)
 public void methodWithTransaction() throws Exception {
     // ...
     throw new Exception("This will trigger a rollback");
 }

2 事务传播机制不当

如果在一个事务内部调用另一个使用不同事务传播机制的方法,可能会导致子事务无法正常回滚。例如,如果外部事务使用REQUIRED传播行为,而内部事务使用REQUIRES_NEW传播行为,内部事务的回滚不会影响外部事务。确保事务传播机制符合业务需求。

 @Transactional(propagation = Propagation.REQUIRED)
 public void outerMethod() {
     // ...
     innerMethod(); // This method has a different propagation behavior
 }
 ​
 @Transactional(propagation = Propagation.REQUIRES_NEW)
 public void innerMethod() {
     // ...
     throw new RuntimeException("This will not rollback the outer transaction");
 }

或者我们在手动设置propagation参数的时候,把传播特性设置错了,比如:

 @Service
 public class UserService {
 ​
     @Transactional(propagation = Propagation.NEVER)
     public void add(UserModel userModel) {
         saveData(userModel);
         updateData(userModel);
     }
 }

我们可以看到add方法的事务传播特性定义成了Propagation.NEVER,这种类型的传播特性不支持事务,如果有事务则会抛异常。

目前只有这三种传播特性才会创建新事务:REQUIRED,REQUIRES_NEW,NESTED。

3 自己吞了异常

事务不会回滚,最常见的问题是:开发者在代码中手动try...catch了异常。比如:

 @Slf4j
 @Service
 public class UserService {
     
     @Transactional
     public void add(UserModel userModel) {
         try {
             saveData(userModel);
             updateData(userModel);
         } catch (Exception e) {
             log.error(e.getMessage(), e);
         }
     }
 }

这种情况下spring事务当然不会回滚,因为开发者自己捕获了异常,又没有手动抛出,换句话说就是把异常吞掉了。

四 嵌套事务

1 嵌套事务回滚多了

 public class UserService {
 ​
     @Autowired
     private UserMapper userMapper;
 ​
     @Autowired
     private RoleService roleService;
 ​
     @Transactional
     public void add(UserModel userModel) throws Exception {
         userMapper.insertUser(userModel);
         roleService.doOtherThing();
     }
 }
 ​
 @Service
 public class RoleService {
 ​
     @Transactional(propagation = Propagation.NESTED)
     public void doOtherThing() {
         System.out.println("保存role表数据");
     }
 }

因为doOtherThing方法出现了异常,没有手动捕获,会继续往上抛,到外层add方法的代理方法中捕获了异常。所以,这种情况是直接回滚了整个事务,不只回滚单个保存点。

可以将内部嵌套事务放在try/catch中,并且不继续往上抛异常。这样就能保证,如果内部嵌套事务中出现异常,只回滚内部事务,而不影响外部事务。

 @Slf4j
 @Service
 public class UserService {
 ​
     @Autowired
     private UserMapper userMapper;
 ​
     @Autowired
     private RoleService roleService;
 ​
     @Transactional
     public void add(UserModel userModel) throws Exception {
 ​
         userMapper.insertUser(userModel);
         try {
             roleService.doOtherThing();
         } catch (Exception e) {
             log.error(e.getMessage(), e);
         }
     }
 }

还有一种情况需要注意,也可能导致回滚多了,那就是doOtherThing的事务传播是Propagation.REQUIRED,就算使用了try-catch也会将外部事务标记为已回滚。这种情况之前在生产问题碰到过,比较坑,感兴趣的可以在网上查看具体原因或者等我下次整理发出一篇新的博客。

License:  CC BY 4.0