Spring事务,Spring事务的10种失效场景.加入型传播和嵌套型传播有什么区别
基础知识:Spring 两种事务管理方式
Spring 支持两种事务管理方式:编程式事务和声明式事务。
事务分为 编程式事务 和声明式事务两种。
编程式事务指在代码中手动的管理事务的提交、回滚等操作,代码侵入性比较强。
声明式事务是通过配置来实现的,不需要在代码中显式地管理事务。
编程式事务是指在代码中显式地开启、提交或回滚事务。这种方式需要在代码中编写事务管理的相关逻辑,比较繁琐,但是灵活性较高,可以根据具体的业务需要进行定制。
声明式事务是通过配置来实现的,不需要在代码中显式地管理事务。这种方式需要在配置文件中声明事务的属性,比如事务的传播行为、隔离级别等。声明式事务的好处是可以将事务管理的逻辑与业务逻辑分离,使得代码更加简洁、清晰,同时也方便了事务管理的统一配置和维护。
在 Spring 中,声明式事务 是基于 AOP 面向切面的,它将具体业务与事务处理部分解耦,代码侵入性很低,声明式事务也有两种实现方式。
Spring 提供了两种声明式事务的方式:
基于 XML 配置
基于注解配置。
基于 XML 配置的方式需要在 Spring 配置文件中声明事务管理器和事务通知等相关信息,
而基于注解配置的方式则可以在代码中通过注解来声明事务的属性,比如 @Transactional。一种是基于 TX 和 AOP 的 xml 配置文件方式,二种就是基于 @Transactional 注解了,实际开发中 @Transactional 用的比较多。
声明式事务1:基于 XML 配置文件进行配置
|
声明式事务2:基于注解的声明式配置
一般来说,更加推荐声明式事务比编程式事务,因为它可以使代码更加简洁、清晰,同时也方便了事务管理的统一配置和维护。
所以,这里使用 声明式事务 进行演示,并且是使用 基于注解配置的 声明式事务。
首先必须要添加 @EnableTransactionManagement 注解,保证事务注解生效
|
其次,在方法上添加 @Transactional 代表注解生效
|
下面的案例,用到基于注解的声明式配置,具体的注解是 @Transactional。
@Transactional 注解的使用
@Transactional 可以作用在类上,当作用在类上的时候,表示所有该类的 public 方法都配置相同的事务属性信息。
@Transactional 也可以作用在方法上,当方法上也配置了 @Transactional,方法的事务会覆盖类的事务配置信息。
我们日常操作里,对于单个方法使用事物,经常是这样:
|
或者说配合手动回滚使用,是这样:
|
以上都是单个事物方法,理解起来很简单,相信大多数场景大家就这么用一下就没有过多去理会了。
首先,我们通过看 @Transactional 的源码来和大家重新认识一下 @Transactional 的用法。
@Transactional 注解 涉及到的 5大属性
总体来说,事务属性包含了5个方面,如图所示:
@Transactional 源码
transactionManager 和 value 是同一个配置项的两个别名:
大多数项目只需要一个事务管理器,但是在有些项目中为了提高效率、或者有多个完全不同又不相干的数据源,所以会有多个事务管理器,这里填的就是你想用的事务管理器的 Bean 的 id。
propagation属性: Spring 事务传播机制是指,包含多个事务的方法在相互调用时,事务是如何在这些方法间传播的。
这个后面详细介绍。
isolation属性: 是事务的隔离级别,默认值为 Isolation.DEFAULT。
这里有四个隔离级别,具体这四个级别是什么意思 :
Isolation.DEFAULT:使用底层数据库默认的隔离级别。
Isolation.READ_UNCOMMITTED
Isolation.READ_COMMITTED
Isolation.REPEATABLE_READ
Isolation.SERIALIZABLE
在Innodb里面默认用的是 RR 级别,
timeout属性: 事务的超时时间,默认值为-1。如果超过该时间限制但事务还没有完成,则自动回滚事务。
readOnly属性 : 指定事务是否为只读事务,默认值为false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true。
rollbackFor属性 : 用于指定能够触发事务回滚的 异常 类型,可以指定多个异常类型。
第一大属性:@Transactional 注解 的 传播机制
什么叫做事务的传播?
Spring 事务传播机制是指,包含多个事务的方法在相互调用时,事务是如何在这些方法间传播的。
给大家举个 生动的例子.
比如,有三个 业务 方法,第一个业务 方法如下:
|
第二个业务 方法如下:
|
然后第三个 业务 方法如下:
|
那么, 三个 业务 方法 之间:
是每一个 业务方法开启一个 新的独立的事务?
还是 第一个 业务 方法、 第二个 业务 方法 加入到 第三个 业务 方法 开启的事务?
还是 第一个 业务 方法、 第二个 业务 方法 各自开一个 NESTED 内嵌事务, 以局部事务的 加入到 第三个 业务 方法 开启的整体事务?
Spring定义了七种传播行为
使用spring声明式事务,自动在方法调用之前 (进入一个新的方法),spring会根据事务属性去决定是否开一个事务,并在方法执行之后,决定事务提交或回滚事务。这就是事务的传播。
Spring定义了七种传播行为:
传播行为 | 含义 |
---|---|
PROPAGATION_REQUIRED | 表示当前方法必须运行在事务中。如果当前事务存在,方法将会在该事务中运行。否则,会启动一个新的事务 |
PROPAGATION_SUPPORTS | 表示当前方法不需要事务上下文,但是如果存在当前事务的话,那么该方法会在这个事务中运行 |
PROPAGATION_MANDATORY | 表示该方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常 |
PROPAGATION_REQUIRED_NEW | 表示当前方法必须运行在它自己的事务中。一个新的事务将被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager |
PROPAGATION_NOT_SUPPORTED | 表示该方法不应该运行在事务中。如果存在当前事务,在该方法运行期间,当前事务将被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager |
PROPAGATION_NEVER | 表示当前方法不应该运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常 |
PROPAGATION_NESTED | 表示如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独地提交或回滚。如果当前事务不存在,那么其行为与PROPAGATION_REQUIRED一样。注意各厂商对这种传播行为的支持是有所差异的。可以参考资源管理器的文档来确认它们是否支持嵌套事务 |
事务7种传播机制 对应的源码如下:
Spring 事务传播机制分为 3 大类,总共 7 种级别,如下图所示:
当我们不指定的时候, 默认使用的是 Propagation.REQUIRED。
1.1 支持当前事务 的三种传播方式
支持当前事务的传播机制有三种,分别是
- 第一种传播: 加入当前事务 REQUIRED
所谓的加入当前事务,是指如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
所谓 当前事务 ,其实是用词 稍有有点错误, 其实 指的是 上一层方法的事务 。
含义:如果上一层方法 已经存在一个事务中,则加入到这个事务中; 如果上一层 方法没有事务,当前层方法 新建一个事务 。
REQUIRED 加入当前事务 , 这是 默认的 传播机制。
- 第二种 传播: 支持当前事务 SUPPORTS
支持一下当前 事务,是指如果当前存在事务,则加入该事务;如果当前没有事务, 就以非事务方式执行
所谓 当前事务 ,其实是用词 稍有有点错误, 其实 指的是 上一层方法的事务 。
含义:支持上一层 方法的 事务,如果上一层 方法没有事务, 那么,当前层方法 就以非事务方式执行
- 第三种 传播: MANDATORY 强制当前事务
强制一下当前 事务,是指如果当前存在事务,则加入该事务;如果当前没有事务, 就抛出 异常 。
含义:如果 上一层 方法 没事务,那么,当前层方法 就抛出 异常 。
1.2 不支持当前事务的三种传播方式
- 第4种 传播: REQUIRES_NEW
含义:新建事务,如果当前存在事务,把当前事务挂起。
- 第5种 传播: NOT_SUPPORTED
含义:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
- 第6种 传播: NEVER
含义: 以非事务方式执行,如果当前存在事务,则抛出异常。
1.3 NESTED 事务嵌套
- 第7种 传播: NESTED 事务嵌套
含义: 如果当前存在事务,则在嵌套事务内执行。
如果当前没有事务,则执行与 REQUIRED类似的操作, 创建一个新的事务。
NESTED事务嵌套和 加入事务(REQUIRED)的主要区别在于 :
NESTED事务 的特点如下 :
当存在外部事务时,NESTED会创建一个嵌套的子事务,这个子事务有自己的保存点(savepoint)。
如果嵌套事务中发生异常,它只会回滚到自己的保存点(savepoint),而不影响外部事务。
因此, NESTED事务可以实现部分事务的回滚,或者说 子事务部分回滚( 只有嵌套事务内的部分操作会被回滚),而外部事务的其他部分可以继续执行。
加入事务(REQUIRED)的特点如下 :
如果当前存在事务,则REQUIRED会加入到当前事务中,作为当前事务的一部分;
如果当前没有事务,则创建一个新的事务。
在REQUIRED传播级别下,如果遇到异常,整个事务(外部事务,包括嵌套之前的所有操作)将会回滚。
总结来说:
NESTED事务允许在当前事务中创建一个新的子事务,这个子事务可以独立于外部事务进行回滚
而REQUIRED事务则会与外部事务一形成一个整体,同生共死,一起回滚。
NESTED事务通过保存点(savepoint)实现部分回滚,而REQUIRED事务则是整个事务的回滚。
默认的传播行为:加入当前事务 REQUIRED
除了Propagation.REQUIRED, 另外两个常用的是 Propagation.REQUIRES_NEW 和 Propagation.NESTED。
除了这个三个, 而另外四种我们基本是不会去使用的,所以小伙伴也没必要去了解。
看代码,默认啥都不指定的时候,我们使用的就是PROPAGATION_REQUIRED这种方式。
那么接下来就是关于 这种默认的事物传播机制 PROPAGATION_REQUIRED 我们需要关心的东西了。
前面介绍了 加入当前事务 REQUIRED 的传播行为:
是指如果当前存在事务,则加入该事务
如果当前没有事务,则创建一个新的事务。
假设,第一个业务类里面的方法 使用了 声明式事务 :
|
假设, 第二个业务类里面的方法,也使用了声明式事务:
|
然后第三个业务类里面的方法没有使用声明式事务,去调用第一个和第二个,如:
|
在testThree方法(对于addOne 和 addTwo 来说是个外部方法)上同样使用声明式事物,且也是默认指定传播机制PROPAGATION_REQUIRED。
默认指定传播机制PROPAGATION_REQUIRED , testThree 让testOne,testTwo 都加入到一个事务里面。
这样addOne事物开启时,发现外部存在指定传播机制PROPAGATION_REQUIRED的事物,那么就会加入该事物;
同样addTwo同理。
第二大属性:@Transactional 注解的 隔离属性
数据库有自己的隔离级别的定义,Spring也有自己的 隔离级别的定义
Spring中的隔离级别
Spring事务由 Transactional 注解实现,隔离级别由它的参数 isolation 控制,Isolation 的 Eum 类中定义了“五个”表示隔离级别的值,如下。
隔离级别 | 含义 |
---|---|
ISOLATION_DEFAULT | 使用后端数据库默认的隔离级别 |
ISOLATION_READ_UNCOMMITTED | 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读 |
ISOLATION_READ_COMMITTED | 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生 |
ISOLATION_REPEATABLE_READ | 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生 |
ISOLATION_SERIALIZABLE | 最高的隔离级别,完全服从ACID的隔离级别,确保阻止脏读、不可重复读以及幻读,也是最慢的事务隔离级别,因为它通常是通过完全锁定事务相关的数据库表来实现的 |
Spring中的隔离级别 和数据一致性问题的 关系:
Isolation的值与隔离级别 | 隔离级别的值 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
Isolation.DEFAULT | 0 | - | - | - |
Isolation.READ_UNCOMMITTED | 1 | √ | √ | √ |
Isolation.READ_COMMITTED | 2 | × | √ | √ |
Isolation.REPEATABLE_READ | 4 | × | × | √ |
Isolation.SERIALIZABLE | 8 | × | × | × |
数据库隔离级别
隔离级别 | 隔离级别的值 | 导致的问题 |
---|---|---|
Read-Uncommitted | 0 | 导致脏读 |
Read-Committed | 1 | 避免脏读,允许不可重复读和幻读 |
Repeatable-Read | 2 | 避免脏读,不可重复读,允许幻读 |
Serializable | 3 | 串行化读,事务只能一个一个执行,避免了脏读、不可重复读、幻读。执行效率慢,使用时慎重 |
MySQL 默认为 RR :PEATABLE_READ;
Oracle,sql server 默认为 RC:READ_COMMITTED;
READ_UNCOMMITTED 由于隔离级别较低,通常不会被使用。
数据库隔离级别 和数据一致性问题 的 关系:
隔离级别 | 隔离级别的值 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
Read uncommitted(未提交读) | 0 | √ | √ | √ |
Read committed(已提交读) | 1 | × | √ | √ |
Repeatable read(可重复读) | 2 | × | × | √ |
Serializable(可串行化) | 3 | × | × | × |
Spring事务的隔离级别与 数据库隔离级别的关系:
Spring默认的隔离级别, 是 Isolation.DEFAULT。
它的含义是:使用数据库默认的事务隔离级别。
除此之外,另外Spring事务的隔离级别 四个与 JDBC 的隔离级别是相对应的,那个四个 Spring事务隔离级别,其实是在数据库隔离级别之上又进一步进行了封装。
如果 Spring事务的隔离级别与 数据库隔离级别的不一致会怎样?
以Spring事务为准的。
Spring 事务管理涉及到了与数据库的交互 。
JDBC 加载的流程 有四步:注册驱动,建立连接,发起请求,输出结果, 伪代码如下:
|
在创建连接阶段,JDBC 从数据库获取一个连接 Connection 对象
Connection 对象不仅有连接数据库的方法,还有设置当前连接的事物隔离级别的方法, 源码如下:
|
该方法的注释说明:尝试将此连接对象的事务隔离级别更改为给定的级别,如果在事务期间调用此方法,则结果由实现定义。
所以,如果spring与数据库事务隔离级别不一致时,spring 会调用类似的方法, 设置 一下 当前链接的 事务隔离级别。
第三大属性:@Transactional 注解的 readOnly属性
@Transactional
注解的readOnly 属性用于指定事务是否为只读事务。
当readOnly
属性设置为true
时,表示该事务只涉及读取数据, 而不进行任何写操作(如INSERT、UPDATE、DELETE等)。这有助于数据库引擎优化事务处理,因为它知道不需要考虑事务的并发写操作。
当使用 @Transaction 注解时,可以通过设置 readOnly=true
来指定这是一个只读事务,这样在事务执行期间就不会对数据进行修改,只会进行查询操作。
以下是一个使用 @Transaction 只读示例的代码片段:
|
在上面的示例中,getUserById
方法被标记为只读事务,因此在执行期间只会进行查询操作。如果在方法中尝试进行修改操作,将会抛出异常。
从数据库层面来讲,设置readOnly = true
会向数据库发送一个信号,告诉数据库这个事务是只读的。
不同的数据库会根据这个提示进行优化。例如
在一些数据库中,对于只读事务,数据库可以避免获取写锁,减少锁竞争,从而提高并发读取性能。
同时,数据库也可能会跳过一些与写操作相关的日志记录和事务处理逻辑,提高事务执行的效率。
第四大属性:@Transactional 注解的 rollbackFor 回滚规则属性
事务五边形的rollbackFor 回滚规则属性 , 定义了哪些异常会导致事务回滚,而哪些异常不会。
下面是一个简单的 Java 代码示例,演示了 @Transactional
回滚规则属性。
首先是 不做配置,使用 rollbackFor 的默认值:
|
在上面的示例中, 如果在方法执行过程中发生异常,事务会自动回滚,保证数据的一致性。
如果用户创建失败,createUser
方法会抛出一个 RuntimeException
异常,这会导致事务回滚,用户创建操作会被撤销。
提示,@Transactional 使用有很多的 约束:
约束1 :
@Transactional
注解只能应用于公共方法,因为只有公共方法才能被代理,从而实现事务管理。约束2 :默认情况下,
@Transactional
注解 只对非受检 异常进行回滚,而对受检查异常不进行回滚。
非检查型异常 (Unchecked Exception/非受检查异常)的是程序在编译时不会提示需要处理该异常,而是在运行时才会出现异常, 如 RuntimeException。
检查型异常(Checked Exception)是指在 Java 中,编译器会强制要求对可能会抛出这些异常的代码进行异常处理,否则代码将无法通过编译。
一般来说,在编写代码时应该尽量避免抛出非检查型异常(如 RuntimeException),因为这些异常的发生通常意味着程序存在严重的逻辑问题。
如果是受检 异常(Checked Exception), 进行回滚,可以在 @Transactional
注解中指定 rollbackFor
属性,例如
|
掌握了 @Transactional 的几个核心属性, 最后我们来说下 @Transactional 的失效场景。
Spring事务 的10种 失效场景
Spring事务管理 是Java应用中确保数据库操作一致性和完整性的关键机制之一。
然而,在实际开发中,有时候会遇到Spring事务失效的情况,导致期望的事务行为无法正常发生。
本文将深入探讨九种常见的导致Spring事务失效的场景,帮助开发者更好地理解事务管理的细节和注意事项。
场景1:非Spring容器管理的 事务方法
Spring事务是通过AOP(面向切面编程)来实现的,如果一个事务注解被应用到一个普通的Java类的方法上,并且该类不是通过Spring容器进行管理的,那么事务将不会生效。
因为Spring无法拦截并管理这个类的方法调用。
示例:
|
在上述示例中,如果TransactionalService
不是通过Spring容器进行管理,那么@Transactional
注解将不会生效。
场景2: 在非公有方法上使用事务
Spring事务默认只对公有方法上的事务注解生效。@Transactional 应用在非 public 修饰的方法上,@Transactional 将会失效。
如果在一个非公有方法上使用事务注解,事务将不会生效。
示例:
|
在上述示例中,performTransaction
是一个私有方法,事务注解不会生效。
protected、private 修饰的方法上使用 @Transactional 注解,虽然事务无效,但不会有任何报错,这是我们很容犯错的一点。
场景3:异常被捕获 而不是 抛出
有时候,开发者可能选择捕获掉一个异常,而不重新抛出或处理。
这样的做法将导致事务失效,因为Spring事务管理依赖于异常来判断是否需要回滚事务。
示例:
|
在上述示例中,异常被捕获但未重新抛出或处理,导致事务失效。
场景4: 对 受检查异常进行 异常拦截
默认情况下, Spring事务只对RuntimeException
(非受检查异常)及其子类进行回滚。
如果一个受事务管理的方法抛出了 受检查异常(如Exception), 默认情况下,事务将不会回滚。
示例:
|
在上述示例中,抛出了一个 受检查异常, 导致事务失效。
如果是 对 受检查异常进行捕获, 需要使用 rollbackFor 定制回滚 规则:
|
场景5:方法内部调用导致的事务失效
Spring事务默认只对外部方法调用进行代理,对于同一个类的内部方法调用是无法触发事务的。
如果在一个事务方法内部调用另一个方法,而这个被调用的方法上标注了@Transactional
注解,事务将不会生效。
示例:
|
在上述示例中,outerTransaction
方法内部调用了innerTransaction
方法,但由于默认只对外部方法调用进行代理,导致innerTransaction
方法上的事务失效。
场景6: 方法自调用导致的事务失效
类似于内部方法调用,如果一个事务方法内部自己调用自己,事务同样会失效。
这是因为Spring使用代理机制来管理事务,自调用会绕过代理对象,导致事务不生效。
示例:
|
在上述示例中,selfInvokingTransaction
方法内部自己调用了自己,导致事务失效。
场景7: 在同一个类中,一个非事务方法调用另一个事务方法
当在同一个类中,一个非事务方法调用了另一个事务方法时,事务将不会生效。
这是因为Spring默认使用动态代理来管理事务,而动态代理只能拦截外部调用。
示例:
|
在上述示例中,nonTransactionMethodA
调用了transactionMethodB
,但事务不会生效。
场景8: 使用错误的事务传播行为
Spring事务提供了不同的传播行为,如REQUIRED
、REQUIRES_NEW
等。
使用错误的传播行为可能导致事务失效,因为传播行为决定了事务如何在方法调用链中传播。
示例:
|
在上述示例中,如果使用了错误的传播行为,可能会导致事务失效。
场景9: 数据库引擎不支持事务
数据库引擎不支持事务,Spring事务 失效。
这一点很简单,myisam 引擎是不支持事务的,innodb 引擎支持事务。
场景10:数据源没有配置事务管理器
数据源没有配置事务管理器,这个也很简单,要使用事务肯定要配事务管理器。
Hibernate 用的是HibernateTransactionManager,
JDBC 和 Mybatis 用的是 DataSourceTransactionManager。
如果数据源没有配置事务管理器 ,Spring事务 失效。
Spring事务 的10种 失效场景总结
开发者应当牢记这些场景,并在开发过程中注意避免出现事务失效的情况,以确保数据的一致性和完整性。