Spring事务(二)-@Transactional事务失效的场景

打印 上一主题 下一主题

主题 906|帖子 906|积分 2718

  有时候,我们明明在类或者方法上添加了@Transactional注解,却发现方法并没有按事务处理。其实,以下场景会导致Spring的@Transactional事务失效。
1、事务方法所在的类没有加载到Spring IOC容器中。

  @Transactional是Spring的注解,未被Spring管理的类中的方法不受@Transactional注解控制,这个应该很好理解。
2、方法没有被public修饰。

  众所周知,java的访问权限修饰符有:private、default、protected、public四种,但是@Transactional注解只能作用于public修饰的方法上。之所以会失效是因为在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 注解,虽然事务无效,但不会有任何报错,这是我们很容犯错的一点。
3、在同一个类中的方法调用。

  假如在同一个类中有A、B两个方法,如下:
  1. @Service
  2. public class UserServiceImpl {
  3.     @Autowired
  4.     UserMapper userMapper;
  5.     public void A() {
  6.         B();
  7.     }
  8.     @Transactional
  9.     public void B() {
  10.         userMapper.deleteById(1);
  11.         int i = 10 / 0; //模拟发生异常
  12.     }
  13.    
  14. }
复制代码
像上面的代码,B方法使用@Transactional注解标注,在A方法中调用了B方法,在外部调用A方法时,B方法的事务不会生效。这是由于使用Spring AOP代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由Spring生成的代理对象来管理。
  那么如果确实在同一类中调用事务方法怎么办呢?有以下3种方法解决:

  • 引入自身bean
  1. @Service
  2. public class UserServiceImpl {
  3.     @Autowired
  4.     UserMapper userMapper;
  5.     @Autowired
  6.     UserServiceImpl userServiceImpl;
  7.     public void A() {
  8.         userServiceImpl.B();
  9.     }
  10.     @Transactional
  11.     public void B() {
  12.         userMapper.deleteById(1);
  13.         int i = 10 / 0; //模拟发生异常
  14.     }
  15. }
复制代码

  • 通过 ApplicationContext 引入bean
  1. @Service
  2. public class UserServiceImpl {
  3.     @Autowired
  4.     UserMapper userMapper;
  5.     @Autowired
  6.     ApplicationContext applicationContext;
  7.     public void A() {
  8.         ((UserServiceImpl) applicationContext.getBean("userServiceImpl")).B();
  9.     }
  10.     @Transactional
  11.     public void B() {
  12.         userMapper.deleteById(1);
  13.         int i = 10 / 0; //模拟发生异常
  14.     }
  15. }
复制代码

  • 通过 AopContext 获取当前代理类
  在启动类上添加注解@EnableAspectJAutoProxy(exposeProxy = true),表示是否对外暴露代理对象,即是否可以获取AopContext。
  1. @Service
  2. public class UserServiceImpl {
  3.     @Autowired
  4.     UserMapper userMapper;
  5.     public void A() {
  6.         ((UserServiceImpl) AopContext.currentProxy()).B();
  7.     }
  8.     @Transactional
  9.     public void B() {
  10.         userMapper.deleteById(1);
  11.         int i = 10 / 0; //模拟发生异常
  12.     }
  13. }
复制代码
4、方法的事务传播类型不支持事务。

  若propagation属性设置如下三种事务传播行为,事务将不会发生回滚。
1、SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
2、NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
3、NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
Spring事务传播行为可以查阅我之前的一篇文章:Spring事务(一)-事务传播行为
5、不正确地捕获异常。

  使用了try-catch代码块将异常捕捉了,没有向上抛出异常,事务不会回滚。
  1. @Transactional
  2. private void A() throws Exception {
  3.     @Autowired
  4.     UserMapper userMapper;
  5.    
  6.     try {
  7.         //A方法插入数据
  8.         User u = New User();
  9.         u.setId(1);
  10.         u.setName("张三");
  11.         userMapper.insert(u);
  12.         /**
  13.          * B方法也插入数据
  14.          */
  15.         b.insert();
  16.     } catch (Exception e) {
  17.         e.printStackTrace();
  18.     }
  19. }
复制代码
上面的代码,如果B方法内部抛了异常,而A方法此时try catch了B方法的异常,那么,事务并不会回滚,而且会抛出如下异常:
  1. org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
复制代码
因为当ServiceB中抛出了一个异常以后,ServiceB标识当前事务需要rollback。但是ServiceA中由于你手动的捕获这个异常并进行处理,ServiceA认为当前事务应该正常commit。此时就出现了前后不一致,也就是因为这样,抛出了前面的UnexpectedRollbackException异常。
6、属性rollbackFor 设置错误。

  rollbackFor 可以指定能够触发事务回滚的异常类型。Spring默认抛出了未检查unchecked异常(继承自 RuntimeException 的异常)或者 Error才回滚事务;其他异常不会触发回滚事务。如果在事务中抛出其他类型的异常,但却期望 Spring 能够回滚事务,就需要指定 rollbackFor 属性。
7、数据库不支持事务。

  以MySQL为例,InnoDB引擎是支持事务的,而像MyISAM、MEMORY等是不支持事务的。 从MySQL5.5.5开始默认的存储引擎是InnoDB,之前默认都是MyISAM。
8、方法使用final修饰。

  如果一个方法不想被子类重写,那么我们就可以把他写成final修饰的方法。如果事务方法使用final修饰,那么AOP就无法在代理类中重写该方法,事务就不会生效。同样的,static修饰的方法也无法通过代理变成事务方法。
9、未开启事务。

  如果是SpringBoot项目,那么SpringBoot通过DataSourceTransactionManagerAutoConfiguration自动配置类帮我们开启了事务。如果是传统的Spring项目,则需要我们自己配置。在Spring配置文件配置如下:
  1. <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
  2.     <property name="entityManagerFactory" ref="entityManagerFactory"/>
  3. </bean>
  4. <tx:advice id="txAdvice" transaction-manager="transactionManager">
  5.     <tx:attributes>
  6.         <tx:method name="insert*" propagation="REQUIRED"/>
  7.         
  8.         <tx:method name="update*" propagation="REQUIRED"/>
  9.         <tx:method name="delete*" propagation="REQUIRED"/>
  10.     </tx:attributes>
  11. </tx:advice>
  12. <aop:config proxy-target-class="true" expose-proxy="true">
  13.    
  14.     <aop:pointcut id="txPointcut"
  15.                       expression="execution( * com.posun..service.impl..*+.*(..)) || execution(* com.posun..task..*+.*(..))
  16.                        || execution(* com.posun.report.ReportJdbc.*(..)) "/>
  17.     <aop:advisor id="txAdvisor" advice-ref="txAdvice" pointcut-ref="txPointcut"/>
  18. </aop:config>
复制代码
 或者,使用注解的方式。
  1. [/code][size=4][b] 10、多线程调用[/b][/size]
  2. [code]@Service
  3. public class UserServiceImpl {
  4.     @Autowired
  5.     UserMapper userMapper;
  6.     @Transactional
  7.     public void A() {
  8.         userMapper.deleteById(1);
  9.         new Thread(()->{
  10.             userMapper.deleteById(2);
  11.             int i = 10/0; //模拟发生异常        
  12.             }).start();
  13.     }
  14. }
复制代码
   以上代码,A方法中,启动了一个新的线程,并在新的线程中发生了异常,这样A方法是不会发生回滚的。因为两个操作不在一个线程中,获取到的数据库连接不一样,从而是两个不同的事务,所以也不会回滚。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

我可以不吃啊

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表