嵌套事务不生效?浅谈Spring事务

前言

Spring对事务做了完整的整合,对我们开发人员来说,仅需要简单的几行配置(甚至不需要)外加@Transactional注解的声明即可在代码中插入事务管理。相反地,Spring同时也对我们开发人员屏蔽了很多细节,使得滥用或者错用导致我们的事务常常无法生效。本文简单地谈谈,事务如何才能生效,尤其针对嵌套事务,应该如何“嵌套”

事务生效

我们先来简单谈谈,事务如何才能生效呢?

开启(配置)事务

  • 基于xml配置(略)
  • 基于注解配置@EnableTransactionManagementspringboot1.4之后默认开启
  • 对于某些场景(比如多数据源的配置)无法自动装配配置类时,需要手动配置事务
	@Bean
    public DataSourceTransactionManager dataSourceTransactionManager() {
        return new DataSourceTransactionManager(targetDataSource);
    }

数据库支持事务

比如mysqlinnodb引擎才能支持事务

线程

事务的入口与业务逻辑必须在同一线程中,即事务入口不会对其他线程内的事务进行回滚。即使在其他线程内开启事务(@Async + @Transactional),事务间也是完全隔离

异常

只有抛出RuntimeException的子类异常事务才会回滚,非该类异常或者异常被处理,则不会触发事务回滚

代码

重点讨论,上面提到的所有条件可以认为是先置条件。一般地,我们为代码(常常是业务层)添加事务时,只需要在方法上添加@Transactional注解。如下

	@Transactional
    public void test1() {
        testUserMapper.insertSelective(
                TestUser.builder()
                        .id(1)
                        .age(1)
                        .name("dd1")
                        .build()
        );

        int i = 1 / 0;
    }

则此时代码在执行到int i = 1 / 0;时抛出异常,触发事务回滚,没有任何问题。

再比如我们使用try catch语句将异常代码包起来消化掉(指的是不往外抛出)

	@Transactional
    public void test1() {
        testUserMapper.insertSelective(
                TestUser.builder()
                        .id(1)
                        .age(1)
                        .name("dd1")
                        .build()
        );

        try {
            int i = 1 / 0;
        } catch (Exception e) {

            System.out.println("消化了异常");
        }
    }

则此时正如我们之前提到的,只有抛出RuntimeException异常的子类才会触发事务,因而此处事务不会回滚,数据被插入

事务嵌套

往往,我们的业务不会这么简单,时常伴随着类内(外)方法的调用,而调用类内方法有两种方式

@Component
@Slf4j
public class TxHandler {

    @Autowired
    private TestUserMapper testUserMapper;

    @Autowired
    TxHandler txHandler;

    public void outter() {
        this.inner();

        txHandler.inner();
    }

    public void inner() {
		
    }
}

这两种调用看似没有区别,txHandler不就是注入的自身对象嘛!但对于事务的使用来说,区别可就大了。

AOP

Spring的事务基于sping-aop实现,而spring实现aop的方式又是基于动态代理(JDK | CGLib)。我们借助spring官网一段aop的示例来说明(类名、方法名有变动,但本质不变)

示例

public interface AopMechanismsIntf {

    void a();

    void b();
}

public class AopMechanisms implements AopMechanismsIntf {

    @Override
    public void a() {

        System.out.println("target a");
        System.out.println("=============================");

        this.b();
    }

    @Override
    public void b() {

        System.out.println("target b");
        System.out.println("=============================");
    }
}

public class MyAdvice implements MethodBeforeAdvice {

    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {

        System.out.println("be proxied");
    }
}

然后我们创建 AopMechanisms 类的代理对象,并调用a方法,看看会发生什么

	public static void main(String[] args) {
        AopMechanismsIntf aopMechanisms = new AopMechanisms();
        
        ProxyFactory proxyFactory = new ProxyFactory(aopMechanisms);

        proxyFactory.addInterface(AopMechanismsIntf.class);
        proxyFactory.addAdvice(new MyAdvice());

        AopMechanismsIntf proxy = (AopMechanismsIntf) proxyFactory.getProxy();
        proxy.a();
    }

哦豁
哦?我们发现a方法被“增强”了,但是b方法却没有被“增强”!事实上,proxy对象确确实实是aopMechanisms 的代理对象,即proxy的b方法确确实实被代理修饰了。但是注意this.b()(因此我们特别的指明了this)的this实际上是aopMechanisms对象 (target object)而不是proxy对象,因此调用的b自然是没有被代理方法修饰咯。

回到事务

那么现在,回到主题,我们修改一下方法,将事务加进来

