拿下 Spring 事务
什么是事务
事务是数据库操作的最基本单元,是逻辑上的一组操作,要么都成功,要么都失败。是一个不可分割的工作单元。
事务的使用
事务具有 4 个特性:原子性、一致性、隔离性】持久性,简称为 ACID 特性。
- 原子性(Atomicity):一个事务是一个不可分割的工作单位,一个事务中包括的操作要么都成功要么都失败。
- 一致性(Consistency):事务必须保证数据库从一个一致性状态变到另一个一致性状态。比如转账的总金额,不能转着转着总金额少了或者多了。大部分一致性的需求需要程序员写业务代码保证。
- 隔离性(Isolation):一个事务的执行不能被其它事务干扰,即一个事务内部的操作及使用的数据对并发的其它事务是隔离的,并发执行的各个事务之间不能互相打扰。
- 持久性(Durability):持久性也称为永久性,指一个事务一旦提交,它对数据库中数据的改变就是永久性的,后面的其它操作和故障都不应该对其有任何影响。
为什么要用事务
举例:银行转账。小明给小红转 100 元。小明需要减少余额 100,小红需要增加余额 100。这是两个操作,需要一起成功。如果在小明转账成功之后发生了异常,就会出现小明 减 100 余额,但是小红并没有加 100 余额。就会造成钱丢失的情况。这是绝对不允许的。伪代码如下:
事务管理方式
Spring 支持 2 种事务管理方式。
- 编程式事务管理 编程式事务管理是通过编写代码实现的事务管理。可以根据需求规定事务从哪里开始,到哪里结束,拥有很高的灵活性。但是这种方式,会使业务代码与事务规则高度耦合,难以维护,因此我们很少使用这种方式对事务进行管理。所以,本文给大家介绍的是如何使用声明式事务管理。
- 声明式 声明式事务管理可以通过 2 种方式实现,分别是 XML和注解方式。Spring 在进行声明式事务管理时,底层使用了 AOP 。
事务管理器
Spring 并不会直接管理事务,而是通过事务管理器对事务进行管理的。
PlatformTransactionManager
Spring 提供了一个 PlatformTransactionManager 接口,这个接口被称为 Spring 的事务管理器,其源码如下:
该接口的源码很简单。这个接口针对不同的框架提供了不同的实现类,如下:
实现类 | 说明 |
org.springframework.jdbc.datasource.DataSourceTransactionManager | 提供给 Spring JDBC 、MBatis 的事务管理器 |
org.springframework.orm.hibernate5.HibernateTransactionManager | 提供给 Hibernate 的事务管理器 |
org.springframework.orm.jpa.JpaTransactionManager | 提供给 JPA 的事务管理器 |
org.springframework.jdo.JdoTransactionManager | 提供给 Jdo 的事务管理器 |
org.springframework.transaction.jta.JtaTransactionManager | 提供给 JTA 的事务管理器 |
注意:这些实现类,需要导入对应的依赖才能看到。 该接口中还有两个对象,分别是 TransactionDefinition 和 TransactionStatus。
TransactionDefinition
- TransactionDefinition:事务定义,定义了事务的名称,传播属性,事务隔离级别,是否只读,超时时间。
- PROPAGATION_** 0 ~ 7 代表的是事务传播行为
- ISOLATION_** -1 ~ 8 代表的是事务的隔离级别
- TIMEOUT_DEFAULT 默认的超时时间,-1,代表使用数据库的超时时间
- getPropagationBehavior:获取事务的传播行为,默认为
PROPAGATION_REQUIRED
- getIsolationLevel:获取事务的隔离级别,默认为所使用数据库的隔离级别
- getTimeout:获取事务的超时时间
- isReadOnly:事务是否只读
- getName:获取事务的名称
TransactionStatus
- TransactionStatus:事务状态,保存了事务执行过程中的状态。
方法说明如下:
名称 | 说明 |
hasSavepoint | 事务内部是否带有保存点 |
flush | 刷新事务 |
- TransactionExecution
方法说明如下:
名称 | 说明 |
isNewTransaction | 当前事务是否是新的 |
setRollbackOnly | 设置事务回滚 |
isRollbackOnly | 事务是否已被标记为回滚 |
isCompleted | 事务是否完成,即是否已经提交或回滚 |
- SavepointManager
方法说明如下:
名称 | 说明 |
createSavepoint | 创建保存点 |
rollbackToSavepoint | 回滚到给定的保存点 |
releaseSavepoint | 释放给定的保存点 |
有一个默认的抽象实现 AbstractTransactionStatus
,对 TransactionExecution、savepoint、SavepointManager 有具体的实现逻辑,代码有点多,就不贴了,但是非常好理解。 对 TransactionExecution、savepoint、SavepointManager 有具体的实现逻辑,代码有点多,就不贴了,但是非常好理解。 DefaultTransactionStatus 又继承了 AbstractTransactionStatus,继续进行扩充。
事务传播行为
事务传播行为指的是,多事务方法之间进行调用时,这个过程中事务应该如何进行管理。例如,事务方法 A 在调用事务方法 B 时,B 方法是在调用者 A 方法的事务中运行呢,还是为自己开启一个新事务运行,这就是由事务方法 B 的事务传播行为决定的。
事务方法:能让数据库表数据发生改变的方法,例如新增、删除、修改数据的方法。
行为 | 说明 |
REQUIRED | 如果有事务在运行,当前的方法就在这个事务内运行,否则,就启动一个新的事务,并在自己的事务内运行 |
SUPPORTS | 如果有事务在运行,当前的方法就在这个事务内运行;如果当前没有事务,则以非事务的方式运行。 |
MANDATORY | 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。 |
REQUIRES_NEW | 当前的方法必须启动新事务,并在它自己的事务内运行,如果有事务正在运行,应该将它挂起 |
NOT_SUPPORTED | 以非事务方式运行,如果当前存在事务,则把当前事务挂起。 |
NEVER | 以非事务方式运行,如果当前存在事务,则抛出异常。 |
NESTED | 如果当前存在事务,则创建一个新事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 REQUIRED。 |
根据上面的描述,我们可以将行为分为三大类。
- 不要事务:NEVER、NOT_SUPPORTED。
- 如果有则用:SUPPORTS
- 必须使用事务:REQUIRED、REQUIRES_NEW、NESTED、MANDATORY
隔离级别
事务有一个特性为隔离性,多事务操作之间不会产生影响。但如果不考虑隔离性,则会产生三个读问题:脏读、不可重复读、虚(幻)读。
- 脏读:一个未提交事务读取到另一个未提交事务的数据
- 不可重复读:一个未提交事务读取到另一个提交事务修改的数据
- 虚(幻)读:一个未提交事务读取到另一提交事务添加的数据 那如何解决呢?可以通过设置事务隔离级别,解决读问题!Spring 中提供了以下隔离级别。
级别 | 说明 |
DEFAULT | 使用所用的数据库的隔离级别 |
READ_UNCOMMITTED(读未提交) | 可以读取到尚未提交的更改,可能导致脏读、幻读和不可重复读 |
READ_COMMITTED(读已提交) | Oracle 的默认级别,可以读取到已提交的更改的数据,防止脏读,可能出现幻读和不可重复读 |
REPEATABLE_READ(可重复读) | MySQL 的默认级别,同一条SQL多次执行,可以读取到已提交的新增的数据,防止脏读和不可重复读,可能出现幻读 |
SERIALIZABLE | 可串行化,什么读问题都不会产生 |
加入依赖
xml方式
我们先来看看不使用事务会发生什么情况。创建名为 aopxml
的包。
提供数据库脚本
开发代码
新建 dao 包
在类中提供两个方法,一个张三增加金额,一个李四减金额。
新建 entity
新建 service 包
项目结构如下:
测试
控制台出现异常
再来查看数据库数据,可以发现张三的金额增加了,但是李四的金额没有减。银行哭死!!! 所以我们需要引入 Spring 事务,解决上述出现的问题。
引入 tx 命名空间
注意: 上面说过 Spring 提供的声明式事务管理是依赖于 Spring AOP 实现的,因此还需要添加 aop 命名空间配置。当然我还额外引入了 spring-context 命名空间。
配置事务管理器以及 JdbcTemplate
配置的事务管理器实现为 DataSourceTransactionManager,是 JDBC 和 MBatis 的PlatformTransactionManager 接口实现。
jdbc.properties
配置事务通知
配置事务通知,指定所需要使用的事务管理器以及指定事务作用的方法和该事务属性。
transaction-manage
参数的默认值就是 transactionManager,如果事务管理器 id 与其一致,则可以不用指定。
元素包含多个属性参数,可以为某个或某些方法(name 属性指定的方法)定义事务属性,如下表所示:
事务属性 | 说明 |
propagation | 指定事务的传播行为,默认为 REQUIRED |
isolation | 指定事务的隔离级别,默认为所使用数据库的隔离级别 |
read-only | 指定是否为只读事务,默认为 false |
timeout | 表示超时时间,单位为“秒”。事务在指定的超时时间后,自动回滚。避免事务长时间不提交导致数据库资源占用。默认为 -1,代表不超时 |
rollback-for | 指定出现哪些异常进行事务回滚 |
no-rollback-for | 指定出现哪些异常不进行事务回滚 |
配置切入点和切面
如上写法就对 transfer 方法进行了事务管理。就不会出现小明减少余额,而小红没有增加余额的情况,发生了异常就进行回滚。
注解方式
使用注解方式就不会有上面如此琐碎的配置了。再重新创建名为 txannon
包,将 xml 方式使用到的 entity、dao、service 相关代码 copy 过来。
开启事务
使用 EnableTransactionManagement
注解开启事务。
相当于 tx:annotation-driven 标签。
创建配置类
可以不需要在配置切入点和切面了。
添加事务注解
在需要添加事务的方法上添加 @Transactional
注解,表明该方法需要进行事务管理。
@Transactional
这个注解可以添加到类上面,也可以添加方法上面。如果把这个注解添加到类上面,这个类里面所有的方法都添加事务,如果把这个注解添加方法上面,则是为这个方法添加事务。
@Transactional
Transactional
这个注解里面可以配置很多事务相关参数。
事务属性 | 说明 |
value | 指定不同的事务管理器。 |
transactionManager | 跟 value 一致。 |
propagation | 指定事务的传播行为,默认为 REQUIRED |
isolation | 指定事务的隔离级别,默认为所使用数据库的隔离级别 |
read-only | 指定是否为只读事务,默认为 false |
timeout | 表示超时时间,单位为“秒”。事务在指定的超时时间后,自动回滚。避免事务长时间不提交导致数据库资源占用。默认为 -1,代表不超时 |
rollbackFor | 指定出现哪些异常进行事务回滚 |
rollbackForClassName | 指定异常类名称,进行事务回滚 |
noRollbackFor | 指定出现哪些异常不进行事务回滚 |
noRollbackForClassName | 指定出现哪些异常类名称不进行事务回滚 |
基本用法会了,现在就来看看事务的传播行为,这是Spring事务中难以理解的一块,因为它的场景很多。
事务传播行为详解
REQUIRED
如果有事务在运行,当前的方法就在这个事务内运行,否则,就启动一个新的事务,并在自己的事务内运行
- 如果 transfer 方法没有事务,则 reduce 方法会创建一个事务。
- 两个方法的事务的传播行为都为
Propagation.REQUIRED
。所以transfer
方法会先开启一个事务,而reduce
会加入到transfer
方法的事务中,这两个方法用的是同一个事务,所以不论是在哪个方法中抛出异常,所有操作都会回滚。
REQUIRES_NEW
当前方法必须启动新事务,并在它自己的事务内运行。如果有事务正在运行,应该将它挂起。
reduce 方法行为修改为 Propagation.REQUIRES_NEW
。transfer 方法创建新事务,然后调用 reduce 方法,reduce 方法会将 transfer 方法的事务挂起,并创建属于 reduce 方法的事务。所以在该例子中会创建两个事务。由于有两个事务,那事务的回滚就出现了几种情况。
- 场景一
- 场景二
- 场景三
transfer 方法进行的操作不会回滚,reduce 方法的操作会回滚。
- 如果 transfer 方法没有事务,则 reduce 方法会创建一个事务。
- 如果 transfer 方法有事务,则 reduce 方法会将 transfer 方法的事务挂起,并创建属于 reduce 方法的事务。如果此时 transfer 方法发生了异常,则 transfer 方法操作会回滚,但不会导致 reduce 方法回滚。如果 reduce 方法发生了异常,则 reduce 方法操作会回滚,如果 transfer 方法没有捕获 reduce 方法的异常,那 transfer 方法也会回滚。
NESTED
如果当前存在事务(主事务),则创建一个新事务作为当前事务的嵌套事务(子事务)来运行;如果当前没有事务,则该取值等价于 REQUIRED。
- 如果 transfer 方法没有事务,则 reduce 方法会创建一个事务。
- 如果 transfer 方法有事务,则 reduce 方法会创建一个新事务,作为 transfer 方法事务的嵌套事务来运行。那会有什么场景呢?
- 场景一
transfer 方法发生异常并回滚,会导致 reduce 方法 同时回滚。
- 场景二
transfer 方法进行的操作不会回滚,reduce 方法的操作会回滚。注意:transfer 方法需要进行 catch,不然 transfer 方法也会回滚。
主事务方法异常回滚时,会同时回滚子事务。而子事务可以单独异常回滚,可以不影响主事务和其他子事务(前提是需要处理掉子事务的异常)
MANDATORY
如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
由于 transfer 方法没有事务,在启动时就会抛出异常,如下:
SUPPORTS
如果有事务在运行,当前的方法就在这个事务内运行;如果当前没有事务,则以非事务的方式运行。
由于 transfer 方法没有事务,所以 reduce 方法也不会创建事务,发生了异常也不会进行回滚。
NOT_SUPPORTED
以非事务方式运行,如果当前存在事务,则把当前事务挂起。
transfer 方法有事务,但 reduce 方法传播行为是 NOT_SUPPORTED,所以会将 transfer 方法事务挂起,reduce 方法以非事务的方式运行。
所以图片例子会出现 transfer 方法进行的操作会回滚,reduce 方法的操作不会回滚。
NEVER
以非事务方式运行,如果当前存在事务,则抛出异常。
由于 transfer 方法有事务,在启动时就会抛出异常,如下:
回滚规则
上面一直在说遇到异常就回滚,那是遇到所有异常都会回滚吗?不是的,默认情况下,Spring 事务只有遇到 RuntimeException 以及 Error 时才会回滚,在遇到检查型异常时是不会回滚的,比如 IOException、TimeoutException。
那如果想在发生检查型异常时也进行回滚呢,可以使用 rollbackFor 属性进行如下配置:
那同理,如果遇到某个异常,不想进行回滚,使用 noRollbackFor 属性配置如下: