详解Transactional
0.介绍
上篇文章讲解@Transactional的使用,这篇主要讲解 @Transactional 注解当中的三个常⻅属性及使用:
- rollbackFor: 异常回滚属性. 指定能够触发事务回滚的异常类型. 可以指定多个异常类型
- Isolation: 事务的隔离级别. 默认值为 Isolation.DEFAULT
- propagation: 事务的传播机制. 默认值为 Propagation.REQUIRED
注:使用的是该文章中的表格和基本代码
1.rollbackFor
1.1 介绍
@Transactional 默认只在遇到运⾏时异常(RuntimeException)和Error时才会回滚, 其他情况下不回滚. 即在Exception的⼦类中, 除了RuntimeException及其⼦类,其他的都不回滚。
1.2 演示
1.2.1 抛IO异常–提交
Controller接口:
@RequestMapping("/user")
@RestController
public class UserInfoController {
@Autowired
private UserInfoService userInfoService;
@RequestMapping("/update")
public boolean updateUserInfo(@RequestBody LogInfo logInfo) throws IOException {
boolean flag = userInfoService.updateUserInfo(logInfo);
return flag;
}
}
Service接口:
@Slf4j
@Service
public class UserInfoService {
@Autowired
private LogInfoMapper logInfoMapper;
@Autowired
private UserInfoMapper userInfoMapper;
@Transactional
public boolean updateUserInfo(LogInfo logInfo) throws IOException {
// 处理 from 向 to 转账 10
// 事务逻辑
// 1.from 的账户 -10
userInfoMapper.updateDelete(logInfo.getFrom(),logInfo.getNum());
// 2.to 的账户 +10
userInfoMapper.updateAdd(logInfo.getTo(),logInfo.getNum());
// 3. 记录日志
logInfoMapper.insert(logInfo);
// 故意设置抛异常
if(true){
throw new IOException();
}
return true;
}
}
使用postman测试:
运行结果:
前端接收到的数据报错,但是后端已经把事务提交了。
MySQL中数据(原始数据 zangsan–70;lisi–130):
可以看到,数据库中的数据发生了改变
1.2.2 rollbackFor 指定所有的异常回滚
如果我们需要所有异常都回滚, 需要来配置 @Transactional 注解当中的 rollbackFor 属性, 通过 rollbackFor 这个属性指定出现何种异常类型时事务进⾏回滚
Service接口代码:
@Slf4j
@Service
public class UserInfoService {
@Autowired
private LogInfoMapper logInfoMapper;
@Autowired
private UserInfoMapper userInfoMapper;
//rollbackFor = Exception.class 指定所有异常回滚
@Transactional(rollbackFor = Exception.class)
public boolean updateUserInfo(LogInfo logInfo) throws IOException {
// 处理 from 向 to 转账 10
// 事务逻辑
// 1.from 的账户 -10
userInfoMapper.updateDelete(logInfo.getFrom(),logInfo.getNum());
// 2.to 的账户 +10
userInfoMapper.updateAdd(logInfo.getTo(),logInfo.getNum());
// 3. 记录日志
logInfoMapper.insert(logInfo);
// 故意设置抛异常
if(true){
throw new IOException();
}
return true;
}
}
使用postman测试:
运行结果:
通过运行结果可以发现,只是把会话释放了,没有提交事务
MySQL:
MySQL中的数据没有发生改变
1.3 结论
结论:
• 在Spring的事务管理中,默认只在遇到运⾏时异常RuntimeException和Error时才会回滚.
• 如果需要回滚指定类型的异常, 可以通过rollbackFor属性来指定.
2.事务隔离级别
2.1 MySQL 事务隔离级别(回顾)
SQL 标准定义了四种隔离级别, MySQL 全都⽀持. 这四种隔离级别分别是:
- 读未提交(READ UNCOMMITTED): 读未提交, 也叫未提交读. 该隔离级别的事务可以看到其他事务中未提交的数据.
因为其他事务未提交的数据可能会发⽣回滚, 但是该隔离级别却可以读到, 我们把该级别读到的数据称之为脏数据, 这个问题称之为脏读.
比特就业课
- 读提交(READ COMMITTED): 读已提交, 也叫提交读. 该隔离级别的事务能读取到已经提交事务的数据.
该隔离级别不会有脏读的问题.但由于在事务的执⾏中可以读取到其他事务提交的结果, 所以在不同时间的相同 SQL 查询可能会得到不同的结果, 这种现象叫做不可重复读
- 可重复读(REPEATABLE READ): 事务不会读到其他事务对已有数据的修改, 即使其他事务已提交. 也就可以确保同⼀事务多次查询的结果⼀致, 但是其他事务新插⼊的数据, 是可以感知到的. 这也就引发了幻读问题. 可重复读, 是 MySQL 的默认事务隔离级别.
⽐如此级别的事务正在执⾏时, 另⼀个事务成功的插⼊了某条数据, 但因为它每次查询的结果都是⼀样的, 所以会导致查询不到这条数据, ⾃⼰重复插⼊时⼜失败(因为唯⼀约束的原因). 明明在事务中查询不到这条信息,但⾃⼰就是插⼊不进去, 这个现象叫幻读.
- 串⾏化(SERIALIZABLE): 序列化, 事务最⾼隔离级别. 它会强制事务排序, 使之不会发⽣冲突, 从⽽解决了脏读, 不可重复读和幻读问题, 但因为执⾏效率低, 所以真正使⽤的场景并不多
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 (READ UNCOMMITTED) | √ | √ | √ |
读已提交 (READ COMMITTED) | × | √ | √ |
可重复读 (REPEATABLE READ) | × | × | √ |
串行化 (SERIALIZABLE) | × | × | × |
2.2 Spring事务隔离级别
Spring 中事务隔离级别有5 种:
- Isolation.DEFAULT : 以连接的数据库的事务隔离级别为主.
- Isolation.READ_UNCOMMITTED : 读未提交, 对应SQL标准中 READ UNCOMMITTED
- Isolation.READ_COMMITTED : 读已提交,对应SQL标准中 READ COMMITTED
- Isolation.REPEATABLE_READ : 可重复读, 对应SQL标准中 REPEATABLE READ
- Isolation.SERIALIZABLE : 串⾏化, 对应SQL标准中 SERIALIZABLE
2.3 ioslation 属性
Spring 中事务隔离级别可以通过 @Transactional 中的 isolation 属性进⾏设置
赋值时需要使用Isolation枚举类:
public enum Isolation {
DEFAULT(-1),
READ_UNCOMMITTED(1),
READ_COMMITTED(2),
REPEATABLE_READ(4),
SERIALIZABLE(8);
private final int value;
private Isolation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
举例:
@Slf4j
@Service
public class UserInfoService {
@Autowired
private LogInfoMapper logInfoMapper;
@Autowired
private UserInfoMapper userInfoMapper;
// Isolation.READ_COMMITTED 设置读已提交
@Transactional(isolation = Isolation.READ_COMMITTED)
public boolean updateUserInfo(LogInfo logInfo) throws IOException {
// 处理 from 向 to 转账 10
// 事务逻辑
// 1.from 的账户 -10
userInfoMapper.updateDelete(logInfo.getFrom(),logInfo.getNum());
// 2.to 的账户 +10
userInfoMapper.updateAdd(logInfo.getTo(),logInfo.getNum());
// 3. 记录日志
logInfoMapper.insert(logInfo);
// 故意设置抛异常
if(true){
throw new IOException();
}
return true;
}
}
3.Spring事务传播机制
3.1 什么是事务传播机制?
事务传播机制就是: 多个事务⽅法存在调⽤关系时, 事务是如何在这些⽅法间进⾏传播的.
⽐如有两个⽅法A, B都被 @Transactional 修饰, A⽅法调⽤B⽅法
A⽅法运⾏时, 会开启⼀个事务. 当A调⽤B时, B⽅法本⾝也是事务, 此时B⽅法运⾏时, 是加⼊A的事务, 还是创建⼀个新的事务呢?
这个就涉及到了事务的传播机制.
⽐如公司流程管理
执⾏任务之前, 需要先写执⾏⽂档, 任务执⾏结束, 再写总结汇报,此时A部⻔有⼀项⼯作, 需要B部⻔的⽀援, 此时B部⻔是直接使⽤A部⻔的⽂档, 还是新建⼀个⽂档呢?
事务隔离级别解决的是多个事务同时调⽤⼀个数据库的问题:
⽽事务传播机制解决的是⼀个事务在多个节点(⽅法)中传递的问题:
3.2 事务的传播机制有哪些
@Transactional 注解⽀持事务传播机制的设置, 通过 propagation 属性来指定传播⾏为
Spring 事务传播机制有以下 7 种:
1.Propagation.REQUIRED
: 默认的事务传播级别. 如果当前存在事务, 则加⼊该事务. 如果当前没有事务, 则创建⼀个新的事务.
2. Propagation.SUPPORTS
: 如果当前存在事务, 则加⼊该事务. 如果当前没有事务, 则以非事务的⽅式继续运行.
3. Propagation.MANDATORY
:强制性. 如果当前存在事务, 则加⼊该事务. 如果当前没有事务, 则抛出异常.
4. Propagation.REQUIRES_NEW
: 创建⼀个新的事务. 如果当前存在事务, 则把当前事务挂起. 也就是说不管外部⽅法是否开启事务, Propagation.REQUIRES_NEW 修饰的内部⽅法都会新开
启⾃⼰的事务, 且开启的事务相互独⽴, 互不⼲扰.
5. Propagation.NOT_SUPPORTED
: 以⾮事务⽅式运⾏, 如果当前存在事务, 则把当前事务挂起(不⽤).
6. Propagation.NEVER
: 以⾮事务⽅式运⾏, 如果当前存在事务, 则抛出异常.
6. Propagation.NESTED
: 如果当前存在事务, 则创建⼀个事务作为当前事务的嵌套事务来运⾏.如果当前没有事务, 则该取值等价于 PROPAGATION_REQUIRED
枚举类Propagation:
public 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;
}
}
3.3 Spring 事务传播机制使用
对于以上事务传播机制,我们重点关注以下两个就可以了:
- REQUIRED(默认值)
- REQUIRES_NEW
3.3.1 Propagation.REQUIRED(加入事务)
程序设计:
在controller中依次调用两个service,传播机制都设置为Propagation.REQUIRED。
(1)Controller层:
@RequestMapping("/user")
@RestController
public class UserInfoController {
@Autowired
private UserInfoService userInfoService;
@Autowired
private LogInfoService logInfoService;
@RequestMapping("/update1")
@Transactional(propagation = Propagation.REQUIRED)
public boolean updateUserInfo(@RequestBody LogInfo logInfo) {
// 1. 更新用户信息
boolean flag1 = userInfoService.updateUserInfo(logInfo);
// 2.插入日志
boolean flag2 = logInfoService.insert(logInfo);
if(flag1 && flag2){
return true;
}
return false;
}
}
(1)Service:
@Slf4j
@Service
public class UserInfoService {
@Autowired
private LogInfoMapper logInfoMapper;
@Autowired
private UserInfoMapper userInfoMapper;
@Transactional(propagation = Propagation.MANDATORY)
public boolean updateUserInfo(LogInfo logInfo) {
// 处理 from 向 to 转账 10
// 事务逻辑
// 1.from 的账户 -10
userInfoMapper.updateDelete(logInfo.getFrom(),logInfo.getNum());
// 2.to 的账户 +10
userInfoMapper.updateAdd(logInfo.getTo(),logInfo.getNum());
// 3. 记录日志
// 在LogInfoService实现
return true;
}
}
(2)Service(故意在该方法中设置一个错误):
@Service
public class LogInfoService {
@Autowired
private LogInfoMapper logInfoMapper;
@Transactional(propagation = Propagation.REQUIRED)
public boolean insert(LogInfo logInfo){
//错误点
int n = 10/0;
//记录日志
int flag = logInfoMapper.insert(logInfo);
if(flag>0 && n>=0){
return true;
}
return false;
}
}
使用postman测试:
运行结果:
在运行结果中可以看到,会话只是释放了没有提交
MySQL中的数据(原来的数据:zangsan–55;lisi–145):
可以看到数据没有发生改变
事务的流程:
运⾏程序, 发现数据库没有插⼊任何数据.
流程描述:
(1)Controller.updateUserInfo方法开始事务
(2) userInfoService.updateUserInfo(logInfo)(执⾏成功) (和Controller.updateUserInfo 使⽤同⼀个事务)
(3)记录操作⽇志, 插⼊⼀条数据(出现异常, 执⾏失败) (和Controller.updateUserInfo 使⽤同⼀个事务)
(5)因为步骤3出现异常, 事务回滚;步骤2和3使⽤同⼀个事务, 所以步骤2的数据也回滚了.
3.3.2 Propagation.REQUIRES_NEW(新建事务)
程序设计:
在controller中依次调用两个service,传播机制都设置为Propagation.REQUIRES_NEW。
(1)ontroller层:
@RequestMapping("/user")
@RestController
public class UserInfoController {
@Autowired
private UserInfoService userInfoService;
@Autowired
private LogInfoService logInfoService;
@RequestMapping("/update1")
@Transactional(propagation = Propagation.REQUIRES_NEW)
public boolean updateUserInfo(@RequestBody LogInfo logInfo) {
// 1. 更新用户信息
boolean flag1 = userInfoService.updateUserInfo(logInfo);
// 2.插入日志
boolean flag2 = logInfoService.insert(logInfo);
if(flag1 && flag2){
return true;
}
return false;
}
}
(1)Service:
@Slf4j
@Service
public class UserInfoService {
@Autowired
private LogInfoMapper logInfoMapper;
@Autowired
private UserInfoMapper userInfoMapper;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public boolean updateUserInfo(LogInfo logInfo) {
// 处理 from 向 to 转账 10
// 事务逻辑
// 1.from 的账户 -10
userInfoMapper.updateDelete(logInfo.getFrom(),logInfo.getNum());
// 2.to 的账户 +10
userInfoMapper.updateAdd(logInfo.getTo(),logInfo.getNum());
// 3. 记录日志
// 在LogInfoService实现
return true;
}
}
(2)Service(故意在该方法中设置一个错误):
@Service
public class LogInfoService {
@Autowired
private LogInfoMapper logInfoMapper;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public boolean insert(LogInfo logInfo){
//错误点
int n = 10/0;
//记录日志
int flag = logInfoMapper.insert(logInfo);
if(flag>0 && n>=0){
return true;
}
return false;
}
}
使用postman测试:
运行结果:
在运行结果中可以看到,有一个会话提交了
MySQL中的数据(原来的数据:zangsan–55;lisi–145):
可以看到user_info中两个用户的数据发生改变,但是log_info中的数据没有发生改变。
流程描述:
(1)Controller.updateUserInfo方法开始事务
(2) userInfoService.updateUserInfo(logInfo)(开新的事物并执⾏成功) (和Controller.updateUserInfo 使用的不是同⼀个事务)
(3)记录操作⽇志, 插⼊⼀条数据(开新的事物,出现异常, 执⾏失败,该事物回滚)
(5)步骤3出现异常, 事务回滚,但是步骤2和3不使用同⼀个事务, 两个事物相互独立,步骤二的事物提交。
3.3.3 Propagation.NEVER (不⽀持当前事务, 抛异常)
在UserInfoService中的方法中把传播机制改为 Propagation.NEVER,其他的传播机制都设置为Propagation.REQUIRES_NEW。
UserInfoService:
@Slf4j
@Service
public class UserInfoService {
@Autowired
private LogInfoMapper logInfoMapper;
@Autowired
private UserInfoMapper userInfoMapper;
// @Transactional(propagation = Propagation.REQUIRES_NEW)
@Transactional(propagation = Propagation.NEVER)
public boolean updateUserInfo(LogInfo logInfo) {
// 处理 from 向 to 转账 10
// 事务逻辑
// 1.from 的账户 -10
userInfoMapper.updateDelete(logInfo.getFrom(),logInfo.getNum());
// 2.to 的账户 +10
userInfoMapper.updateAdd(logInfo.getTo(),logInfo.getNum());
// 3. 记录日志
// 在LogInfoService实现
return true;
}
}
LogInfoService:
@Service
public class LogInfoService {
@Autowired
private LogInfoMapper logInfoMapper;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public boolean insert(LogInfo logInfo) {
//记录日志
int flag = logInfoMapper.insert(logInfo);
if (flag > 0) {
return true;
}
return false;
}
}
使用postman测试:
运行结果:
执行程序报错,没有数据插入
3.3.4 Propagation.NESTED(嵌套事务)
将上述UserService 和LogService 中相关⽅法事务传播机制改为 Propagation.NESTED
(1)Controller层:
@RequestMapping("/user")
@RestController
public class UserInfoController {
@Autowired
private UserInfoService userInfoService;
@Autowired
private LogInfoService logInfoService;
@RequestMapping("/update1")
@Transactional(propagation = Propagation.REQUIRED)
public boolean updateUserInfo(@RequestBody LogInfo logInfo) {
// 1. 更新用户信息
boolean flag1 = userInfoService.updateUserInfo(logInfo);
// 2.插入日志
boolean flag2 = logInfoService.insert(logInfo);
if(flag1 && flag2){
return true;
}
return false;
}
}
(1)Service:
@Slf4j
@Service
public class UserInfoService {
@Autowired
private LogInfoMapper logInfoMapper;
@Autowired
private UserInfoMapper userInfoMapper;
@Transactional(propagation = Propagation.NESTED)
public boolean updateUserInfo(LogInfo logInfo) {
// 处理 from 向 to 转账 10
// 事务逻辑
// 1.from 的账户 -10
userInfoMapper.updateDelete(logInfo.getFrom(),logInfo.getNum());
// 2.to 的账户 +10
userInfoMapper.updateAdd(logInfo.getTo(),logInfo.getNum());
// 3. 记录日志
// 在LogInfoService实现
return true;
}
}
(2)Service(故意在该方法中设置一个错误):
@Service
public class LogInfoService {
@Autowired
private LogInfoMapper logInfoMapper;
@Transactional(propagation = Propagation.NESTED)
public boolean insert(LogInfo logInfo){
//错误点
int n = 10/0;
//记录日志
int flag = logInfoMapper.insert(logInfo);
if(flag>0 && n>=0){
return true;
}
return false;
}
}
使用postman测试:
运行结果:
在运行结果中可以看到,会话只是释放了没有提交
MySQL中的数据(原来的数据:zangsan–40;lisi–160):
可以看到数据没有发生改变
事务的流程:
运⾏程序, 发现数据库没有插⼊任何数据.
流程描述:
(1)Controller.updateUserInfo方法开始事务
(2) userInfoService.updateUserInfo(logInfo)(开启一个事务,嵌套在Controller事务中,执⾏成功) (和Controller.updateUserInfo 使⽤同⼀个大事务)
(3)记录操作⽇志, 插⼊⼀条数据(开启一个事务,嵌套在Controller事务中,出现异常, 执⾏失败) (和Controller.updateUserInfo 使⽤同⼀个大事务)
(5)因为步骤3出现异常, 事务回滚;步骤2和3使⽤同⼀个事务, 所以步骤2的数据也回滚了.
3.4 REQUIRED和NESTED的区别
3.4.1 NESTED–某一嵌套事务回滚
Controller和Service都使用NESTED
logInfoService嵌套事务异常回滚:
@Slf4j
@Service
public class LogInfoService {
@Autowired
private LogInfoMapper logInfoMapper;
@Transactional(propagation = Propagation.NESTED)
public boolean insert(LogInfo logInfo){
try {
int n = 10/0;
//记录日志
int flag = logInfoMapper.insert(logInfo);
if(flag>0 && n>=0){
return true;
}
}catch (Exception e){
//手动回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
log.info("日志事务回滚");
return true;
}
return false;
}
}
使用postman测试:
运行结果:
MySQL数据:
log_info中的数据没有发生改变,但是user_info数据发生了改变
流程描述:
(1)Controller.updateUserInfo方法开始事务
(2) userInfoService.updateUserInfo(logInfo)(开启一个事务,嵌套在Controller事务中,执⾏成功) (和Controller.updateUserInfo 使⽤同⼀个大事务)
(3)记录操作⽇志, 插⼊⼀条数据(开启一个事务,嵌套在Controller事务中,出现异常并捕获,手动回滚事务) (和Controller.updateUserInfo 使⽤同⼀大个事务)
(5)步骤3出现异常, 但是捕获了并手动回滚事务,没有发生异常;步骤2和3使⽤同⼀个事务, 步骤3相当于来说执行成功,所以步骤2的数据提交了。
3.4.2 REQUIRED–部分事务回滚
Controller和Service都使用REQUIRED
logInfoService部分事务异常回滚:
@Slf4j
@Service
public class LogInfoService {
@Autowired
private LogInfoMapper logInfoMapper;
@Transactional(propagation = Propagation.REQUIRED)
public boolean insert(LogInfo logInfo){
try {
int n = 10/0;
//记录日志
int flag = logInfoMapper.insert(logInfo);
if(flag>0 && n>=0){
return true;
}
}catch (Exception e){
//手动回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
log.info("日志事务回滚");
return true;
}
return false;
}
}
使用postman测试:
运行结果:
事务回滚后没有提交事务
MySQL数据:
log_info和user_info中的数据没有发生改变
流程描述:
(1)Controller.updateUserInfo方法开始事务
(2) userInfoService.updateUserInfo(logInfo)(加入Controller事务中,执⾏成功) (和Controller.updateUserInfo 使⽤同⼀个事务)
(3)记录操作⽇志, 插⼊⼀条数据(加入Controller事务中,出现异常并捕获,手动回滚事务) (和Controller.updateUserInfo 使⽤同⼀个事务)
(5)步骤3出现异常, 但是捕获了并手动回滚事务,没有发生异常;步骤2和3使⽤同⼀个事务, 在同一个事务中不能实现部分回滚,所以步骤2也回滚了。
3.4.3 总结
NESTED和REQUIRED区别:
(1)整个事务如果全部执⾏成功, ⼆者的结果是⼀样的.
(2)如果事务⼀部分执⾏成功, REQUIRED加⼊事务会导致整个事务全部回滚. NESTED嵌套事务可以实现局部回滚, 不会影响上⼀个⽅法中执⾏的结果.
嵌套事务之所以能够实现部分事务的回滚, 是因为事务中有⼀个保存点(savepoint)的概念, 嵌套事务进⼊之后相当于新建了⼀个保存点, 而滚回时只回滚到当前保存点.
资料参考: MySQL官方文档
4. 总结
- Spring中使⽤事务, 有两种⽅式: 编程式事务(⼿动操作)和声明式事务. 其中声明式事务使⽤较多,在⽅法上添加 @Transactional 就可以实现了
- 通过 @Transactional(isolation = Isolation.SERIALIZABLE) 设置事务的隔离级别. Spring 中的事务隔离级别有 5 种
- 通过 @Transactional(propagation = Propagation.REQUIRED) 设置事务的传播机制, Spring 中的事务传播级别有 7 种, 重点关注 REQUIRED (默认值) 和 REQUIRES_NEW