有时候,我们明明在类或者方法上添加了@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 注解的事务配置信息。

protected TransactionAttribute computeTransactionAttribute(Method method,    Class targetClass) {        // Don't allow no-public methods as required.        if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {        return null;}

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

注意:protected、private 修饰的方法上使用 @Transactional 注解,虽然事务无效,但不会有任何报错,这是我们很容犯错的一点。

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

  假如在同一个类中有A、B两个方法,如下:

@Servicepublic class UserServiceImpl {    @Autowired    UserMapper userMapper;    public void A() {        B();    }    @Transactional    public void B() {        userMapper.deleteById(1);        int i = 10 / 0; //模拟发生异常    }    }

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

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

  • 引入自身bean
@Servicepublic class UserServiceImpl {    @Autowired    UserMapper userMapper;    @Autowired    UserServiceImpl userServiceImpl;    public void A() {        userServiceImpl.B();    }    @Transactional    public void B() {        userMapper.deleteById(1);        int i = 10 / 0; //模拟发生异常    }}
  • 通过 ApplicationContext 引入bean
@Servicepublic class UserServiceImpl {    @Autowired    UserMapper userMapper;    @Autowired    ApplicationContext applicationContext;    public void A() {        ((UserServiceImpl) applicationContext.getBean("userServiceImpl")).B();    }    @Transactional    public void B() {        userMapper.deleteById(1);        int i = 10 / 0; //模拟发生异常    }}
  • 通过 AopContext 获取当前代理类

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

@Servicepublic class UserServiceImpl {    @Autowired    UserMapper userMapper;    public void A() {        ((UserServiceImpl) AopContext.currentProxy()).B();    }    @Transactional    public void B() {        userMapper.deleteById(1);        int i = 10 / 0; //模拟发生异常    }}

4、方法的事务传播类型不支持事务。

  若propagation属性设置如下三种事务传播行为,事务将不会发生回滚。

1、SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。

2、NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。

3、NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。

5、不正确地捕获异常。

  使用了try-catch代码块将异常捕捉了,没有向上抛出异常,事务不会回滚。

@Transactionalprivate void A() throws Exception {    @Autowired    UserMapper userMapper;        try {        //A方法插入数据        User u = New User();        u.setId(1);        u.setName("张三");        userMapper.insert(u);        /**         * B方法也插入数据         */        b.insert();    } catch (Exception e) {        e.printStackTrace();    }}

上面的代码,如果B方法内部抛了异常,而A方法此时try catch了B方法的异常,那么,事务并不会回滚,而且会抛出如下异常:

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配置文件配置如下:

<!--事务管理器配置--><bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">    <property name="entityManagerFactory" ref="entityManagerFactory"/></bean><tx:advice id="txAdvice" transaction-manager="transactionManager">    <tx:attributes>        <tx:method name="insert*" propagation="REQUIRED"/>        <!-- 所有insert开头的方法,以下同理 -->        <tx:method name="update*" propagation="REQUIRED"/>        <tx:method name="delete*" propagation="REQUIRED"/>    </tx:attributes></tx:advice><aop:config proxy-target-class="true" expose-proxy="true">    <!-- 只对业务逻辑层实施事务 -->    <aop:pointcut id="txPointcut"                      expression="execution( * com.posun..service.impl..*+.*(..)) || execution(* com.posun..task..*+.*(..))                       || execution(* com.posun.report.ReportJdbc.*(..)) "/>    <aop:advisor id="txAdvisor" advice-ref="txAdvice" pointcut-ref="txPointcut"/></aop:config>

或者,使用注解的方式。

<!-- 配置事务管理器--><bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"><property name="dataSource" ref="dataSource"/></bean><!-- 注解式事务声明配置--><tx:annotation-driven transaction-manager="transactionManager" />

10、多线程调用

@Servicepublic class UserServiceImpl {    @Autowired    UserMapper userMapper;    @Transactional    public void A() {        userMapper.deleteById(1);        new Thread(()->{            userMapper.deleteById(2);            int i = 10/0; //模拟发生异常                    }).start();    }}

  以上代码,A方法中,启动了一个新的线程,并在新的线程中发生了异常,这样A方法是不会发生回滚的。因为两个操作不在一个线程中,获取到的数据库连接不一样,从而是两个不同的事务,所以也不会回滚。