深入Spring事务源码剖析事务(四)

深入Spring事务之实际场景下各个传播特性对事务的影响

概述

本篇文章将结合代码去演示,在各个场景下,各个传播特性到底会怎样去操作事务,可以更好的了解传播特性,更好的让我们理解传播特性的使用,以至于实际场景可以更灵活配置事务。

环境准备

这里为了方便,直接使用SpringBoot快速开发做实验,省去了编写配置文件的麻烦。

  • SpringBoot

  • MyBatis

  • MySQL

  • IDEA 2017.2

  • AOP 切面依赖包(其实这个可有可无,这里配置AOP切面依赖是为了演示如何在本类事务方法中执行同类的另一个事务方法,具体为什么后面会介绍到)

    <!--aop 切面-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjrt</artifactId>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
    </dependency>
    

场景简介

model层

有两个model分别为UserRole对象

public class User {

    private Integer id;

    private String name;

    //get set 略...
}
public class Role {

    private Integer id;

    private String name;

    //get set 略...
}

Mapper层

Mapper层意义其实不大,这里就只是分别对UserMapperRoleMapper写了一个insert方法

Service层

@Service
public class TransactionAService {

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private TransactionBService bService;
    @Autowired
    private TransactionCService cService;

    // 稍后修改传播特性
    @Transactional(propagation = Propagation.REQUIRED)
    public void insertUserAndRole(User user, Role role){
        // 后面添加...
    }

    @Transactional()
    public void insertUser(User user){
        // 后面添加...
    }
}
@Service
public class TransactionBService {

    @Autowired
    private RoleMapper roleMapper;

    // 稍后修改传播特性
    @Transactional()
    public void insertRole(Role role){
        // 这里根据role对象信息插入一条语句
        roleMapper.insert(role);
        // 抛出一个异常
        throw new RuntimeException("BService抛出一个异常");
    }
}
@Service
public class TransactionCService {
    
    @Autowired
    private RoleMapper roleMapper;

    @Transactional
    public void insertRole(Role role){
        // 这里CService与BService一样的操作
        roleMapper.insert(role);
        throw new RuntimeException("CService抛出一个异常");
    }
}

Controller层

@RequestMapping("test")
public void test(){
    User user = new User();
    user.setName("testUser");

    Role role = new Role();
    role.setName("testRole");

    // 调用AService的方法
    transactionAService.insertUserAndRole(user, role);
}

在多事务的场景下实验各个传播特性

(外层事务传播特性为REQUIRED时)

内层事务为REQUIRES_NEW时

当外层事务正常,内层事务抛出异常时

AService

@Transactional(propagation = Propagation.REQUIRED)
public void insertUserAndRole(User user, Role role){
    userMapper.insert(user);
    try {
        bService.insertRole(role);
    }
    catch (Throwable e2){

    }
    //throw new RuntimeException("AService抛出一个异常");
}

此时bService不变,里面会抛出一个异常,此时访问test数据库数据每次都将清空),结果如下图:
在这里插入图片描述
从上面可以看出,就算B事务抛出了异常,A事务也照样提交的好好的,这是由于这两个是在两个事务中执行的,内层事务完全可以提交,外层事务catch异常就等于没有异常发生,外层事务也可以照常提交。

当外层事务抛出异常,内层事务无异常时

将上面最后一行代码注释打开,在执行完BService之后在AService里抛出一个异常,而BService中不抛异常,正常插入一条数据,结果如下图:

在这里插入图片描述

从上面可以看出,就算A事务抛出了异常,如果B事务已经执行完,提交完了之后就算A回滚了,也是不会影响到B事务的,B事务的数据并不会回滚,原因应该也很清楚了,这里是两个事务,B事务可以照常提交,A事务出异常之后照常回滚,两者互相隔离。

结论

如果内层事务抛出异常,内层事务正常回滚数据。外层事务需catch异常,就不会影响外层事务正常流程,可以正常提交事务更改数据

