- Spring对事务的支持
- 声明式事务之注解实现方式
- 声明式事务之XML实现方式
十六、★Spring对事务的支持
- Spring 除了通过
AOP
,使得我们可以用编程进行事务的管理(编程式事务)外,它还提供了一套API
,使得我们可以通过声明来实现事务的管理。- Spring 对于事务的支持,底层还是通过动态代理来实现的,是基于面向切面编程
AOP
的,Spring 对AOP
进行了封装。
- Spring 对于事务的支持,底层还是通过动态代理来实现的,是基于面向切面编程
16.1 事务概述
- 什么是事务?
- 在一个业务流程当中,通常需要多条
DML(insert delete update)
语句共同联合才能完成,这多条DML
语句必须同时成功,或者同时失败,这样才能保证数据的安全。 - 多条
DML
要么同时成功,要么同时失败,这叫做事务。 - 事务:Transaction(tx)。
- 在一个业务流程当中,通常需要多条
- 事务的四个处理过程:
- 第一步:开启事务
(start transaction)
。 - 第二步:执行核心业务代码。
- 第三步:提交事务(如果核心业务处理过程中没有出现异常)
(commit transaction)
。 - 第四步:回滚事务(如果核心业务处理过程中出现异常)
(rollback transaction)
。
- 第一步:开启事务
- 事务的四个特性 ACID:
- A 原子性:事务是最小的工作单元,不可再分。
- C 一致性:事务要求要么同时成功,要么同时失败。事务前和事务后的总量不变。
- I 隔离性:事务和事务之间因为有隔离性,才可以保证互不干扰。
- D 持久性:成功的操作对数据库的影响永久保存,持久性是事务结束的标志。
16.2 Spring对事务的支持
Spring 实现事务的两种方式:
编程式事务:通过编写代码的方式来实现事务的管理。
声明式事务:
- 基于注解方式
- 基于 XML 方式
声明式事务:Spring 框架封装了一套用于处理事务的
API
。Spring 对事务的管理底层实现方式是基于
AOP
实现的,采用AOP
的方式进行了封装。Spring 专门针对事务开发了一套API
,API
的核心接口如下:
PlatformTransactionManager
接口:Spring 事务管理器的核心接口。- 第三方
ORM
框架可以通过提供该接口的实现类,让 Spring 可以帮助第三方ORM
框架进行事务的管理。
- 第三方
- 在 Spring6 中
PlatformTransactionManager
接口有两个实现:DataSourceTransactionManager
:支持JdbcTemplate
、MyBatis
、Hibernate
等事务管理。- 如果要在 Spring6 中使用
JdbcTemplate
,就要使用DataSourceTransactionManager
来管理事务(Spring 内置写好了,可以直接用)。
- 如果要在 Spring6 中使用
JtaTransactionManager
:支持分布式事务管理。
16.3 声明式事务之注解实现方式
下面的代码都是基于别人笔记中的代码。
使用声明式事务之注解实现方式,需要引入 Spring JDBC 依赖:
1
2
3
4
5
6<!-- spring jdbc 依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>6.0.8</version>
</dependency>
在 Spring 配置文件中配置事务管理器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<!--配置数据源-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/spring6"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</bean>
<!--
配置事务管理器
由于管理事务需要通过数据库连接对象connection调用setAutoCommit方法
所以需要提供数据源
-->
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>在 Spring 配置文件中引入
tx
命名空间。1
2
3
4
5
6
7
8
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">在 Spring 配置文件中配置 “事务注解驱动器”,开启事务注解,告诉 Spring 采用注解的方式控制事务。
1
2<!-- 开启事务注解驱动器,采用注解的方式控制事务 -->
<tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>在
Service
类上或方法上添加@Transactional
注解。- 在类上添加该注解,表示该类中所有的方法都有事务。
- 在某个方法上添加该注解,表示只有这个方法使用事务。
1
2
3
4
5
6
7
8
9
public class AccountServiceImpl implements AccountService {
...
// 控制事务
public void transfer(String fromActno, String toActno, Double balance) {
...
}
}
16.4 事务的属性
查看注解
@Transactional
源码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public Transactional {
String value() default "";
String transactionManager() default "";
String[] label() default {};
// 事务的传播行为
Propagation propagation() default Propagation.REQUIRED;
// 事务的隔离级别
Isolation isolation() default Isolation.DEFAULT;
// 事务的超时时间,默认-1表示不限时
int timeout() default -1;
String timeoutString() default "";
// 属性值为true,表示当前事务是一个只读事务
boolean readOnly() default false;
// 设置当程序出现什么样的异常的时候进行回滚
Class<? extends Throwable>[] rollbackFor() default {};
String[] rollbackForClassName() default {};
// 设置当程序出现什么样的异常的时候不回滚
Class<? extends Throwable>[] noRollbackFor() default {};
String[] noRollbackForClassName() default {};
}
16.4.1 事务传播行为
事务的传播行为:在
Service
类中有a()
方法和b()
方法,a()
方法上有事务,b()
方法上也有事务,当a()
方法执行过程中调用了b()
方法,事务是如何传递的?合并到一个事务里?还是开启一个新的事务?- 这就是事务传播行为。
事务传播行为在 Spring 框架中被定义为枚举类型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public enum Propagation {
REQUIRED(0),
SUPPORTS(1),
MANDATORY(2),
REQUIRES_NEW(3),
NOT_SUPPORTED(4),
NEVER(5),
NESTED(6);
private final int value;
private Propagation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}一共有七种传播行为:
REQUIRED
:支持当前事务,如果不存在就新建一个。**(默认)【没有就新建,有就加入】**- 执行到当前业务方法时,如果原先有事务就加入原来的事务,如果原先没有事务,就自己新建一个事务。
SUPPORTS
:支持当前事务,如果当前没有事务,就以非事务方式执行【有就加入,没有就不管了】- 如果原先有事务就加入原来的事务,如果原先没有事务,那就不开启事务,不进行事务控制。
MANDATORY
:必须运行在一个事务中,如果当前没有事务正在发生,将抛出一个异常【有就加入,没有就抛异常】- 如果原先有事务就加入原来的事务,如果原先没有事务,那就抛异常。
REQUIRES_NEW
:开启一个新的事务,如果一个事务已经存在,则将这个存在的事务挂起【不管有没有,直接开启一个新事务,开启的新事务和之前的事务不存在嵌套关系,之前事务被挂起】- 不管原先是否有事务,都开启一个新的事务,与原先的事务并列,挂起暂停原先的事务,执行控制新的事务。
NOT_SUPPORTED
:以非事务方式运行,如果有事务存在,挂起当前事务【不支持事务,存在就挂起】- 不进行事务控制,原先有事务,就暂停挂起事务。
NEVER
:以非事务方式运行,如果有事务存在,抛出异常【不支持事务,存在就抛异常】NESTED
:如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套式事务中。被嵌套的事务可以独立于外层事务进行提交或回滚。如果外层事务不存在,行为就像REQUIRED
一样。【有事务的话,就在这个事务里再嵌套一个完全独立的事务,嵌套的事务可以独立的提交和回滚。没有事务就和REQUIRED
一样】- 原先没有事务就新建一个事务,原先有事务就在原先的事务内新建一个独立的嵌套事务。
在代码中设置事务的传播行为:
1
代码没有执行到业务方法的结尾花括号,那么业务方法就没结束,事务也不会结束,因为没有执行到结尾花括号,永远不知道后面有什么代码。
对于在同一个事务内捕获出现的异常,依然会进行当前事务的回滚,捕获异常无效,因为当前事务不知道捕获异常后面还有什么
DML
语句,为了保证数据安全,出现异常只能回滚。事务 1 调用事务 2,事务 1 的传播行为为
REQUIRED
,事务 2 的传播行为为REQUIRES_NEW
,当事务 2 中抛出异常时,对于事务 2 会进行事务回滚,如果在事务 1 中捕获事务 2 抛出的异常,则事务 1 还会正常进行执行,因为REQUIRES_NEW
是挂起原先的事务,创建一个与原先事务并列的事务,两个事务并没有关系,所以事务 1 相当于无异常,不会进行回滚。
16.4.2 事务隔离级别
事务隔离级别类似于教室 A 和教室 B 之间的那道墙,隔离级别越高表示墙体越厚,隔音效果越好。
- 事务隔离:防止在多事务并发的情况下,多个事务同时操作同一个表时互相干扰的一种机制。
事务隔离级别在 Spring 中定义为枚举类型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public enum Isolation {
DEFAULT(-1), // 默认隔离级别
READ_UNCOMMITTED(1), // 读未提交
READ_COMMITTED(2), // 读已提交(Oracle默认隔离级别)
REPEATABLE_READ(4), // 可重复读(MySQL默认隔离级别)
SERIALIZABLE(8); // 序列化
private final int value;
private Isolation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}在 Spring 代码中如何设置隔离级别:
1
脏读、不可重复读、幻读
数据库中读取数据时存在的三大问题(三大读问题):
- 脏读:读取到没有提交到数据库的数据,叫做脏读。
- 事务 1 对数据库表中的数据进行了修改,但是还没有进行提交,此时事务 2 就可以读到事务 1 修改后没有提交的数据,这个时候就发生了脏读。
- 不可重复读:在同一个事务当中,第一次和第二次读取的数据不一样。
- 对于两个事务
Session A
、Session B
,Session A
读取 了一个字段,然后Session B
更新了该字段并且进行了提交。 之后Session A
再次读取同一个字段, 值就不同了,那就意味着发生了不可重复读。 - 不可重复读,同一个事务内,两次读取相同字段的数据,获取到的值不一样。
- 不可重复读,是前后两次从数据库中读取数据的结果不同。
- 对于两个事务
- 幻读:读到的数据是假的。
- 对于两个事务
Session A
、Session B
,Session A
从一个表中读取 了一个字段,然后Session B
在该表中插入了一些新的行。 之后,如果Session A
再次读取同一个表,就会多出几行,那就意味着发生了幻读。 - 幻读是针对于向数据库中进行数据插入的操作,当前从数据库中读到的数据记录的条数比之前读取到的数据记录条数多了,好像出现了幻觉一样,凭空多了一些记录出来。
- 幻读强调的是读到了之前没有读到的记录。
- 对于两个事务
- 脏读:读取到没有提交到数据库的数据,叫做脏读。
事务隔离级别包括四个级别:
- 读未提交:
READ_UNCOMMITTED
- 会读到缓存中还未提交的数据。
- 这种隔离级别,存在脏读问题,所谓的脏读
(dirty read)
表示能够读取到其它事务未提交的数据。
- 读提交:
READ_COMMITTED
- 解决了脏读问题,其它事务提交之后才能读到,但存在不可重复读问题。
- 同一个事务内前后读取到的数据不一样。
- 可重复读:
REPEATABLE_READ
- 解决了不可重复读,可以达到可重复读效果,只要当前事务不结束,读取到的数据一直都是一样的,但存在幻读问题。
- 序列化:
SERIALIZABLE
- 解决了幻读问题,事务排队执行。不支持并发,事务排队执行,效率低。
隔离级别 脏读 不可重复读 幻读 读未提交 有 有 有 读提交 无 有 有 可重复读 无 无 有 序列化 无 无 无 - 读未提交:
16.4.3 事务超时
在 Spring 代码中如何设置事务超时:
1
- 以上代码表示设置事务的超时时间为 10 秒。
- 表示超过 10 秒如果该事务中所有的
DML
语句还没有执行完毕的话,最终结果会选择回滚。
- 表示超过 10 秒如果该事务中所有的
- 默认值 -1,表示没有时间限制。
- 这里有个坑,事务的超时时间指的是哪段时间?
- 在当前事务中,最后一条
DML
语句执行之前的时间。如果最后一条DML
语句后面很有很多业务逻辑,这些业务代码执行的时间不被计入超时时间。 - 超时时间,是到最后一条
DML
语句执行完所耗费的时间,最后一条DML
语句之后的代码执行时间不计入超时时间。 - 如果想把所有代码执行时间都记入超时时间,可以在最后添加一个无关紧要的
DML
语句。
- 在当前事务中,最后一条
- 以上代码表示设置事务的超时时间为 10 秒。
16.4.4 只读事务
在 Spring 代码中如何设置只读事务:
1
readOnly
属性默认值为false
。- 将当前事务设置为只读事务,则表示在该事务执行过程中只允许
select
语句执行,delete insert update
均不可执行。 - 该特性的作用是:启动 Spring 的优化策略,提高
select
语句执行效率。 - 如果该事务中确实没有增删改操作,建议设置为只读事务,提高
select
语句的执行效率。
16.4.5 设置哪些异常回滚事务
在 Spring 代码中如何设置异常回滚事务:
1
rollbackFor
属性为Class
数组类型。- 上面表示只有发生
RuntimeException
异常或该异常的子类异常才回滚。
16.4.6 设置哪些异常不回滚事务
在 Spring 代码中如何设置异常不回滚事务:
1
noRollbackFor
属性为Class
数组类型。- 上面表示发生
NullPointerException
或该异常的子类异常不回滚,其他异常则回滚。
16.5 事务的全注解式开发
类似于
12.9
、15.9
小节,通过写一个配置类来代替 XML 配置文件。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47package cw.study.spring.bank;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
// 标注该类为配置类
// 组件扫描
// 开启事务注解驱动器,采用注解的方式控制事务 ,代替了tx:annotation-driven标签
public class SpringConfig {
// Spring看到这个@Bean注解后会调用该注解标注的方法
// 该方法的返回值是一个Java对象,这个Java对象会自动纳入Spring容器管理
// @Bean(name = "Bean的id")
// 注册组件
public DruidDataSource getDruidDataSource() {
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
druidDataSource.setUrl("jdbc:mysql://localhost:3306/spring6");
druidDataSource.setUsername("root");
druidDataSource.setPassword("root");
return druidDataSource;
}
// Spring在调用该方法时,会自动为该方法的参数赋值,根据类型自动注入
// 这里会将id为druidDataSource的对象传入到dataSource参数中
// 注册组件
public JdbcTemplate getJdbcTemplate(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource) {
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(dataSource);
return dataSourceTransactionManager;
}
}
16.6 声明式事务之XML实现方式
直接看别人的笔记吧: