Spring事务 - Spring事务失效的场景

Spring事务失效的场景

   概要

   说到Spring事务,大家可能都会想到@Transactional注解,但是很多人只是停留在基础的使用层面上,在遇到一些比较特殊的场景,事务可能没有生效,直接在生产上暴露了,这可能就会导致比较严重的生产事故。今天,我们就简单来说下Spring事务的原理,然后总结一下spring事务失败的场景,并提出对应的解决方案。

   一、Spring事务原理

   JDBC中操作事务的伪代码如下: 

 1 //Get database connection
 2 Connection connection = DriverManager.getConnection();
 3 //Set autoCommit is false
 4 connection.setAutoCommit(false);
 5 //use sql to operate database
 6 .........
 7 //Commit or rollback
 8 connection.commit()/connection.rollback
 9 
10 connection.close();     

   需要在各个业务代码中编写代码如commit()、close()来控制事务。

   但是Spring不乐意这么干了,这样对业务代码侵入性太大了,所有就用一个事务注解@Transactional来控制事务,底层实现是基于切面编程AOP实现的,而Spring中实现AOP机制采用的是动态代理,具体分为JDK动态代理和CGLIB动态代理两种模式。

   1. Spring的bean的初始化过程中,发现方法有@Transactional注解,就需要对相应的Bean进行代理,生成代理对象。

   2. 然后在方法调用的时候,会执行切面的逻辑,而这里切面的逻辑中就包含了开启事务、提交事务或者回滚事务等逻辑。

   注意:Spring 本身不实现事务,底层还是依赖于数据库的事务。没有数据库事务的支持,Spring事务是不会生效的。

   二、事务失效场景

   有时候,我们明明在类或者方法上添加了@Transactional注解,却发现方法并没有按事务处理。其实,以下场景会导致事务失效。整理如下图:

 

   三、Spring框架配置问题

   1. 没有被Spring管理

1 // @Service
2 public class OrderServiceImpl implements OrderService {
3     @Transactional
4     public void updateOrder(Order order) {
5         // update order
6     }
7 }

   Spring声明式事务的实现完全依赖于Spring的AOP代理机制,未被Spring管理的类中的方法不受Spring的AOP代理管理,因此,声明式事务失效。

   这里将@Service 注解注释掉,这个类就不会被加载成一个 Bean,那这个类就不会被 Spring 管理了,事务自然就失效了。

   事务不生效原因

   @Service注解注释之后, Spring事务( @Transactional)没有生效,是因为 Spring事务是由 AOP机制实现的,也就是说从 Spring IOC容器获取 bean时, Spring会为目标类创建代理,来支持事务的。但是 @Service被注释后,这个service类都不是 spring管理的, 那怎么创建代理类来支持事务呢。

   解决方案:

   需要保证每个事务注解的每个Bean被Spring管理。

   2.  没有在Spring配置文件中启用事务管理器

 1 @Configuration
 2 public class AppConfig {
 3     // 没有配置事务管理器
 4 }
 5 
 6 @Service
 7 public class MyService {
 8     @Transactional
 9     public void doSomething() {
10         // ...
11     }
12 }

   事务不生效原因:

   如果没有配置事务管理器或未启用事务管理,使用 @Transactional 注解时,Spring 将不会对方法进行事务处理,导致事务失效。事务管理器负责创建、提交和回滚事务,是实现事务控制的核心组件。

   解决方案:

   在 AppConfig中配置一个事务管器并且启用事务管理器

 1 @Configuration
 2 @EnableTransactionManagement
 3 public class AppConfig {
 4     @Bean
 5     public PlatformTransactionManager transactionManager() {
 6         return new DataSourceTransactionManager(dataSource());
 7     }
 8 }
 9 
10 @Service
11 public class MyService {
12     @Transactional
13     public void doSomething() {
14         // ...
15     }
16 }

   说明:如果是Spring Boot项目,它默认会自动配置事务管理器并开启事务支持。

   四、Spring AOP代理问题

   1.  方法使用 final 或 static关键字

1 @Service
2 public class TianLuoServiceImpl  {
3 
4     @Transactional
5     public final void addTianLuo(TianLuo tianluo) {
6        //...
7     }
8 }

   事务不生效的原因:如果一个方法被声明为 final或者 static,则该方法不能被子类重写,也就是说无法在该方法上进行动态代理,这会导致 Spring无法生成事务代理对象来管理事务。

   具体来说:

   1)如果Spring使用了Cglib代理实现(比如你的代理类没有实现接口),而你的业务方法恰好使用了final或者static关键字,那么事务也会失败。更具体地说,它应该抛出异常,因为Cglib使用字节码增强技术生成被代理类的子类并重写被代理类的方法来实现代理。如果被代理的方法的方法使用final或static关键字,则子类不能重写被代理的方法。

   2)如果Spring使用JDK动态代理实现,JDK动态代理是基于接口实现的,那么final和static修饰的方法也就无法被代理。

   总而言之,方法连代理都没有,那么肯定无法实现事务回滚了。

   解决方案: addTianLuo 事务方法不要用 final修饰或者 static修饰。

   2.  方法没有被public修饰

   @Transactional注解只能作用于public修饰的方法上

 1 @Service
 2 public class TianLuoServiceImpl implements TianLuoService {
 3 
 4     @Autowired
 5     private TianLuoMapper tianLuoMapper;
 6 
 7     @Transactional
 8     private void addTianLuo(TianLuo tianluo) {
 9         tianLuoMapper.save(tianluo);
10     }
11 }

    事务失效原因:Spring AOP 代理时,TransactionInterceptor (事务拦截器)在目标方法执行前后进行拦截,DynamicAdvisedInterceptor(CglibAopProxy 的内部类)的 intercept 方法或 JdkDynamicAopProxy 的 invoke 方法会间接调用 AbstractFallbackTransactionAttributeSource的 computeTransactionAttribute 方法,获取Transactional 注解的事务配置信息。

1 protected TransactionAttribute computeTransactionAttribute(Method method,
2     Class<?> targetClass) {
3         // Don't allow no-public methods as required.
4         if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
5         return null;
6 }

    此方法会检查目标方法的修饰符是否为 public,不是 public 则不会获取@Transactional 的属性配置信息。

   注意:protected、private 修饰的方法上使用 @Transactional 注解,虽然事务无效,但不会有任何报错。

   解决方案:将当前方法访问级别更改为public。

   3.  在同一个类中的方法调用

 1 @Service
 2 public class UserServiceImpl {
 3 
 4     @Autowired
 5     UserMapper userMapper;
 6 
 7     public void A() {
 8         B();
 9     }
10 
11     @Transactional
12     public void B() {
13         userMapper.deleteById(1);
14         int i = 10 / 0; //模拟发生异常
15     }
16     
17 }

   事务失效原因:像上面的代码,B方法使用@Transactional注解标注,在A方法中调用了B方法,在外部调用A方法时,B方法的事务不会生效。这是由于使用Spring AOP代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由Spring生成的代理对象来管理。

   那么如果确实在同一类中调用事务方法怎么办呢?有以下3种方法解决:

   1) 引入自身bean

 1 @Service
 2 public class UserServiceImpl {
 3 
 4     @Autowired
 5     UserMapper userMapper;
 6 
 7     @Autowired
 8     UserServiceImpl userServiceImpl;
 9 
10     public void A() {
11         userServiceImpl.B();
12     }
13 
14     @Transactional
15     public void B() {
16         userMapper.deleteById(1);
17         int i = 10 / 0; //模拟发生异常
18     }
19 
20 }

   2) 通过ApplicationContext引入bean

 1 @Service
 2 public class UserServiceImpl {
 3 
 4     @Autowired
 5     UserMapper userMapper;
 6 
 7     @Autowired
 8     ApplicationContext applicationContext;
 9 
10     public void A() {
11         ((UserServiceImpl) applicationContext.getBean("userServiceImpl")).B();
12     }
13 
14     @Transactional
15     public void B() {
16         userMapper.deleteById(1);
17         int i = 10 / 0; //模拟发生异常
18     }
19 
20 }

   3)通过AopContext获取当前代理类

   在启动类上添加注解@EnableAspectJAutoProxy(exposeProxy = true),表示是否对外暴露代理对象,即是否可以获取AopContext。然后,在业务类上使用AopContext。

 1 @Service
 2 public class UserServiceImpl {
 3 
 4     @Autowired
 5     UserMapper userMapper;
 6 
 7     public void A() {
 8         ((UserServiceImpl) AopContext.currentProxy()).B();
 9     }
10 
11     @Transactional
12     public void B() {
13         userMapper.deleteById(1);
14         int i = 10 / 0; //模拟发生异常
15     }
16 
17 }

  五、底层数据库不支持

  1.  数据库的存储引擎不支持事务

  Spring事务的底层还是依赖于数据库本身的事务支持。在MySQL中,MyISAM存储引擎是不支持事务的,InnoDB引擎才支持事务。因此开发阶段设计表的时候,确认你的选择的存储引擎是支持事务的。

  六、Transactional配置问题

  1.  配置错误的@Transactional注解

