Spring事务传播机制定义
我正在参与掘金新人创作活动,一起开启写作之路。
如果对Spring事务原理有不明白的,建议先看上一篇文章Spring事务原理
来自官方的定义:传播机制定义了我们业务逻辑的事务边界,Spring 根据我们的传播机制设置启动和暂停事务,下面就看看Spring支持的传播机制有什么。
思考题:传播机制的作用到底是什么?文末揭晓
| 传播机制 | 作用 |
|---|---|
| PROPAGATION_REQUIRED | 支持当前事务,如果没有事务会创建一个新的事务 |
| PROPAGATION_SUPPORTS | 支持当前事务,如果没有事务的话以非事务方式执行 |
| PROPAGATION_MANDATORY | 支持当前事务,如果没有事务抛出异常 |
| PROPAGATION_REQUIRES_NEW | 创建一个新的事务并挂起当前事务 |
| PROPAGATION_NOT_SUPPORTED | 以非事务方式执行,如果当前存在事务则将当前事务挂起 |
| PROPAGATION_NEVER | 以非事务方式进行,如果存在事务则抛出异常 |
| PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。 |
下面将会围绕工作中比较常用的传播机制来进行相关讲解
- PROPAGATION_REQUIRED
- PROPAGATION_REQUIRES_NEW
- PROPAGATION_NESTED
黑板敲重点:以非事务方式运行,表示以非Spring事务运行在执行这个方法时,Spring事务管理器 不会去建立数据库连接,执行sql时,由Mybatis或JdbcTemplate自己来建立数据库连接来执行 sql。
PROPAGATION_REQUIRED:相同事务
insertUser调用insertTest方法,insertTest方法报错,执行流程如下:
思考题:insertTest抛错后,insertUser和insertTest是否会回滚?我们看下大概执行流程
-
controller层调用userService.insertUser()方法\
-
代理对象拦截insertUser方法,获取此方法上事务注解
-
判断当前事务传播机制,默认事务则看此事务对象是否有数据库连接,没有则创建
-
设置数据库的隔离级别,autocommit设置为false,设置超时时间
-
把数据连接conn放入ThreadLocal中
-
执行insertUser方法中的sql 时,获取此线程的数据库连接
-
调用自身的insertTest方法
-
insertTest抛出异常,异常被捕获
看完以上的流程后,不知道大家会不会已经有答案了,实际上,这两个方法执行的sql还是会提交成功的,我给大家来分析一下里面的细节
insertUser事务生效是因为AOP,事务AOP的切面类似于around环切,@Transatcational类似Poincut切点,有事务注解的方法就会被代理。
代理逻辑就是:在insertUser方法执行前生成事务-》执行insertUser方法-》事务提交或回滚,在这个过程中,要注意的是insertTest方法并没有产生新的代理,因为他们是同一个类,属于类自身的方法调用,也就是说,同一个类中两个具有事务注解的方法,被调用的一方事务是不起作用的,所以由始至终都是在同一个事务里面,所以抛错被捕获,最后还是一起提交了,如果没有捕获错误继续向上抛,那么会一起回滚,如果是两个方法都被代理,就算insertTest被捕获异常,也会一同回滚,因为同一个事务,要么一起成功,要么一起失败,那么有没有办法让insertTest的事务生效呢,答案是肯定的,有下面两种方法
1.注入自己UserService,用userService.insertUser(),这样可以保留方法在同一类中,需要注意的是这种方法只适用于单例对象,Spring会帮我们解决循环依,多例的话不适用
2.将事务方法挪去另外一个类TestService中,调用TestService.insertUser()
PROPAGATION_REQUIRED_NEW:新事务
insertUser调用insertTest方法,insertTest方法报错,执行流程如下:
同样的,我们看一下大概执行流程
1.首先,代理对象执行insertUser()方法前,先利用事务管理器新建一个数据库连接a
2. 将数据库连接a的autocommit改为false
3. 把数据库连接a设置到ThreadLocal中
4. 执行insertUser()方法中的sql
5. 执行insertUser()方法过程中,调用了insertTest()方法(注意用代理对象调用)
i. 代理对象执行insertTest()方法前,判断出来了当前线程中已经存在一个数据库连接a了, 表示当前线 程其实已经拥有一个Spring事务了,则进行挂起
ii. 挂起就是把ThreadLocal中的数据库连接a从ThreadLocal中移除,并放入一个挂起资源对象中
iii. 挂起完成后,再次利用事务管理器新建一个数据库连接b
iv. 将数据库连接b的autocommit改为false
v. 把数据库连接b设置到ThreadLocal中
vi. 执行insertTest()方法中的sql
vii. insertTest()方法执行完,则从ThreadLocal中拿到数据库连接b进行提交
viii. 提交之后会恢复所挂起的数据库连接a,这里的恢复,其实只是把在挂起资源对象中所保存的数据库连 接a再次设置到ThreadLocal中
6. insertUser()方法正常执行完,则从ThreadLocal中拿到数据库连接a进行提交
这个过程中最为核心的是:在执行某个方法时,判断当前是否已经存在一个事务,就是判断当前线程 的ThreadLocal中是否存在一个数据库连接对象,如果存在则表示已经存在一个事务了。再根据传播机制进行相应操作,如果是新事务则挂起旧事务
所以上面执行的结果是,insertUser提交成功(因为异常被捕获),insertTest回滚了,因为是两个事务,所以互不影响
执行结果如下图,可以看出一些关键字,如挂起,以及两个不同的数据库连接
PROPAGATION_REQUIRED_NESTD:嵌套事务
insertUser调用insertTest方法,insertTest方法报错,执行流程如下:
同样的,我们看一下大概执行流程:
1.首先,代理对象执行insertUser()方法前,先利用事务管理器新建一个数据库连接a
2. 将数据库连接a的autocommit改为false
3. 把数据库连接a设置到ThreadLocal中
4. 执行insertUser()方法中的sql
5. 执行insertUser()方法过程中,调用了insertTest()方法(注意用代理对象调用)
i. 代理对象执行insertTest()方法前,判断出来了当前线程中已经存在一个数据库 连接a了,表示当前线 程其实已经拥有一个Spring事务了,则使用这个事务
ii. 利用这个事务在数据库中创建savePoint
iii 执行insertTest()方法中的sql
6. insertUser()方法正常执行完,则从ThreadLocal中拿到数据库连接a进行提交
这里可以看出,嵌套事务实际上会复用已创建的事务,相当于一个子事务,那么它和PROPAGATION_REQUIRED有什么 区别呢?因为都是会复用前面创建的事务,那么它们的不同点就是,嵌套事务会创建一个savePoint,相当于一个回滚点,如果insertUser方法报错,会回滚到savePoint而不是全回滚也就是说insertTest()执行的内容是成功提交的了,但还需要注意的是,需要支持savePoint的数据库才行,不然的话和PROPAGATION_REQUIRED没区别
执行结果如下图,因为我用的oracle不支持,所以全回滚了
Spring事务传播机制小结
本篇主要讲解Spring事务的传播机制原理,围绕几个比较常用的传播方法进行解析,剩下没有介绍的可以自己再去分析,或者写个demo跑一下加深理解,然后我再回答一下开篇的思考题,传播机制的作用到底是什么?我个人观点是,事务是数据库的一种特性,而Spring只是封装这个特性,方便我们使用,最终的执行实际上都是在数据库中完成,但是对于数据库来说,事务是单一的,没有那么多业务场景,但是对于Spring来说,会面对各种各样的业务需求,所以需要有一套可以从代码层面去控制事务来满足我们的场景需求,所以也就有了传播机制。
本篇内容到此结束,有问题欢迎留言交流,最后附上Spring 创建事务的流程图,希望对你有所帮助,我是老道,只做最肝的干货分享,蟹蟹