如果外层事务抛出异常,外层事务正常回滚数据。内层事务只要运行过(提交过了),就不会影响内层事务的正常流程,正常提交事务更改数据

内层事务为NESTED 时

当外层事务正常,内层事务抛出异常时

在这里插入图片描述

可以看出,此时结果其实与REQUIRES_NEW并无二异,但其实这里A事务与B事务是同一个事务,但为什么可以做到与REQUIRES_NEW效果一样的结果呢?就是因为NESTED在外层有事务的情况下会创建一个SavePoint,在内层事务抛出异常时事务回滚到了SavePoint,并不会影响外层事务,外层事务catch住异常了,也就可以正常走流程了。

当外层事务抛出异常,内层事务无异常时

在这里插入图片描述

这里就是与REQUIRES_NEW最大的不同点了,因为这里A事务和B事务还是同一个事务,在B事务完成,提交时只是去释放了SavePoint,而不是真正的提交,B事务要在A事务正常提交的情况下,B事务才能正常新增数据,因为他们都还是同一个事务。

结论

如果内层事务抛出异常,外层事务没有异常。内层事务正常回滚数据,外层事务需catch异常,就不会影响外层事务正常流程,可以正常提交事务更改数据。(结果与REQUIRES_NEW无异)

如果外层事务抛出异常,内层事务没有异常。内层事务将与外层事务共进退,外层事务将正常回滚,内层事务也一并回滚,只有外层事务正常提交,内层事务才可以提交(因为是同一个事务)。(与REQUIRES_NEW的区别)

内层事务为REQUIRED 时

当外层事务正常,内层事务抛出异常时

在这里插入图片描述

外层事务正常,当内层抛异常时,就算外层catch住了异常,也都还会将整个事务回滚(因为此时A事务与B事务都为同一个事务),具体为什么其实在上一篇文章中都有很详细的解释了,这里不多赘述

当外层事务抛出异常,内层事务无异常时

在这里插入图片描述
相信这种情况猜都能猜对吧?外层事务抛出异常,整个事务都会回滚,包括内层事务,因为同属一个事务。

结论

无论内层外层,哪一个地方抛出了异常,就算catch住异常都会回滚整个事务。因为在REQUIRED传播特性下,不管来几个他都会在原有事务上进行,用的同一个数据库连接,并且子事务在提交的时候都不会真正去提交,到最外层事务提交的时候才真正提交了。REQUIRED是最常用,也是Spring默认的事务。

内层事务为NOT_SUPPORTED

其他事务传播特性其实都不用讲了,其余的其实字面上都很好理解了,唯独这个传播特性个人感觉值得实验一下。

在上一篇文章中说到,如果这个传播特性是内层事务的话,将会把原事务挂起,也就意味着如果内层事务又来一个内内层事务,此时的内内层事务会变成新的一个事务,现在来验证一下这个观点吧!

内内层事务为REQUIRED
环境准备

AService 外层事务

@Transactional(propagation = Propagation.REQUIRED)
public void insertUserAndRole(User user, Role role){
    userMapper.insert(user);
    role.setName("BTransaction");
    try {
        bService.insertRole(role);
    }
    catch (Throwable e2){

    }
    // throw new RuntimeException("AService抛出一个异常");
}

BService 内层事务

@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void insertRole(Role role){
    roleMapper.insert(role);
    role.setName("CTransaction");
    // 其实这里catch与否都不影响结果
    //        try{
    cService.insertRole(role);
    //        }catch (Throwable e){
    //
    //        }

    //        throw new RuntimeException("BService抛出一个异常");
}

CService 内内层事务