1

@Component
@Slf4j
public class TxHandler {

    @Autowired
    private TestUserMapper testUserMapper;

    @Autowired
    TxHandler txHandler;

    @Transactional
    public void outter() {

        this.inner();
    }

    public void inner() {

        testUserMapper.insertSelective(
                TestUser.builder()
                        .id(1)
                        .age(1)
                        .name("dd1")
                        .build()
        );

        int i = 1 / 0;
    }
}

可以看到,外层方法是有事务的,执行内部方法是会抛出异常,抛出到外部方法,外部方法事务回滚。数据不被插入,即事务生效。没有问题。
异常
事务生效

2

然后我们修改外层方法,让他处理掉inner抛出来的异常

	@Transactional
    public void outter() {

        try {
            this.inner();
        } catch (Exception e) {
            
        }
    }

	public void inner() {

        testUserMapper.insertSelective(
                TestUser.builder()
                        .id(1)
                        .age(1)
                        .name("dd1")
                        .build()
        );

        int i = 1 / 0;
    }

则此时内部方法抛出异常,被外部方法处理消化,事务无感知,没有回滚,因此数据被插入,事务没生效(添加的事务形同虚设,平时开发很常见的一个问题。)
被插入

3

现在我们给内部方法也加上事务

@Component
@Slf4j
public class TxHandler {

    @Autowired
    private TestUserMapper testUserMapper;

    @Autowired
    TxHandler txHandler;

    // 此处我们去掉外部事务
    public void outter() {

        try {
            this.inner();
        } catch (Exception e) {

        }
    }

    @Transactional
    public void inner() {

        testUserMapper.insertSelective(
                TestUser.builder()
                        .age(1)
                        .name("dd1")
                        .build()
        );

        int i = 1 / 0;
    }
}

试想一下,此时不存在外部事务,内部方法的事务会不会感知到异常并回滚呢。答案是不会,准确地说,内部方法根本没有事务。因为this.inner();,this对象可不是事务代理类哦~连事务都不存在,何谈回滚呢?
数据还是被插入

4

那么如何在这种情况下让事务生效呢?
我们的原业务类(target class)要被aop代理后才具有事务处理能力,而aop代理这个行为必然是由spring容器处理的,因此我们在类中注入本身的一个实例(该实例由容器管理,必然是被aop过的),再通过该实例调用内部方法,这样内部方法的事务就生效了。如下

	// 外部方法没有事务
	public void outter() {
		
		// 外部方法没有处理异常,且通过代理对象调用内部方法
        txHandler.inner();
    }

	@Transactional
    public void inner() {

        testUserMapper.insertSelective(
                TestUser.builder()
                        .age(1)
                        .name("dd1")
                        .build()
        );

        int i = 1 / 0;
    }

txHandler对象可是容器代理(有了事务功能)的代理对象,因此用它调用inner方法,事务感知到抛出的异常而生效,数据没有被插入
异常
数据没有插入

5

可以发现,上面方法我们刻意去掉了外部方法的事务。一方面是孤立内部事务证明它确实生效了。另一方面,我们现在加上外部事务,并处理掉异常。那么现在,因为事务的默认传播机制Propogation.REQUIRED,内部事务会加入外部事务。此时内部事务感知到异常,将事务标记为回滚,并将异常抛给外部方法。到了外部方法,异常被处理,又要提交事务,那么会发生什么情况
异常
自然是抛出了org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only异常

6

如何处理上述问题呢?其实,如果我们不干预业务层的异常处理,而是统一抛出去由上层处理的话,是不会出现这种问题的。但如果遇上了,解决方法也是有的:
第一种,我们提到事务默认的传播机制导致内部事务加入外部事务,那么我们显示的修改内部事务传播机制为Propagation.REQUIRES_NEW,让他开启一个新的事务就好了。但这种在一定程度上改变了原方法的语义

第二种,我们在外部方法处理完异常后,显示的标记事务状态为回滚TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();,那么内外达成一致的话,事务自然也就成功回滚啦~这也是比较“优雅”的一种解决方法

但究其根本,应该从源头杜绝这种问题的出现。正确的使用、嵌套事务,对于异常做好严格的处理与抛出,才是正确的解决方法

总结

综上,我们通过几个简单的示例来说明了事务为什么不会生效,以及某些嵌套事务的场景为什么会导致事务失效甚至发生异常。其实理解了Spring事务的本质是基于aop代理后,这些问题也就迎刃而解了。

©️2020 CSDN 皮肤主题: 精致技术 设计师:CSDN官方博客 返回首页