本篇重点总结:
- 在 Spring 项目中使用事务,有两种方式:编程式手动操作和声明式自动提交,声明式自动提交使用最多,只需要在方法上添加注解 @Transactional
- 设置事务的隔离级别 @Transactional(isolation = Isolation.SERIALIZABLE),Spring 中的事务隔离级别有5种
- 设置事务的传播机制 @Transactional(propagation = Propagation.REQUIRED),Spring 中的事务传播级别有 7 种
事务定义:将一组操作封装成一个执行单元(封装到一起),要么全部成功,要么全部失败
那么为什么要用事务呢
比如两个银行账户之间的转账操作:
如果没有事务。第一步执行成功了,第二步执行失败了,那么 A 账号就丢失了 100 元,而如果使用事务就可以解决这个问题,让这一组操作要么一起成功,要么一起失败
Sping 中事务的操作用两种:
MySQL 中事务有 3 个重要的操作:开启事务、提交事务、回滚事务,它们对应的操作命令如下
-- 开启事务 start transaction; -- 业务执⾏ -- 提交事务 commit; -- 回滚事务 rollback;
Spring 中手动操作事务和 MySQL操作事务类似,也是有 3 个重要操作
Spring Boot 内置了两个对象,DataSourceTransactionManager (事务管理器)用来获取事务(开启事务)、提交或回滚事务的,而 TransactionDefinition 是事务的属性,在获取事务的时候需要将 TransactionDefinition 传递进去从而获得一个事务 TransactionStatus 对象
@RestController
public class UserController {
@Autowired
private UserService userService;
@Autowired
private DataSourceTransactionManager transactionManager;
@Autowired
private TransactionDefinition transactionDefinition;
// 在此方法中使用编程式的事物
@RequestMapping("/add")
public int add(UserInfo userInfo) {
// 非空效验【验证用户名和密码不为空】
if(userInfo==null || !StringUtils.hasLength(userInfo.getUsername())
|| !StringUtils.hasLength(userInfo.getPassword())) {
return 0;
}
// 开启事务(获取事务)
TransactionStatus transactionStatus = transactionManager.getTransaction(transactionDefinition);
int result = userService.add(userInfo);
System.out.println("add 受影响的行数:" + result);
// 提交事务
transactionManager.commit(transactionStatus);
// // 回滚事务
// transactionManager.rollback(transactionStatus);
return result;
}
}
运行程序分别查看提交事务和回滚事务的效果
![Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),第1张 Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CDdDwGaw-1676374722301)(C:\Users463\AppData\Roaming\Typora\typora-user-images76287772298.png)],第1张](/upload/website_attach/202312/1_4QSH9BE4Z84HPCVY.jpeg)
![Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),第2张 Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y3cm5944-1676374722302)(C:\Users463\AppData\Roaming\Typora\typora-user-images76287783586.png)],第2张](/upload/website_attach/202312/1_R3U6P646RK73WVBX.jpeg)
声明式事务的实现,只需要在方法上添加 @Transactional 注解就可以实现,无序手动开启事务和提交事务,进入方法时自动开启事务,方法执行完全会自动提交事务,如果中途发生了没有处理的异常会自动回滚事务
// 在此方法中使用声明式的事物
// 在进入方法之前,自动开启事务,在方法之前完后,自动提交事务,如果出现异常,则自动回滚事务
@Transactional
@RequestMapping("/add2")
public int add2(UserInfo userInfo) {
if(userInfo==null || !StringUtils.hasLength(userInfo.getUsername())
|| !StringUtils.hasLength(userInfo.getPassword())) {
return 0;
}
int result = userService.add(userInfo);
System.out.println("add2 受影响的行数:" + result);
int num = 10/0;
return result;
}
![Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),第3张 Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S21gpiHu-1676374722303)(C:\Users463\AppData\Roaming\Typora\typora-user-images76288819713.png)],第3张](/upload/website_attach/202312/1_S7RJRQDFDBDA8JG2.jpeg)
去除异常的那行代码重新运行程序
![Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),第4张 Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3Vx76KGj-1676374722303)(C:\Users463\AppData\Roaming\Typora\typora-user-images76289003919.png)],第4张](/upload/website_attach/202312/1_3B9XXXWUG6PX7KKN.jpeg)
@Transactional 可以用来修饰方法或类:
| 参数 | 作用 |
|---|---|
| value | 当你配置多个事务管理器时,可以使用该属性指定选择用哪个事务管理器 |
| transactionManager | 同上 |
| propagation | 事务的传播行为,默认值为 Propagation.REQUIRED |
| isolation | 事务的隔离级别,默认值为 Isolation.DEFAULT |
| timeout | 事务的超时时间,默认值为-1,如果超过该时间限制但事务还没完成,则自动回滚事务 |
| readOnly | 指定事务是否为只读事务,默认值为 false,为了忽略那些不需要事务的方法,比如读取数据可以设置 read-only 为 true |
| rolibackFor | 用于指定能够触发事务回滚的异常类型,可以指定多个异常类型 |
| rolibackForClassName | 同上 |
| noRolibackFor | 抛出指定的异常类型,不回滚事务,也可以指定多个异常类型 |
| noRollbackForClassName | 同上 |
设置事务的隔离级别
![Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),第5张 Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ibpM3tny-1676374722304)(C:\Users463\AppData\Roaming\Typora\typora-user-images76292506013.png)],第5张](/upload/website_attach/202312/1_7G5N3D9DYHPZ3ZC2.jpeg)
设置事务的超时时间
![Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),第6张 Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NIxWwKWm-1676374722304)(C:\Users463\AppData\Roaming\Typora\typora-user-images76293014348.png)],第6张](/upload/website_attach/202312/1_E2JGDN7QN73KV7PQ.jpeg)
@Transactional 在异常被捕获的情况下,不会进⾏事务⾃动回滚
@Transactional
@RequestMapping("/add3")
public int add3(UserInfo userInfo) {
if(userInfo==null || !StringUtils.hasLength(userInfo.getUsername())
|| !StringUtils.hasLength(userInfo.getPassword())) {
return 0;
}
int result = userService.add(userInfo);
System.out.println("add2 受影响的行数:" + result);
try {
int num = 10/0;
} catch (Exception e) {
}
return result;
}
![Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),第7张 Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ueCyvrWj-1676374722305)(C:\Users463\AppData\Roaming\Typora\typora-user-images76356422586.png)],第7张](/upload/website_attach/202312/1_C5NCTAZCDXN2ZDYE.jpeg)
解决方法1:将异常重新抛出去
对于捕获的异常,事务是会⾃动回滚的,因此解决⽅案1就是将异常重新抛出
![Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),第8张 Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y0emxEE9-1676374722306)(C:\Users463\AppData\Roaming\Typora\typora-user-images76356654556.png)],第8张](/upload/website_attach/202312/1_RGB7FEM9JABRNSUR.jpeg)
解决方法2:使用代码的方式手动回滚当前事务
手动回滚事务,在⽅法中使⽤ TransactionAspectSupport.currentTransactionStatus() 可以得到当前的事务,然后设置回滚方法 setRollbackOnly 就可以实现回滚了,具体实现代码
@Transactional
@RequestMapping("/add3")
public int add3(UserInfo userInfo) {
if(userInfo==null || !StringUtils.hasLength(userInfo.getUsername())
|| !StringUtils.hasLength(userInfo.getPassword())) {
return 0;
}
int result = userService.add(userInfo);
System.out.println("add2 受影响的行数:" + result);
try {
int num = 10/0;
} catch (Exception e) {
//手动回滚事务
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return result;
}
![Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),第9张 Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X3KVRTil-1676374722306)(C:\Users463\AppData\Roaming\Typora\typora-user-images76356967349.png)],第9张](/upload/website_attach/202312/1_ZEGQUKCPUEBMNAN4.jpeg)
@Transactional 是基于 AOP 实现的,AOP 又是使用动态代理来实现的。如果目标对象实现了接口,默认情况下采用 JDK 的动态代理,如果目标对象没有实现接口,会使用 CGLIB 动态代理。@Transactional 在开始执行业务之前,通过代理先开启事务,在执行成功之后再提交事务,如果中途遇到异常,则回滚事务
@Transactional 实现思路
![Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),第10张 Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U53Wzj7E-1676374722307)(C:\Users463\AppData\Roaming\Typora\typora-user-images76358204440.png)],第10张](/upload/website_attach/202312/1_WUHCFSYUC95RPMVZ.jpeg)
@Transactional 具体执行细节
![Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),第11张 Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2L3MlAy7-1676374722308)(C:\Users463\AppData\Roaming\Typora\typora-user-images76358241159.png)],第11张](/upload/website_attach/202312/1_92ZH9925SQ3CHBQ5.jpeg)
事务有四大特性(ACID),原子性、持久性、一致性和隔离性
这四个特性中,只有隔离性(隔离级别)是可以设置的
为什么要设置事务的隔离级别
设置事务的隔离级别是用来保障多个并发事务执行更可控,更符合操作者预期的
这个可控表示的是,比如疫情的时候,有确诊、密接、次密接等针对不同的人群,采取不同的隔离级别,这种方式与事务的隔离级别类似,都是让某种行为操作变的 更可控,事务的隔离级别就是为了防止,其他事务影响当前事务执行的一种策略
MySQL 事务隔离级别有 4 种
| 事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交(READ UNCOMMITTED) | √ | √ | √ |
| 读已提交(READ COMMITTED) | √ | √ | |
| 可重复读(REPEATABLE READ) | √ | ||
| 串行化(SERIALIZABLE) |
Spring 中事务隔离级别有 5 种
多了一个默认事务隔离级别 DEFAULT 以连接的数据库事务隔离级别为准,如果连接的是 MySQL 那么默认就是 可重复读
注意事项:
Spring 中事务隔离级别可以通过 @Transactional 中的 isolation 属性进行设置
Spring 事务传播机制:多个事务在相互调用时,事务是如何传递的
事务隔离级别是保证多个并发事务执⾏的可控性的(稳定性的),⽽事务传播机制是保证⼀个事务在多个调⽤⽅法间的可控性的(稳定性的)
![Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),第12张 Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nl69KKT9-1676374722309)(C:\Users463\AppData\Roaming\Typora\typora-user-images76361930104.png)],第12张](/upload/website_attach/202312/1_Y28D4HVJ2PKKN4W2.jpeg)
事务的传播机制有 7 种:
![Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),第13张 Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pvcupysn-1676374722309)(C:\Users463\AppData\Roaming\Typora\typora-user-images76362237124.png)],第13张](/upload/website_attach/202312/1_4GGX6V6TT84YZD6U.jpeg)
![Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),第14张 Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LNBCGXn4-1676374722310)(C:\Users463\AppData\Roaming\Typora\typora-user-images76364699386.png)],第14张](/upload/website_attach/202312/1_E2AG729M4W85FEZP.jpeg)
在mycnblog数据库中,先创建一个表
mysql> create table loginfo(
-> id int primary key auto_increment,
-> name varchar(250),
-> `desc` text,
-> createtime datetime default CURRENT_TIMESTAMP);
Query OK, 0 rows affected (0.04 sec)
mysql> desc loginfo;
+------------+--------------+------+-----+-------------------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+--------------+------+-----+-------------------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| name | varchar(250) | YES | | NULL | |
| desc | text | YES | | NULL | |
| createtime | datetime | YES | | CURRENT_TIMESTAMP | |
+------------+--------------+------+-----+-------------------+----------------+
4 rows in set (0.00 sec)
以下代码实现中,先开启事务先成功插入一条用户数据,然后再执行日志报错,而在日志报错是发生了异常,观察 propagation = Propagation.REQUIRED 的执行结果
@RestController
public class UserController {
@Autowired
private UserService userService;
@Autowired
private LogService logService;
@Transactional(propagation = Propagation.REQUIRED)
@RequestMapping("/add4")
public int add4(UserInfo userInfo) {
if(userInfo == null || !StringUtils.hasLength(userInfo.getUsername())
|| !StringUtils.hasLength(userInfo.getPassword())) {
return 0;
}
int userResult = userService.add(userInfo);
System.out.println("添加用户:" + userResult);
LogInfo logInfo = new LogInfo();
logInfo.setName("添加用户");
logInfo.setDesc("添加用户结果:" + userResult);
int logResult = logService.add(logInfo);
return userResult;
}
}