@Transactional(propagation = Propagation.REQUIRED)
public void insertRole(Role role){
    roleMapper.insert(role);
    throw new RuntimeException("CService抛出一个异常");
}
当外层事务正常,内内层事务抛出异常时
  1. 当内层事务不catch异常
    在这里插入图片描述

    由结果可见,这似乎与你想象中的不太一致?不是说好的外层事务为REQUIRED时,内层事务也是REQUIRED的话,是同一个事务并且共进退吗?不是应该是内内层事务抛出异常,就算外层catch住了异常也一样都会回滚的吗?或者说,B事务明明没开启事务,为什么也会提交?

    这里我希望读者带着这些问题看我给出的答案。

    • 首先,为什么只有内内层事务回滚,外层事务不回滚。在上一篇文章中我也有详细解释,只不过当时是理论上去说明,这里用实践再次告诉你,因为NOT_SUPPORTED这个传播特性会将原事务挂起,这里就是精髓所在了,上一篇文章有说到挂起的作用,这里实例中很好的体现了挂起的作用,在这里挂起起到的作用是将线程变量中的holder变为null,在后面如果还有内层事务的话,创建事务时其实transaction中的holder是为null的,判断当前是没有事务存在的情况,所以这里内内层事务不管是REQUIRED也好NOT_SUPPORTED也好REQUIRES_NEW也好,都是一样的效果,因为都会新建一个事务,新建一个连接,在抛出异常时是可以进行正常回滚的(因为是新事务),而在上层事务,catch住了异常,由于A事务与B事务都不是同一个事务,所以根本就不会影响,只要外层catch住了异常,A事务仍然可以照常提交。
    • 其次,为什么B事务明明没有开启事务,也可以提交。这是因为MyBatis的缘故,B事务中Spring确实没有给它开事务,是用一个空事务运行的,但MyBatis底层会判断当前是否存在事务,如果不存在的话它会自动执行提交操作,如果存在事务的话它就不会自动提交,而交给Spring去进行事务的管理,这也就可以解释平时生产环境中那些不打事务注解的方法,仍然可以提交事务更改数据库(不提交事务是不可以更改数据库内容的)。由于这里MyBatis帮我们自动提交了,所以就算回滚的话事务也已经做了提交操作了,并不会帮你回滚,所以在生产环境中有必要的方法还是需要加上事务,让Spring做相关的事务操作,可以减少脏数据的产生。
  2. 当内层事务catch住异常时

    其实这里结果是一样的,为什么其实我前面已经讲过了,这里其实是没有Spring事务的,就算你抛出异常,也不会执行回滚操作,如果catch住异常就更不用说了,这里B事务能提交数据其实是MyBatis自动帮我们提交了事务,因为MyBatis判断此时是没有事务存在,才会自动提交,验证这个观点就在于catch与不catch结果是相同的。

当外层事务抛出异常,内内层事务无异常时

在这里插入图片描述

其实上面已经讲了大部分原理了,因为此时A事务中会经过一个B事务,而B事务是NOT_SUPPORTED,在外层有事务情况下会讲外层事务挂起,此时C事务是B事务的一个子事务,是判断外层没有事务的,所以C会另起一个新事务,A事务与C事务是两个事务,B事务没有事务,所以此时内内层事务是可以提交的(因为是新事务),外层事务抛出了异常,也是可以正常回滚的,但仅仅只是回滚A事务的操作,由于B事务无事务,MyBatis底层自动提交,所以是否有异常对B事务都没有所谓,这也就可以解释了为什么只有B事务与C事务成功新增了数据,就是因为C事务成功提交了,B事务自动提交了,A事务回滚了,而C与A并不是同一个事务,C不作回滚操作

结论

如果内内层事务抛出异常,外层事务没有异常。内内层事务正常回滚数据,外层事务需catch异常,就不会影响外层事务正常流程,可以正常提交事务更改数据

如果外层事务抛出异常,内内层事务没有异常。内内层事务将正常提交外层事务将正常回滚内内层事务不回滚,因为外层事务与内内层事务并不是同一个事务。

在这里,内层事务(B事务)都是没有事务的状态。所以得出结论,如果事务中间有一个NOT_SUPPORTED相隔,外层事务与内层事务都将是两个事务

总结多事务场景下传播特性的结果

