✏️作者:银河罐头
📋系列专栏:JavaEE
🌲“种一棵树最好的时间是十年前,其次是现在”
目录
- Spring 中事务的实现
- Spring 编程式事务
- Spring 声明式事务
- @Transactional 作⽤范围
- @Transactional 参数说明
- Spring 事务隔离级别
- Spring 事务传播机制
- 事务传播机制
- 演示事务传播机制
- 嵌套事务 NESTED 原理
Spring 中事务的实现
Spring 中的事务操作分为两类:
- 编程式事务(⼿动写代码操作事务)。
- 声明式事务(利⽤注解⾃动开启和提交事务)。
事务在 MySQL 有 3 个重要的操作:开启事务、提交事务、回滚事务,它们对应的操作命令如下:
-- 开启事务
start transaction;
-- 业务执⾏
-- 提交事务
commit;
-- 回滚事务
rollback;
Spring 编程式事务
Spring ⼿动操作事务和上⾯ MySQL 操作事务类似,它也是有 3 个重要操作步骤:
开启事务(获取事务)。
提交事务。
回滚事务。
application.properties:
spring.datasource.url= jdbc:mysql://localhost:3306/mycnblog?characterEncoding=utf8&useSSL=false
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#设置 MyBatis
mybatis.mapper-locations=classpath:/mybatis/*Mapper.xml
#打印 MyBatis 执行的 sql
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
#因为打印 MyBatis 执行的 sql 日志级别是 debug,而默认级别是 info,所以要修改日志的默认级别为 debug
logging.level.com.example.demo=debug
package com.example.demo.controller;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
//编程式事务
@Autowired
private DataSourceTransactionManager transactionManager;
@Autowired
private TransactionDefinition transactionDefinition;
@RequestMapping("/del")
public int del(Integer id){
if(id != null && id > 0){
// 开启事务
TransactionStatus transactionStatus =
transactionManager.getTransaction(transactionDefinition);
// 业务操作: 删除用户
int result = userService.del(id);
System.out.println(result);
// 提交事务
// transactionManager.commit(transactionStatus);
// 回滚事务
transactionManager.rollback(transactionStatus);
return result;
}
return 0;
}
}
mysql> select * from userinfo;
+----+----------+----------+-------+---------------------+---------------------+-------+
| id | username | password | photo | createtime | updatetime | state |
+----+----------+----------+-------+---------------------+---------------------+-------+
| 1 | admin | 123456 | | 2021-12-06 17:10:48 | 2021-12-06 17:10:48 | 1 |
| 2 | zhangsan | 123456 | | 2023-05-29 20:10:14 | 2023-05-29 20:10:14 | 1 |
| 3 | lisi | 123456 | | 2023-05-29 20:27:53 | 2023-05-29 20:27:53 | 1 |
| 4 | wangwu | 123456 | | 2023-05-30 14:43:23 | 2023-05-30 14:43:23 | NULL |
| 5 | wangwu2 | 123456 | | 2023-05-30 14:44:30 | 2023-05-30 14:44:30 | 1 |
| 13 | liliu | 123456 | | 2023-05-30 15:57:37 | 2023-05-30 15:57:37 | 1 |
+----+----------+----------+-------+---------------------+---------------------+-------+
6 rows in set (0.00 sec)
再次查询数据库:
mysql> select * from userinfo;
+----+----------+----------+-------+---------------------+---------------------+-------+
| id | username | password | photo | createtime | updatetime | state |
+----+----------+----------+-------+---------------------+---------------------+-------+
| 1 | admin | 123456 | | 2021-12-06 17:10:48 | 2021-12-06 17:10:48 | 1 |
| 2 | zhangsan | 123456 | | 2023-05-29 20:10:14 | 2023-05-29 20:10:14 | 1 |
| 3 | lisi | 123456 | | 2023-05-29 20:27:53 | 2023-05-29 20:27:53 | 1 |
| 4 | wangwu | 123456 | | 2023-05-30 14:43:23 | 2023-05-30 14:43:23 | NULL |
| 5 | wangwu2 | 123456 | | 2023-05-30 14:44:30 | 2023-05-30 14:44:30 | 1 |
| 13 | liliu | 123456 | | 2023-05-30 15:57:37 | 2023-05-30 15:57:37 | 1 |
+----+----------+----------+-------+---------------------+---------------------+-------+
6 rows in set (0.00 sec)
发现 id = 13 这条数据还在,就是因为回滚操作。
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
//编程式事务
@Autowired
private DataSourceTransactionManager transactionManager;
@Autowired
private TransactionDefinition transactionDefinition;
@RequestMapping("/del")
public int del(Integer id){
if(id == null || id <= 0){
return 0;
}
// 开启事务
TransactionStatus transactionStatus = null;
// 业务操作: 删除用户
int result = 0;
try{
transactionStatus =
transactionManager.getTransaction(transactionDefinition);
result = userService.del(id);
System.out.println("删除: " + result);
// 提交事务/回滚事务
transactionManager.commit(transactionStatus); // 提交事务
}catch (Exception e){
if(transactionStatus != null) {
transactionManager.rollback(transactionStatus); // 回滚事务
}
}
return result;
}
}
Spring 声明式事务
声明式事务的实现很简单,只需要在需要的⽅法上添加 @Transactional 注解就可以实现了,⽆需⼿动 开启事务和提交事务,进⼊⽅法时⾃动开启事务,⽅法执⾏完会⾃动提交事务,如果中途发⽣了没有处理的异常会⾃动回滚事务。
@Transactional 在单元测试中使用,无论结果如何都会 rollback;
@Transactional 在 普通方法中使用,没有出现异常就会提交事务,如果出现异常才会 rollback.
@RestController
@RequestMapping("/user2")
public class UserController2 {
@Autowired
private UserService userService;
@RequestMapping("/del")
@Transactional
public int del(Integer id){
if(id == null || id <= 0){
return 0;
}
return userService.del(id);
}
}
id = 5 的这条数据 成功删除。
- 下面来验证 "回滚"效果:
@RestController
@RequestMapping("/user2")
public class UserController2 {
@Autowired
private UserService userService;
@RequestMapping("/del")
@Transactional
public int del(Integer id){
if(id == null || id <= 0){
return 0;
}
int result = userService.del(id);
System.out.println("删除: " + result);
int num = 10/0;
return result;
}
}
mysql> select * from userinfo;
+----+----------+----------+-------+---------------------+---------------------+-------+
| id | username | password | photo | createtime | updatetime | state |
+----+----------+----------+-------+---------------------+---------------------+-------+
| 1 | admin | 123456 | | 2021-12-06 17:10:48 | 2021-12-06 17:10:48 | 1 |
| 2 | zhangsan | 123456 | | 2023-05-29 20:10:14 | 2023-05-29 20:10:14 | 1 |
| 3 | lisi | 123456 | | 2023-05-29 20:27:53 | 2023-05-29 20:27:53 | 1 |
| 4 | wangwu | 123456 | | 2023-05-30 14:43:23 | 2023-05-30 14:43:23 | NULL |
+----+----------+----------+-------+---------------------+---------------------+-------+
4 rows in set (0.00 sec)
@Transactional 作⽤范围
@Transactional 可以⽤来修饰⽅法或类:
修饰⽅法时:需要注意只能应⽤到 public ⽅法上,否则不⽣效。
修饰类时:表明该注解对该类中所有的 public ⽅法都⽣效。
@Transactional 参数说明
Spring 事务隔离级别
Spring 中事务隔离级别包含以下 5 种:
- Isolation.DEFAULT:以连接的数据库的事务隔离级别为主。
- Isolation.READ_UNCOMMITTED:读未提交,可以读取到未提交的事务,存在脏读。
- Isolation.READ_COMMITTED:读已提交,只能读取到已经提交的事务,解决了脏读,存在不可重 复读。
- Isolation.REPEATABLE_READ:可重复读,解决了不可重复读,但存在幻读(MySQL默认级 别)。
- Isolation.SERIALIZABLE:串⾏化,可以解决所有并发问题,但性能太低。
可以看出,相比于 MySQL 的事务隔离级别,Spring 的事务隔离级别只是多了⼀个 Isolation.DEFAULT(以数据库的全局事务隔离级别为主)。
事务类型:
1.普通事务
2.只读事务,没设置事务隔离级别的情况下(可重复读) => 可以设置隔离级别
3.无事务(默认的隔离级别可重复读)
@Transactional(readOnly = true, isolation = Isolation.SERIALIZABLE)
- 再来看一个例子:
@RestController
@RequestMapping("/user2")
public class UserController2 {
@Autowired
private UserService userService;
@RequestMapping("/del")
@Transactional(readOnly = true, isolation = Isolation.SERIALIZABLE)
public int del(Integer id){
if(id == null || id <= 0){
return 0;
}
int result = userService.del(id);
System.out.println("删除: " + result);
try {
int num = 10/0;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
}
在 int num = 10/0; 语句外面加 try - catch ,事务还会 rollback 吗?
mysql> select * from userinfo;
+----+----------+----------+-------+---------------------+---------------------+-------+
| id | username | password | photo | createtime | updatetime | state |
+----+----------+----------+-------+---------------------+---------------------+-------+
| 1 | admin | 123456 | | 2021-12-06 17:10:48 | 2021-12-06 17:10:48 | 1 |
| 2 | zhangsan | 123456 | | 2023-05-29 20:10:14 | 2023-05-29 20:10:14 | 1 |
| 3 | lisi | 123456 | | 2023-05-29 20:27:53 | 2023-05-29 20:27:53 | 1 |
+----+----------+----------+-------+---------------------+---------------------+-------+
3 rows in set (0.00 sec)
事务直接提交了,没有 rollback。删除了一条数据。
声明式事务发生异常,并添加 try-catch 有可能出现异常,事务不会自动回滚,那么就会导致业务出错。
解决方案有 2 种:
1.将异常抛出去,让框架感知到异常,框架感知到异常之后会自动回滚事务。
@RestController
@RequestMapping("/user2")
public class UserController2 {
@Autowired
private UserService userService;
@RequestMapping("/del")
@Transactional
public int del(Integer id){
if(id == null || id <= 0){
return 0;
}
int result = userService.del(id);
System.out.println("删除: " + result);
try {
int num = 10/0;
} catch (Exception e) {
throw e;
}
return result;
}
}
回滚了。
2.通过代码的方式手动回滚事务。
@RestController
@RequestMapping("/user2")
public class UserController2 {
@Autowired
private UserService userService;
@RequestMapping("/del")
@Transactional
public int del(Integer id){
if(id == null || id <= 0){
return 0;
}
int result = userService.del(id);
System.out.println("删除: " + result);
try {
int num = 10/0;
} catch (Exception e) {
// 手动回滚事务
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return result;
}
}
手动回滚成功。
面试题:Spring 事务失效的场景有哪些?
类没有修饰符,默认是 default,
接口没有修饰符,默认是 public.
Spring 事务传播机制
Spring 事务传播机制定义了多个包含了事务的⽅法,相互调⽤时,事务是如何在这些⽅法间进⾏传递 的。
事务隔离级别是保证多个并发事务执⾏的可控性(稳定性的),⽽事务传播机制是保证⼀个事务在多个调⽤⽅法间传递的可控性。
事务隔离级别解决的是多个事务同时调⽤⼀个数据库的问题。
⽽事务传播机制解决的是⼀个事务在多个节点(⽅法)中传递的问题。
事务传播机制
Spring 事务传播机制包含以下 7 种:
- Propagation.REQUIRED:默认的事务传播级别,它表示如果当前存在事务,则加入该事务;如果当前没有事务,则创建⼀个新的事务。
- Propagation.SUPPORTS:如果当前存在事务,则加⼊该事务;如果当前没有事务,则以⾮事务的 ⽅式继续运⾏。
- Propagation.MANDATORY:(mandatory:强制性)如果当前存在事务,则加⼊该事务;如果当 前没有事务,则抛出异常。
- Propagation.REQUIRES_NEW:表示创建⼀个新的事务,如果当前存在事务,则把当前事务挂 起。也就是说不管外部⽅法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部⽅法会新开 启⾃⼰的事务,且开启的事务相互独⽴,互不⼲扰。
- Propagation.NOT_SUPPORTED:以⾮事务⽅式运⾏,如果当前存在事务,则把当前事务挂起。
- Propagation.NEVER:以⾮事务⽅式运⾏,如果当前存在事务,则抛出异常。
- Propagation.NESTED:如果当前存在事务,则创建⼀个事务作为当前事务的嵌套事务来运⾏;如 果当前没有事务,则该取值等价于 PROPAGATION_REQUIRED。
以上7种传播机制,可根据“是否支持当前事务”的维度分为一下3类:
接下来我们用一个例子,来说明这3类事务传播机制的区别:
以情侣之间是否买房为例,我们将以上3类事务传播机制看作是恋爱中的3类女生类型:
- 普通型
- 强势型
- 懂事型
这三类女生如下图:
演示事务传播机制
1.支持当前事务 Propagation.REQUIRED.
“一荣俱荣一损俱损”.
package com.example.demo.controller;
@RestController
@RequestMapping("/user3")
public class UserController3 {
@Autowired
private UserService userService;
@RequestMapping("/add")
@Transactional(propagation = Propagation.REQUIRED)
public int add(String username, String password){
if(null == username || null == password || username.equals(" ") || password.equals(" ")){
return 0;
}
UserInfo userInfo = new UserInfo();
userInfo.setUsername(username);
userInfo.setPassword(password);
int result = userService.add(userInfo);
return result;
}
}
package com.example.demo.service;
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private LogService logService;
public int del(Integer id){
return userMapper.del(id);
}
@Transactional(propagation = Propagation.REQUIRED)
public int add(UserInfo userInfo){
//给用户表添加用户信息
int addUserResult = userMapper.add(userInfo);
System.out.println("添加用户结果: " + addUserResult);
//添加日志信息
Log log = new Log();
log.setMessage("添加用户信息");
logService.add(log);
return addUserResult;
}
}
package com.example.demo.service;
@Service
public class LogService {
@Autowired
private LogMapper logMapper;
@Transactional(propagation = Propagation.REQUIRED)
public int add(Log log){
int result = logMapper.add(log);
System.out.println("添加日志结果: " + result);
int num = 10/0;
return result;
}
}
mysql> select * from userinfo;
+----+----------+----------+-------+---------------------+---------------------+-------+
| id | username | password | photo | createtime | updatetime | state |
+----+----------+----------+-------+---------------------+---------------------+-------+
| 1 | admin | 123456 | | 2021-12-06 17:10:48 | 2021-12-06 17:10:48 | 1 |
| 2 | zhangsan | 123456 | | 2023-05-29 20:10:14 | 2023-05-29 20:10:14 | 1 |
| 3 | lisi | 123456 | | 2023-05-29 20:27:53 | 2023-05-29 20:27:53 | 1 |
+----+----------+----------+-------+---------------------+---------------------+-------+
3 rows in set (0.01 sec)
mysql> select * from log;
Empty set (0.00 sec)
算数异常,log 和 userinfo 都回滚了。
Propagation.REQUIRED:默认的事务传播级别,它表示如果当前存在事务,则加入该事务;如果当前没有事务,则创建⼀个新的事务。
2.不支持当前事务 Propagation.REQUIRES_NEW.
把上面例子调用链中所有的 Propagation.REQUIRED 都改成 Propagation.REQUIRES_NEW.
预期结果是 添加日志失败,添加用户成功。
发现 添加用户操作也回滚了?!和预期不符。因为UserController 感知到了异常,整个调用链都回滚了。
- 为了演示 添加日志失败,添加用户成功 这种效果。把代码稍微改动。
@RestController
@RequestMapping("/user3")
public class UserController3 {
@Autowired
private UserService userService;
@RequestMapping("/add")
@Transactional(propagation = Propagation.REQUIRES_NEW)
public int add(String username, String password){
if(null == username || null == password || username.equals(" ") || password.equals(" ")){
return 0;
}
UserInfo userInfo = new UserInfo();
userInfo.setUsername(username);
userInfo.setPassword(password);
int result = userService.add(userInfo);
return result;
}
}
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private LogService logService;
public int del(Integer id){
return userMapper.del(id);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public int add(UserInfo userInfo){
//给用户表添加用户信息
int addUserResult = userMapper.add(userInfo);
System.out.println("添加用户结果: " + addUserResult);
//添加日志信息
Log log = new Log();
log.setMessage("添加用户信息");
logService.add(log);
return addUserResult;
}
}
@Service
public class LogService {
@Autowired
private LogMapper logMapper;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public int add(Log log){
int result = logMapper.add(log);
System.out.println("添加日志结果: " + result);
//回滚操作
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return result;
}
}
日志回滚添加失败,用户没有回滚添加成功。
mysql> delete from userinfo where id = 5;
Query OK, 1 row affected (0.00 sec)
mysql> select * from userinfo;
+----+----------+----------+-------+---------------------+---------------------+-------+
| id | username | password | photo | createtime | updatetime | state |
+----+----------+----------+-------+---------------------+---------------------+-------+
| 1 | admin | 123456 | | 2021-12-06 17:10:48 | 2021-12-06 17:10:48 | 1 |
| 2 | zhangsan | 123456 | | 2023-05-29 20:10:14 | 2023-05-29 20:10:14 | 1 |
| 3 | lisi | 123456 | | 2023-05-29 20:27:53 | 2023-05-29 20:27:53 | 1 |
+----+----------+----------+-------+---------------------+---------------------+-------+
3 rows in set (0.00 sec)
- 把调用链中所有的 Propagation.REQUIRES_NEW 都改成 Propagation.REQUIRED.
- 为什么这里会报错?
日志(内层事务)要求回滚,用户(外层事务)没有感知到异常要提交事务,二者矛盾。
用户和日志都回滚了。
- 对于 Propagation.REQUIRED,如果外部事物回滚,那么内部事务也会回滚。但是不会报错。
@RestController
@RequestMapping("/user3")
public class UserController3 {
@Autowired
private UserService userService;
@RequestMapping("/add")
@Transactional(propagation = Propagation.REQUIRED)
public int add(String username, String password){
if(null == username || null == password || username.equals(" ") || password.equals(" ")){
return 0;
}
UserInfo userInfo = new UserInfo();
userInfo.setUsername(username);
userInfo.setPassword(password);
int result = userService.add(userInfo);
//回滚操作
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return result;
}
}
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private LogService logService;
public int del(Integer id){
return userMapper.del(id);
}
@Transactional(propagation = Propagation.REQUIRED)
public int add(UserInfo userInfo){
//给用户表添加用户信息
int addUserResult = userMapper.add(userInfo);
System.out.println("添加用户结果: " + addUserResult);
//添加日志信息
Log log = new Log();
log.setMessage("添加用户信息");
logService.add(log);
return addUserResult;
}
}
@Service
public class LogService {
@Autowired
private LogMapper logMapper;
@Transactional(propagation = Propagation.REQUIRED)
public int add(Log log){
int result = logMapper.add(log);
System.out.println("添加日志结果: " + result);
return result;
}
}
没有报错。
mysql> select * from log;
Empty set (0.00 sec)
mysql> select * from userinfo;
+----+----------+----------+-------+---------------------+---------------------+-------+
| id | username | password | photo | createtime | updatetime | state |
+----+----------+----------+-------+---------------------+---------------------+-------+
| 1 | admin | 123456 | | 2021-12-06 17:10:48 | 2021-12-06 17:10:48 | 1 |
| 2 | zhangsan | 123456 | | 2023-05-29 20:10:14 | 2023-05-29 20:10:14 | 1 |
| 3 | lisi | 123456 | | 2023-05-29 20:27:53 | 2023-05-29 20:27:53 | 1 |
+----+----------+----------+-------+---------------------+---------------------+-------+
3 rows in set (0.00 sec)
而且 用户和日志都回滚。
3.嵌套事务 Propagation.NESTED.
@RestController
@RequestMapping("/user3")
public class UserController3 {
@Autowired
private UserService userService;
@RequestMapping("/add")
@Transactional(propagation = Propagation.NESTED)
public int add(String username, String password){
if(null == username || null == password || username.equals(" ") || password.equals(" ")){
return 0;
}
UserInfo userInfo = new UserInfo();
userInfo.setUsername(username);
userInfo.setPassword(password);
int result = userService.add(userInfo);
return result;
}
}
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private LogService logService;
public int del(Integer id){
return userMapper.del(id);
}
@Transactional(propagation = Propagation.NESTED)
public int add(UserInfo userInfo){
//给用户表添加用户信息
int addUserResult = userMapper.add(userInfo);
System.out.println("添加用户结果: " + addUserResult);
//添加日志信息
Log log = new Log();
log.setMessage("添加用户信息");
logService.add(log);
return addUserResult;
}
}
@Service
public class LogService {
@Autowired
private LogMapper logMapper;
@Transactional(propagation = Propagation.NESTED)
public int add(Log log){
int result = logMapper.add(log);
System.out.println("添加日志结果: " + result);
//回滚操作
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return result;
}
}
预期结果:日志回滚,用户不回滚。添加日志失败,添加用户成功。
总结:
1.REQUIRED
是一个整体。如果外部事物回滚,那么内部事务也会回滚,不报错;如果内部事务回滚,那么外部事务也会回滚,报错。
2.REQUIRES_NEW
无论如何都会新建一个事务。
外部事务和内部事务相互独立,互不影响。
3.NESTED
虽然是嵌套关系,
但是外部事务和内部事务相互独立,互不影响。
嵌套事务 NESTED 原理
嵌套事务只所以能够实现部分事务的回滚,是因为事务中有⼀个保存点(savepoint)的概念,嵌套事务 进⼊之后相当于新建了⼀个保存点,⽽滚回时只回滚到当前保存点,因此之前的事务是不受影响的。
⽽ REQUIRED 是加⼊到当前事务中,并没有创建事务的保存点,因此出现了回滚就是整个事务回滚, 这就是嵌套事务和加⼊事务的区别。
MySQL :: MySQL 5.7 Reference Manual :: 13.3.4 SAVEPOINT, ROLLBACK TO SAVEPOINT, and RELEASE SAVEPOINT Statements