执行结果:程序报错,数据库没有插⼊任何数据
执行流程
UserService 中的保存⽅法正常执⾏完成。
LogService 保存⽇志程序报错,因为使⽤的是 Controller 中的事务,所以整个事务回滚。
数据库中没有插⼊任何数据,也就是步骤 1 中的⽤户插⼊⽅法也回滚了。
UserController 类中的代码不变,将添加用户和添加日志的方法修改为 REQUIRES_NEW 不支持当前事务,重新创建事务

运行程序
![Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),第17张 Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xRek64FZ-1676374722312)(C:\Users463\AppData\Roaming\Typora\typora-user-images76369232749.png)],第17张](/upload/website_attach/202312/1_6WW8Y8BFTH7JYYTJ.jpeg)
![Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),第18张 Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QNO5pVdC-1676374722312)(C:\Users463\AppData\Roaming\Typora\typora-user-images76372475953.png)],第18张](/upload/website_attach/202312/1_45VMUG4JDTPUBVGT.jpeg)
方法调用流程:Controller/add ——》 用户添加方法(userservice) ——》 日志添加方法(logservice)
当日志添加方法出现异常之后,嵌套事务的执行结果是:
![Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),第19张 Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fGPQVmTM-1676374722312)(C:\Users463\AppData\Roaming\Typora\typora-user-images76371659498.png)],第19张](/upload/website_attach/202312/1_Y5JYJ2W4JJWJNBCQ.jpeg)
先看嵌套事务
![Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),第20张 Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JqyEXrxd-1676374722313)(C:\Users463\AppData\Roaming\Typora\typora-user-images76373164427.png)],第20张](/upload/website_attach/202312/1_4PQDAJ67K7ADHERH.jpeg)
在 LogService 中进行事务的回滚操作
最终执行的效果就是,User 表成功添加数据,而 Log 表中没有添加数据。Log 中的事务已经回滚,但是嵌套事务不会回滚嵌套之前的事务,也就是说 嵌套事务可以实现部分事务回滚
加入事务
![Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),第21张 Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制),[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GnP7AlMP-1676374722313)(C:\Users463\AppData\Roaming\Typora\typora-user-images76373526005.png)],第21张](/upload/website_attach/202312/1_5372EYFJ7YEMQGXJ.jpeg)
最终程序的执行结果:用户表和日志表都没有添加任何数据,说明整个事务都回滚了。也就是说 REQUIRED 如果回滚就是回滚所有事务,不能实现部分事务的回滚
嵌套事务之所以能够实现部分事务的回滚,是因为事务中有一个保存点(相当于游戏存档),嵌套事务进入之后相当于新建一个保存点,而回滚时只回滚到当前保存点,因此之前的事务是不受影响的。
而 REQUIRED 是加入到当前事务中,并没有创建事务的保存点,因此出现了回滚就是整个事务的回滚
总结二者区别: