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 提供了丰富的配置项设置事务属性,源码如下:
  • valuetransactionManager:它们两个是一样的意思。当配置了多个事务管理器时,可以使用该属性指定选择哪个事务管理
  • 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、HibernateJTA(分布式事务,Java Transaction API)等持久化框架。PlatformTransactionManager 只是个事务管理的标准接口,不同的持久化框架有各自的实现。我们在切换不同的持久化框架的时候,只需要修改注入 Spring 容器的事务管理器的实现类,就可以为每个持久化框架提供事务支持。
扩展:JPA 是什么?
JPA, 即Java Persistence API,是 JDK5.x 以后引入的基于 ORM 的持久化操作的标准规范,代码在 javax.persistence 的包下面。
Spring Data JPA 是 Spring 提供的简化JPA开发的框架,可以理解为JPA规范的再次封装抽象,它只是一个代码抽象层,底层实现依旧是 Hibernate 等 ORM 框架。
他们的关系是:
notion image
 
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 失效的七个场景

  1. 未启用 Spring 事务管理功能:@EnableTransactionManagement 注解用来启用 Spring 事务自动管理事务的功能。
  1. 方法不是 public 类型的:@Transaction 用在了非 public 方法上,事务将无效。
  1. 数据源未配置事务管理器:Spring 是通过事务管理器 TransactionManager 来管理事务,每个数据源都要配置。
  1. 自身调用问题:@Transactional 基于 AOP 实现事务功能,同类方法调用会导致事务失效,只有在代理对象之间进行调用时,才会触发切面逻辑。可以在类中注入该类代理对象,然后在嵌套的方法中调用代理对象的方法。
  1. 异常类型错误:默认情况下,发生 RuntimeExceptionError 类型及子类的异常,Spring 事务才会回滚。可以自定义回滚的异常类型,@Transactional(rollbackFor = {异常类型列表})
  1. 异常被吞:在方法中 catch 异常后没有向上抛出。
  1. 业务和 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回滚

嵌套事务是指一个事务内部包含了另一个或多个子事务的概念。在嵌套事务中,外部事务被称为父事务,内部事务被称为子事务。父事务可以包含多个子事务,并且父事务和子事务之间存在层级关系。
  1. 不同层级的事务注解:父事务和子事务使用不同的事务传播属性时,可能会导致事务失效。如果父事务的传播属性为 REQUIRED,而子事务的传播属性为 REQUIRES_NEW,那么子事务将独立于父事务,并且父事务的回滚不会影响子事务。
  1. 外部方法调用:如果嵌套事务中的子事务是通过外部方法调用的,而外部方法没有事务注解,那么子事务将独立于父事务,在外部方法调用结束后才提交或回滚子事务。这种情况下,嵌套事务的特性可能无法生效。
嵌套事务的核心实现原理是依赖 JDBC 的 savepoint 机制,因为嵌套事务是在一个事务中启动另一个子事务。实际上,在嵌套事务中每一个事务都有自己的保存点,如果子事务失败或者回滚,它可以回滚到自己的保存点,而不会影响其他的事务。在 Springboot 中,Spring 会使用 JDBC 来创建一个事务,并将其关联到当前线程中,当方法执行到嵌套事务的点时,Spring 会使用 JDBC 的 SavePoint 机制来创建一个保存点(savePoint),并将其记录在当前事务的状态中。
  1. 默认情况下,我们创建了一个数据的连接,会运行在自动提交模式下,这意味着,任何时候我们执行了一条SQL之后,事务就会自动提交,基于事务的持久性,当一个事务提交后,数据会被持久化到数据库中。那么如果我们想让一组SQL语句成为事务的一部分,那么我们就可以选择让所有的语句在运行成功的时候再提交,并且如果出现任何异常,这些语句作为事务的一部分,其是可以全部回滚的。
  1. 有时候一个事务可能是一组复杂的语句,因为可能想要回滚到事务中某一个特殊的点,便可以通过 JDBC Savepoint 帮助我们创建保存点(savepoint),这样就可以回滚到指定的点。当事务提交或者整个事务回滚之后,为事务创建的的任何保存点都会释放并变为无效。同时把事务回滚到一个保存点,会使其他所有的保存点自动释放为无效。

3.2.1 @Transactional回滚Savepoint

3.2.2 TransactionTemplate回滚Savepoint

Spring系列:使用SpEL表达式Spring系列:使用JdbcTemplate
mcbilla
mcbilla
一个普通的干饭人🍚
Announcement
type
status
date
slug
summary
tags
category
icon
password
🎉欢迎来到飙戈的博客🎉
-- 感谢您的支持 ---
👏欢迎学习交流👏