其实实验进行到这里已经结束了,为什么这么说?其实你外层事务不论是REQUIRED还是REQUIRES_NEW还是NESTED,都是一样的效果,为什么?这里留给读者实验与思考,而内层事务的三种情况都已经实验完成,外层事务只要是那三种,结果都是一样的。

而一个特殊情况,内层事务为NOT_SUPPORTED时,内内层事务其实不管是那三种的哪一种,实验的结果都还是一样,外层事务不管是那三种的哪一种,实验的结果都也还是一样的,这里的为什么与上面的为什么都是一样的原因,留给读者去实验去思考,很简单的,只要理解了Spring底层事务的操作,各种传播特性的组合你都可以信手拈来,操作自如,不管如何变换,你都可以猜测出最后的结果,理解才是最重要的。所以这里我才说,实验其实已经做完了。而单事务场景,字面上都能很好的理解,这里不做演示,那三种传播特性在单事务场景都是一样的功能,这里的为什么和上面两个为什么原因也都还是一样…(这么拗口的吗)

如何在同类的事务方法中运行同类事务方法?

场景

@Service
public class TransactionAService implements ITransactionAService {

    @Autowired
    private UserMapper userMapper;

    @Transactional(propagation = Propagation.REQUIRED)
    public void insertUserAndRole(User user, Role role){
        userMapper.insert(user);
        try {
//            ((TransactionAService)AopContext.currentProxy()).insertUser(user);
            insertUser(user);
        }
        catch (Throwable e2){

        }
//        throw new RuntimeException("AService抛出一个异常");
    }

    @Transactional()
    public void insertUser(User user){
        userMapper.insert(user);
        throw new RuntimeException();
    }
}

运行insertUserAndRole方法,会发生什么呢?

在这里插入图片描述

此时user表中神奇般插入了两条语句,这里应该需要有一个疑问,内层事务不是REQUIRED的隔离特性吗,在内层事务抛出异常时,外层事务就算catch了也会回滚,但结果好像两个都没有回滚。

这是因为insertUser这个方法并没有事务,然后MyBatis将内层事务自动提交了,外层事务catch住了异常,外层是有事务的,此时没有异常所以正常提交,所以可以看到两条数据。

为什么没有事务了呢?这就需要从AOP的原理去分析了,事务的原理其实就是AOP,那么在什么情况下事务是可以正常运行的呢?。

前提条件:当你执行insertUserAndRole方法时,其实执行的并不是原方法,是执行代理类的方法,而代理类会去匹配advisor(前面的文章中国有详细介绍),然后去执行事务增强器(advise),需要执行到事务增强器才会有事务的功能 ,也就是说,如果你执行的是原方法,是没有事务功能的。

需要理解这个前提条件,你才能懂得为什么,然后理解怎么做。

回到场景中来,分析这个场景,此时insertUserAndRole确实可以执行到事务增强器的,但insertUserAndRole中的insertUser方法是不能执行事务增强器的,为什么?这个方法不是也打上了**@Transaction注解吗?这是因为在执行insertUserAndRole方法时,会去执行代理类的方法,而代理类的方法中,名为insertUser的方法调用其实只是原方法,是没有被代理过的Method对象(这里需要有动态代理的基础,懂得动态代理的原理,底层重写了字节码,此时将原方法变成一个Method对象存放起来,在method对象调用前后加入代理逻辑从而完成动态代理),调用的是那个存储起来的Method.invoke,而不是代理类中的那个insertUser方法,这里应该就可以解惑为什么事务会失效了。同时我们可以知道,只要调用了代理类中的那个insertUser**方法,就可以避免事务失效了,那么怎么去调用代理类的方法?代理类是动态生成的,我们怎么去获取它?