1 @Transactional(readOnly = true)
2 public void updateUser(User user) {
3     userDao.updateUser(user);
4 }

  事务不生效的原因

  虽然使用了 @Transactional注解,但是注解中的 readOnly=true属性指示这是一个只读事务,因此在更新 User实体时会抛出异常。

  解决方案

  将 readOnly属性设置为 false,或者移除了 @Transactional注解中的 readOnly属性。

  2.  事务超时时间设置过短

1 @Transactional(timeout = 1)
2 public void doSomething() {
3     //...
4 }

  事务不生效的原因

  在上面的例子中, timeout属性被设置为 1秒,这意味着如果事务在 1 秒内无法完成,则报事务超时了。

  解决方案:需要合理设置超时时间,以确保事务有足够的时间完成。

  3.  错误使用事务传播机制

  Spring事务的传播机制是指在多个事务方法相互调用时,确定事务应该如何传播的策略。Spring提供了七种事务传播机制:REQUIRED、SUPPORTS、MANDATORY、REQUIRES_NEW、NOT_SUPPORTED、NEVER、NESTED。如果不知道这些传播策略的原理,很可能会导致操作失败。详细请参考文章《Spring事务 - 事务传播机制》

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

 1 @Service
 2 public class OrderServiceImpl {
 3 
 4     @Transactional(propagation = Propagation.NEVER)
 5     public void cancelOrder(UserModel userModel) {
 6         // 取消订单
 7         cancelOrder(orderDTO);
 8         // 还原库存
 9         restoreProductStock(orderDTO.getProductId(), orderDTO.getProductCount());
10     }
11 }

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

   解决方案:选择正确的事务传播机制。

   4.  rollbackFor属性配置错误

1 @Service
2 public class TianLuoServiceImpl implements TianLuoService {
3 
4     @Transactional(rollbackFor = Error.class)
5     public void addTianLuo(TianLuo tianluo) {
6         //...
7         throw new Exception();
8     }
9 }

   事务不生效的原因:

   其实 rollbackFor属性指定的异常必须是 Throwable或者其子类。默认情况下, RuntimeException和 Error两种异常都是会自动回滚的。但是因为以上的代码例子,指定了 rollbackFor = Error.class,但是抛出的异常又是 Exception,而 Exception和Error没有任何什么继承关系,因此事务就不生效。

   如下图:

   解决方案: rollbackFor 属性指定的异常与抛出的异常匹配。

   七、开发使用不当

   1.  事务注解被覆盖导致事务失效

 1 public interface MyRepository {
 2     @Transactional
 3     void save(String data);
 4 }
 5 
 6 public class MyRepositoryImpl implements MyRepository {
 7     @Override
 8     public void save(String data) {
 9         // 数据库操作
10     }
11 }
12 
13 public class MyService {
14 
15     @Autowired
16     private MyRepository myRepository;
17 
18     @Transactional
19     public void doSomething(String data) {
20         myRepository.save(data);
21     }
22 }
23 
24 public class MyTianluoService extends MyService {
25     @Transactional(propagation = Propagation.REQUIRES_NEW)
26     public void doSomething(String data) {
27         super.doSomething(data);
28     }
29 }

   事务失效的原因

   在 MyTianluoService 中重写 doSomething() 方法并使用 REQUIRES_NEW 传播行为时,会创建一个新事务。在调用 super.doSomething(data) 时,父类的事务被挂起,MyRepository.save(data) 在新事务中执行。因此,这可能导致数据不一致,因为如果新事务回滚,父类的操作不会回滚。

   2. 嵌套事务的坑

 1 @Service
 2 public class TianLuoServiceInOutService {
 3 
 4     @Autowired
 5     private TianLuoFlowService tianLuoFlowService;
 6     @Autowired
 7     private TianLuoMapper tianLuoMapper;
 8 
 9     @Transactional
10     public void addTianLuo(TianLuo tianluo) throws Exception {
11         tianLuoMapper.save(tianluo);
12         tianLuoFlowService.saveFlow(tianluo);
13     }
14 }
15 
16 @Service
17 public class TianLuoFlowService {
18 
19     @Autowired
20     private TianLuoFlowMapper tianLuoFlowMapper;
21 
22     @Transactional(propagation = Propagation.NESTED)
23     public void saveFlow(TianLuo tianLuo) {
24         tianLuoFlowMapper.save(tianLuo);
25         throw new RuntimeException();
26     }
27 }

   以上代码使用了嵌套事务,如果saveFlow出现运行时异常,会继续往上抛,到外层addTianLuo的方法,导致tianLuoMapper.save也会回滚啦。如果不想因为被内部嵌套的事务影响,可以用try-catch包住,如下:

1 @Transactional
2     public void addTianLuo(TianLuo tianluo) throws Exception {
3         tianLuoMapper.save(tianluo);
4         try {
5             tianLuoFlowService.saveFlow(tianluo);
6         } catch (Exception e) {
7           log.error("save tian luo flow fail,message:{}",e.getMessage());
8         }
9     }

   3. 事务多线程调用

 1 @Service
 2 public class TianLuoService {
 3 
 4     @Autowired
 5     private TianLuoMapper tianLuoMapper;
 6 
 7     @Autowired
 8     private TianLuoFlowService tianLuoFlowService;
 9 
10     @Transactional
11     public void addTianLuo(TianLuo tianluo) {
12         //保存tianluo数据库记录
13         tianLuoMapper.save(tianluo);
14         //多线程调用
15         new Thread(() -> {
16             tianLuoFlowService.saveFlow(tianluo);
17         }).start();
18     }
19 }
20 
21 @Service
22 public class TianLuoFlowService {
23 
24     @Autowired
25     private TianLuoFlowMapper tianLuoFlowMapper;
26 
27     @Transactional
28     public void save(TianLuo tianLuo) {
29         tianLuoFlowMapper.saveFlow(tianLuo);
30     }
31 }

    事务不生效原因:这是因为 Spring事务是基于线程绑定的, 每个线程都有自己的事务上下文,而多线程环境下可能会存在多个线程共享同一个事务上下文的情况,导致事务不生效。 Spring事务管理器通过使用线程本地变量( ThreadLocal)来实现线程安全。

    分析: 在Spring事务管理器中,通过TransactionSynchronizationManager类来管理事务上下文。TransactionSynchronizationManager内部维护了一个ThreadLocal对象,用来存储当前线程的事务上下文。在事务开始时,TransactionSynchronizationManager会将事务上下文绑定到当前线程的ThreadLocal对象中,当事务结束时,TransactionSynchronizationManager会将事务上下文从ThreadLocal对象中移除。

