✏️作者:银河罐头
📋系列专栏: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 {@Autowiredprivate UserService userService;//编程式事务@Autowiredprivate DataSourceTransactionManager transactionManager;@Autowiredprivate 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 {@Autowiredprivate UserService userService;//编程式事务@Autowiredprivate DataSourceTransactionManager transactionManager;@Autowiredprivate 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 {@Autowiredprivate UserService userService;@RequestMapping("/del")@Transactionalpublic int del(Integer id){if(id == null || id <= 0){return 0;}return userService.del(id);}
}
id = 5 的这条数据 成功删除。
- 下面来验证 "回滚"效果:
@RestController
@RequestMapping("/user2")
public class UserController2 {@Autowiredprivate UserService userService;@RequestMapping("/del")@Transactionalpublic 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 {@Autowiredprivate 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 {@Autowiredprivate UserService userService;@RequestMapping("/del")@Transactionalpublic 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 {@Autowiredprivate UserService userService;@RequestMapping("/del")@Transactionalpublic 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 {@Autowiredprivate 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 {@Autowiredprivate UserMapper userMapper;@Autowiredprivate 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 {@Autowiredprivate 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 {@Autowiredprivate 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 {@Autowiredprivate UserMapper userMapper;@Autowiredprivate 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 {@Autowiredprivate 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 {@Autowiredprivate 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 {@Autowiredprivate UserMapper userMapper;@Autowiredprivate 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 {@Autowiredprivate 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 {@Autowiredprivate 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 {@Autowiredprivate UserMapper userMapper;@Autowiredprivate 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 {@Autowiredprivate 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