JdkDynamicAopProxy这个类是AOP中的代理类,其中事务也都是由它创建(如果是CGLib动态代理的话是CglibAopProxy这个类),是哪种动态代理都不重要,因为都是一样的道理,我们需要关注的是两者共同的一个invoke方法,这里关注JDK动态代理的invoke方法(同等道理类比到CGLib中):

	@Override
	@Nullable
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		MethodInvocation invocation;
		Object oldProxy = null;
		boolean setProxyContext = false;

		TargetSource targetSource = this.advised.targetSource;
		Object target = null;

		//中间略过 ....
        
        Object retVal;

        if (this.advised.exposeProxy) {
            // Make invocation available if necessary.
            oldProxy = AopContext.setCurrentProxy(proxy);
            setProxyContext = true;
        }

		// 中间略过 ....

        if (setProxyContext) {
            // Restore old proxy.
            AopContext.setCurrentProxy(oldProxy);
        }
	}

这里只看关键代码,在invoke方法中,会现判断this.advisedexposeProxy这个属性是否为true,如果是,会将当时的这个代理对象放入AopContext中:

private static final 
    ThreadLocal<Object> currentProxy = new NamedThreadLocal<>("Current AOP proxy");

@Nullable
static Object setCurrentProxy(@Nullable Object proxy) {
    Object old = currentProxy.get();
    if (proxy != null) {
        currentProxy.set(proxy);
    }
    else {
        currentProxy.remove();
    }
    return old;
}

这里,currentProxy是一个线程变量,如果this.advised属性为true,将会把当前代理类对象放入线程变量中,那么是否有get方法?

public static Object currentProxy() throws IllegalStateException {
    Object proxy = currentProxy.get();
    if (proxy == null) {
        throw new IllegalStateException(
            "Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available.");
    }
    return proxy;
}

确实有get方法,但这里不叫getProxy,方法名为currentProxy(),到这里我们已经可以推断出,只要在同类事务方法中调用AopContextcurrentProxy方法即可拿到当前代理对象(因为是同一个线程,可以拿到对应的线程变量),但有一个前提,this.advised属性必须为true,那么这个属性需要怎么配置呢?

在SpringBoot中只需要在主运行类上打上注解@EnableAspectJAutoProxy(exposeProxy = true)即可,其效果相当于在配置文件中配置:

<aop:aspectj-autoproxy expose-proxy="true" />

原理是一样的,都是利用AOP原理,将此属性初始化成true,事务又是在AOP的基础上才有的功能,所以这里理解AOP的原理,这里也会很好的理解。

这样配置之后,需要加入AOP的依赖,否则会报错:

<!--aop 切面-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>

然后事务类变成:

@Service
public class TransactionAService implements ITransactionAService {

    @Autowired
    private UserMapper userMapper;

    @Transactional(propagation = Propagation.REQUIRED)
    public void insertUserAndRole(User user, Role role){
        userMapper.insert(user);
        try {
            ((TransactionAService)AopContext.currentProxy()).insertUser(user);
        }
        catch (Throwable e2){

        }
//        throw new RuntimeException("AService抛出一个异常");
    }

    @Transactional()
    public void insertUser(User user){
        userMapper.insert(user);
        throw new RuntimeException();
    }
}

这个时候再实验一下,看看结果如何:
在这里插入图片描述

此时的结果就在我们的预想范围之内了,内层事务确实加上了事务,在内层抛出异常时,就算外层事务catch住了,由于是同一个事务,也将一起回滚。

总结

到这里,Spring的事务剖析也就结束了,原本预估用两个篇幅完成它,结果写出来用了四篇,大概是3万多字的样子,不得不感概,Spring源码的复杂程度,Spring考虑了各种各样的情况,对各种场景各种类进行封装,一个小小的事务功能就需要这么长的篇幅来介绍,也仅仅只能介绍到最基本的,例如事务管理器都还是最基本的,也只介绍了声明式事务,没有介绍编程式事务,再次感慨Spring框架的强大。

在接下来我陆续会对Spring IOC AOP等等关于Spring的,深入源码进行分析,写几篇文章出来。

猜你喜欢

转载自blog.csdn.net/qq_41737716/article/details/84869652