部分源码如下:

 1 public class TransactionSynchronizationManager {
 2 
 3     // 使用 ThreadLocal 来存储每个线程的事务上下文
 4     private static final ThreadLocal<TransactionalResourceHolder> resourceHolder =
 5             new ThreadLocal<>();
 6 
 7     // 绑定事务资源到当前线程
 8     public static void bindResource(Object resource) {
 9         // 创建一个新的 TransactionalResourceHolder,并将其绑定到当前线程
10         resourceHolder.set(new TransactionalResourceHolder(resource));
11     }
12 
13     // 从当前线程中解绑事务资源
14     public static Object unbindResource() {
15         // 获取当前线程绑定的 TransactionalResourceHolder
16         TransactionalResourceHolder holder = resourceHolder.get();
17         if (holder != null) {
18             // 获取资源
19             Object resource = holder.getResource();
20             // 清除当前线程的资源绑定
21             resourceHolder.remove(); // 清除资源
22             return resource; // 返回解绑的资源
23         }
24         return null; // 如果没有绑定的资源,则返回 null
25     }
26 
27     // 检查当前线程是否有活跃的事务
28     public static boolean isActualTransactionActive() {
29         // 如果当前线程的 ThreadLocal 中存在资源,说明有活跃的事务
30         return resourceHolder.get() != null;
31     }
32 }

   解决方案:尽量还是保证在同一个事务中处理。

   4. 业务方法本身捕获了异常

 1 @Transactional(rollbackFor = Exception.class)
 2 public void transactionTest() {
 3     try {
 4         User user = new User();
 5         UserService.insert(user);
 6         int i = 1 / 0;
 7     }catch (Exception e) {
 8         e.printStackTrace();
 9     }
10 }

    事务不生效的原因:

    事务中的异常已经被业务代码捕获并处理,而没有被正确地传播回事务管理器,事务将无法回滚。我们可以从 spring源码( TransactionAspectSupport这个类)中找到答案:

 1 public abstract class TransactionAspectSupport implements BeanFactoryAware, InitializingBean {
 2 
 3  //这方法会省略部分代码,只留关键代码哈
 4   @Nullable
 5  protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass, final InvocationCallback invocation) throws Throwable {
 6 
 7   if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
 8   
 9    TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
10    Object retVal;
11    try {
12     //Spring AOP中MethodInterceptor接口的一个方法,它允许拦截器在执行被代理方法之前和之后执行额外的逻辑。
13     retVal = invocation.proceedWithInvocation();
14    }
15    catch (Throwable ex) {
16     //用于在发生异常时完成事务(如果Spring catch不到对应的异常的话,就不会进入回滚事务的逻辑)
17     completeTransactionAfterThrowing(txInfo, ex);
18     throw ex;
19    }
20    finally {
21     cleanupTransactionInfo(txInfo);
22    }
23 
24    //用于在方法正常返回后提交事务。
25    commitTransactionAfterReturning(txInfo);
26    return retVal;
27   }
28 }

   分析:在invokeWithinTransaction方法中,当Spring catch到Throwable异常的时候,就会调用completeTransactionAfterThrowing()方法进行事务回滚的逻辑。但是,在TianLuoServiceImpl类的spring事务方法addTianLuo中,直接把异常catch住了,并没有重新throw出来,因此 Spring自然就catch不到异常啦,因此事务回滚的逻辑就不会执行,事务就失效了。

   解决方案

   在 spring事务方法中,当我们使用了 try-catch,如果catch住异常,记录完异常日志什么的,一定要重新把异常抛出来,如下:

 1 @Service
 2 public class TianLuoServiceImpl implements TianLuoService {
 3 
 4     @Autowired
 5     private TianLuoMapper tianLuoMapper;
 6 
 7     @Autowired
 8     private TianLuoFlowMapper tianLuoFlowMapper;
 9 
10     @Transactional(rollbackFor = Exception.class)
11     public void addTianLuo(TianLuo tianluo) {
12         try {
13             //保存tianluo数据库记录
14             tianLuoMapper.save(tianluo);
15             //保存tianluo flow数据库记录
16             tianLuoFlowMapper.saveFlow(tianluo);
17         } catch (Exception e) {
18             log.error("add TianLuo error,id:{},message:{}", tianluo.getId(),e.getMessage());
19             throw e;
20         }
21     }
22 }

   5.  手动抛了别的异常

 1 @Service
 2 public class TianLuoServiceImpl implements TianLuoService {
 3 
 4     @Autowired
 5     private TianLuoMapper tianLuoMapper;
 6     
 7     @Autowired
 8     private TianLuoFlowMapper tianLuoFlowMapper;
 9 
10     @Transactional
11     public void addTianLuo(TianLuo tianluo) throws Exception {
12         //保存tianluo数据库记录
13         tianLuoMapper.save(tianluo);
14         //保存tianluo流水数据库记录
15         tianLuoFlowMapper.saveFlow(tianluo);
16         throw new Exception();
17     }
18 }

   失效的原因:上面的代码例子中,手动抛了 Exception异常,但是是不会回滚的,因为Spring默认只处理 RuntimeException和Error,对于普通的 Exception不会回滚,除非,用 rollbackFor属性指定配置。
   解决方案:添加属性配置 @Transactional(rollbackFor = Exception.class)。


   参考链接:

   https://juejin.cn/post/7179080622504149029

   https://heapdump.cn/article/5542790

posted @ 2024-10-10 18:19  欢乐豆123  阅读(412)  评论(0编辑  收藏  举报