type
status
date
slug
summary
tags
category
icon
password
事务的基本原理参考这篇文章 Imspring 源码介绍(五):事务实现,这篇直接介绍事务怎么使用。
1、声明式事务
1.1 @Transaction 使用
声明式事务的使用很简单,先在配置类上加上注解
@EnableTransactionManagement
。然后对需要事务支持的方法,加一个
@Transactional
注解。或者更简单一点,直接在Bean的
class
处加上,表示所有 public
方法都具有事务支持。1.2 @Transactional 深入
可以把
@Transactional
注解理解为一种特殊的 AspectJ 注解,原理类似于 @Before
、@After
这些注解。@Transactional
提供了丰富的配置项设置事务属性,源码如下:- value 和 transactionManager:它们两个是一样的意思。当配置了多个事务管理器时,可以使用该属性指定选择哪个事务管理
- propagation:事务的传播行为,默认值为
Propagation.REQUIRED
- isolation:事务的隔离级别,默认值为
Isolation.DEFAULT
- timeout:事务的超时时间,默认值为
-1(不设置)
。如果超过该时间限制但事务还没有完成,则自动回滚事务
- readOnly:指定事务是否为只读事务,默认值为
false
;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true
- rollbackFor:用于指定能够触发事务回滚的异常类型,可以指定多个异常类型。
- noRollbackFor:抛出指定的异常类型,不回滚事务,也可以指定多个异常类型。
下面详细说明一下这些字段怎么使用。
1.2.1 事务管理器
回忆一下事务的关键三要素:
- TransactionManager:事务管理器。一般使用
PlatformTransactionManager
。
- TransactionDefinition:事务定义信息(隔离,传播,超时,只读)
- TransactionStatus:事务具体运行状态
事务管理器(PlatformTransactionManager)在这里充当事务的执行器的角色,传入事务的定义信息(TransactionDefinition)开启事务,然后返回事务的执行状态(TransactionStatus)。
之所以要抽象出 PlatformTransactionManager 这些概念,目前是为了屏蔽底层数据库持久化框架的差异。Spring 不仅支持 JdbcTemplate、Mybatis 等常用的 ORM 框架,还支持 Spring Data JPA、Hibernate、JTA(分布式事务,Java Transaction API)等持久化框架。PlatformTransactionManager 只是个事务管理的标准接口,不同的持久化框架有各自的实现。我们在切换不同的持久化框架的时候,只需要修改注入 Spring 容器的事务管理器的实现类,就可以为每个持久化框架提供事务支持。
扩展:JPA 是什么?
JPA
, 即Java Persistence API
,是 JDK5.x 以后引入的基于 ORM 的持久化操作的标准规范,代码在javax.persistence
的包下面。Spring Data JPA
是 Spring 提供的简化JPA
开发的框架,可以理解为JPA
规范的再次封装抽象,它只是一个代码抽象层,底层实现依旧是Hibernate
等 ORM 框架。他们的关系是:
Spring 内部已经为不同的持久化框架提供了 PlatformTransactionManager 接口的实现类。
实现类 | 说明 |
org.springframework.jdbc.datasource. DataSourceTransactionManager | 使用 SpringJDBC 或 Mybatis 等基于 DataSource 数据源的持久化持久时,使用此事务管理器。最常用的事务管理器。 |
org.springframework.orm.jpa. JpaTransactionManager | 使用 JPA 进行持久化时,使用该事务管理器 |
org.springframework.orm.hibernateX. HibernateTransactionManager | 使用 HibernateX.0(X可为3,4,5) 进行持久化时,使用此事务管理器 |
org.springframework.orm.jdo. JdoTransactionManager | 使用 JDO 进行持久化时,使用此事务管理器 |
org.springframework.transaction.jta. JtaTransactionManager | 具有多个数据源的全局事务使用该事务管理器(不管采用何种持久化技术) |
如果 Spring 容器中只有一个 PlatformTransactionManager 实例,是不需要在
@Transactional
里面显式指定 PlatformTransactionManager 的。有一种使用场景是我们的项目只有一个数据源,但是使用了两种持久化框架(比如 Mybatis 和 JPA),然后在配置文件里面配置了两个 PlatformTransactionManager 的 Bean 实例。在使用
@Transactional
的时候,需要手动指定事务管理器。注意不支持多个数据源事务,如果有多数据源事务需求且使用 mybatis-plus 框架,可以参考使用 dynamic-datasource 框架,只需要一个@DS
注解就能实现多数据源切换,然后使用@DSTransactional
代替@Transactional
即可实现多数据源事务。
1.2.2 传播行为
事务本身并没有传播概念,所谓的”传播行为“其实是 Spring 为了方便开发者管理事务而引入的概念。我们调用一个方法的时候往往会涉及多个方法,形成一条调用方法链。传播行为定义了这条方法链里面【哪些方法需要加入事务?是加入一个已有的事务还是开启一个新的事务?】等内容。
Spring 定义了七种事务传播行为:
- REQUIRED:如果当前没有事务,则自己新建一个事务,如果当前存在事务,则加入这个事务。Spring 默认的事务的传播类型。
- SUPPORTS:当前存在事务,则加入当前事务,如果当前没有事务,就以非事务方法执行。
- MANDATORY:当前存在事务,则加入当前事务,如果当前事务不存在,则抛出异常。
- REQUIRES_NEW:创建一个新事务,如果存在当前事务,则挂起该事务。新事务和原事务是两个独立的事务。
- NOT_SUPPORTED:始终以非事务方式执行,如果当前存在事务,则挂起当前事务。
- NEVER:不使用事务,如果当前事务存在,则抛出异常。
- NESTED:如果当前事务存在,则在嵌套事务中执行,否则和 REQUIRED 一样开启一个事务。
常用的默认级别 REQUIRED 这里就不举例子了,这里举一个不太常见的例子。比如在下面两个方法,第一个没有使用事务,第二个设置传播行为 SUPPORTS。
由于 testA 没有声明事务,且 testB 的事务传播行为是 SUPPORTS,所以执行 testB 时就是没有事务的(如果当前没有事务,就以非事务方法执行),则在 testB 抛出异常时也不会发生回。最终结果就是 a1,b1 存入数据库,b2 没有存入数据库。
1.2.3 隔离级别
隔离级别这个概念是数据库事务自带的,定义了并发事务执行的过程,当前事务的执行结果对其他事务的可见性。Spring 定义了五种隔离级别:
- DEFAULT 用数据库的设置隔离级别,数据库设置的是什么我就用什么
- UNCOMMITTED:读未提交,最低隔离级别、事务未提交前,就可被其他事务读取
- COMMITTED:读已提交,一个事务提交后才能被其他事务读取到
- REPEATABLE_READ:可重复读,本事务会建立快照,在快照建立前其他事务提交的内容可见,快照后提交的内容不可见
- SERIALIZABLE:序列化,事务必须一个执行完才允许建下个事务,隔离级别最高
1.2.4 超时时间
超时时间的使用要注意:
- 单位是秒。
- timeout 的设置只适用于以下两种传播规则: REQUIRED 和 REQUIRES_NEW,其他的传播规则只允许超时默认值 -1 (即永不超时),否则会抛出异常。
- 这里的超时时间只是在 SQL 执行前判断下当前的执行时间是否超时,而不是整体执行时间是否超时。
比如
上面两个方法都是设置了超时时间为 2秒,但整体方法执行 3 秒以上。结果是 methodA 不会报错,methodB 会报错。
其原因是事务开启时,会读取当前时间 和 timeout 的值,并将其相加得到事务“过期时间”,而在每次执行 sql 前则会获取到 statement,此时才会判断当前时间是否已超期,超期才会抛出超时异常。也就是说,通俗的理解,只有在每次执行 SQL 前,才会判断下是否已超过事务限时。
1.2.5 是否只读
可以使用
@Transactional(readOnly = true)
来设置只读事务。大家是不是很好奇为什么查询操作也需要使用事务呢?这里分情况讨论:- 若一个事务里只发出一条 select 语句,则没有必要启用事务支持,数据库默认支持 sql 执行期间的读一致性。此时可以不加
@Transactional
注解。
- 若一个事务里先后发出了多条 select 语句,使用
@Transactional
表明这些 select 语句是一个整体事务。在 mysql 的RR
隔离级别下,普通的无锁 select 是镜像读,多次查询结果不会改变,所以能保证读一致性
(可重复读)。相反若不加@Transactional
注解,则多条select都是独立的事务。在前条 select 之后,后条 select 之前,数据被其他事务改变,则该次整体的查询将会出现读数据不一致的现象。此时需要添加@Transactional
注解。
这里的只读是对开发者的提示,并不意味着你标注只读就真的不能写更新语句。实际上你更新,出错后回滚等功能都还是正常的。甚至该标注也支持 SUPPORTS 传播规则,说明即使没有开启事务也无妨。Spring 的原文注释
This just serves as a hint for the actual transaction subsystem; it will not necessarily cause failure of write access attempts.A transaction manager which cannot interpret the read-only hint will not throw an exception when asked for a read-only transaction.return 这只是作为实际事务子系统的提示,它不一定会导致写访问尝试失败。不能理解只读提示的事务管理器在请求只读事务时也不会抛出异常
如果我们真的想开启只读事务,可以在配置 PlatformTransactionManager 的时候设置
setEnforceReadOnly(true)
,Spring 会自动进行只读事务的优化。1.2.6 回滚的异常类型
指定什么异常需要回滚,什么异常不需要回滚。默认情况下,事务只在 RuntimeException 和 Error 及其子类异常类型上回滚。
如果
@Transactional
定义的回滚异常类型里面没有包含当前发生异常的类型,我们又想进行事务回滚那怎么办呢?其实还是有办法的,使用 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
。例如1.3 @Transactional 失效的七个场景
- 未启用 Spring 事务管理功能:
@EnableTransactionManagement
注解用来启用 Spring 事务自动管理事务的功能。
- 方法不是 public 类型的:
@Transaction
用在了非 public 方法上,事务将无效。
- 数据源未配置事务管理器:Spring 是通过事务管理器
TransactionManager
来管理事务,每个数据源都要配置。
- 自身调用问题:
@Transactional
基于 AOP 实现事务功能,同类方法调用会导致事务失效,只有在代理对象之间进行调用时,才会触发切面逻辑。可以在类中注入该类代理对象,然后在嵌套的方法中调用代理对象的方法。
- 异常类型错误:默认情况下,发生
RuntimeException
和Error
类型及子类的异常,Spring 事务才会回滚。可以自定义回滚的异常类型,@Transactional(rollbackFor = {异常类型列表})
。
- 异常被吞:在方法中 catch 异常后没有向上抛出。
- 业务和 Spring 事务代码不在一个线程:Spring 事务实现中使用了 ThreadLocal,要求业务代码和 Spring 事务的代码必须在一个线程中,才会受 Spring 事务的控制。
2、编程式事务
和编程式事务相比,声明式事务不足地方是,它的最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。编程式事务有三种方式:
- 使用
TransactionTemplate
(推荐)
- 使用
PlatformTransactionManager
- 原生方式
2.1 使用 TransactionTemplate
在配置类配置
TransactionTemplate
的 Bean 实例(这步可跳过) 然后使用
TransactionTemplate
2.2 使用 PlatformTransactionManager
PlatformTransactionManager
实例已经被自动注入 Spring 容器了,可以直接使用2.3 原生方式
3、事务回滚
3.1 手动回滚事务
3.1.1 @Transactional完全回滚
手动回滚事务一定要先开启事务(例如加上@Transactional
),不然会报以下错误:
3.1.2 TransactionTemplate完全回滚
3.2 Savepoint回滚
嵌套事务是指一个事务内部包含了另一个或多个子事务的概念。在嵌套事务中,外部事务被称为父事务,内部事务被称为子事务。父事务可以包含多个子事务,并且父事务和子事务之间存在层级关系。
- 不同层级的事务注解:父事务和子事务使用不同的事务传播属性时,可能会导致事务失效。如果父事务的传播属性为 REQUIRED,而子事务的传播属性为 REQUIRES_NEW,那么子事务将独立于父事务,并且父事务的回滚不会影响子事务。
- 外部方法调用:如果嵌套事务中的子事务是通过外部方法调用的,而外部方法没有事务注解,那么子事务将独立于父事务,在外部方法调用结束后才提交或回滚子事务。这种情况下,嵌套事务的特性可能无法生效。
嵌套事务的核心实现原理是依赖 JDBC 的
savepoint
机制,因为嵌套事务是在一个事务中启动另一个子事务。实际上,在嵌套事务中每一个事务都有自己的保存点,如果子事务失败或者回滚,它可以回滚到自己的保存点,而不会影响其他的事务。在 Springboot 中,Spring 会使用 JDBC 来创建一个事务,并将其关联到当前线程中,当方法执行到嵌套事务的点时,Spring 会使用 JDBC 的 SavePoint
机制来创建一个保存点(savePoint
),并将其记录在当前事务的状态中。- 默认情况下,我们创建了一个数据的连接,会运行在自动提交模式下,这意味着,任何时候我们执行了一条SQL之后,事务就会自动提交,基于事务的持久性,当一个事务提交后,数据会被持久化到数据库中。那么如果我们想让一组SQL语句成为事务的一部分,那么我们就可以选择让所有的语句在运行成功的时候再提交,并且如果出现任何异常,这些语句作为事务的一部分,其是可以全部回滚的。
- 有时候一个事务可能是一组复杂的语句,因为可能想要回滚到事务中某一个特殊的点,便可以通过
JDBC Savepoint
帮助我们创建保存点(savepoint
),这样就可以回滚到指定的点。当事务提交或者整个事务回滚之后,为事务创建的的任何保存点都会释放并变为无效。同时把事务回滚到一个保存点,会使其他所有的保存点自动释放为无效。
3.2.1 @Transactional回滚Savepoint
3.2.2 TransactionTemplate回滚Savepoint
- Author:mcbilla
- URL:http://mcbilla.com/article/201c31d6-01ef-442f-bff0-54e07fa556ac
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!
Relate Posts