博客链接:https://blog.csdn.net/yyhgo_/article/details/128050926
事务定义: 将⼀组操作封装成⼀个执⾏单元(封装到⼀起),要么全部成功,要么全部失败。
为什么要用事务?
⽐如转账分为两个操作:
第⼀步操作:A 账户 -100 元。
第⼆步操作:B 账户 +100 元。
如果没有事务,第⼀步执⾏成功了,第⼆步执⾏失败了,那么 A 账户平⽩⽆故的 100 元就“⼈间蒸发”了。⽽如果使⽤事务就可以解决这个问题,让这⼀组操作要么⼀起成功,要么⼀起失败。
Spring 中的事务操作分为两类:
在开始讲解它们之前,咱们先来回顾事务在 MySQL 中是如何使⽤的?
事务在 MySQL 有 3 个重要的操作:开启事务、提交事务、回滚事务,它们对应的操作命令如下:
-- 开启事务
start transaction;
-- 业务执⾏
-- 提交事务
commit;
-- 回滚事务
rollback;
Spring ⼿动操作事务和上⾯ MySQL 操作事务类似,它也是有 3 个重要操作步骤:
SpringBoot 内置了两个对象,DataSourceTransactionManager ⽤来获取事务(开启事务)、提交或回滚事务的,⽽ TransactionDefinition 是事务的属性,在获取事务的时候需要将 TransactionDefinition 传递进去从⽽获得⼀个事务 TransactionStatus,实现代码如下:
@RestController
public class UserController {
@Resource
private UserService userService;
// JDBC 事务管理器
@Resource
private DataSourceTransactionManager dataSourceTransactionManager;
// 定义事务属性
@Resource
private TransactionDefinition transactionDefinition;
@RequestMapping("/sava")
public Object save(User user) {
// 开启事务
TransactionStatus transactionStatus = dataSourceTransactionManager
.getTransaction(transactionDefinition);
// 插⼊数据库
int result = userService.save(user);
// 提交事务
dataSourceTransactionManager.commit(transactionStatus);
// // 回滚事务
// dataSourceTransactionManager.rollback(transactionStatus);
return result;
}
}
从上述代码可以看出,以上代码虽然可以实现事务,但操作也很繁琐,有没有更简单的实现⽅法呢?请看下⾯声明式事务。
声明式事务的实现很简单,只需要在需要的方法上添加 @Transactional 注解就可以实现了,无需手动开启事务和提交事务,进入方法时自动开启事务,方法执行完会自动提交事务,如果中途发生了没有处理的异常会自动回滚事务, 具体实现代码如下:
@RequestMapping("/save")
@Transactional
public Object save(User user) {
int result = userService.save(user);
return result;
}
接下⾥使⽤以下代码,分别设置 @Transactional 注解和不设置 @Transactional,观察它们的执⾏区别:
@Transactional 可以⽤来修饰⽅法或类:
参数 | 作用 |
---|---|
value | 当配置了多个事务管理器时,可以使用该属性指定选择哪个事务管理器. |
transactionManager | 当配置了多个事务管理器时,可以使用该属性指定选择哪个事务管理器. |
propagation | 事务的传播行为,默认值为Propagation.REQUIRED |
isolation | 事务的隔离级别,默认值为lsolation.DEFAULT |
timeout | 事务的超时时间,默认值为-1.如果超过该时间限制但事务还没有完成,则自动回滚事务. |
readOnly | 指定事务是否为只读事务,默认值为false;为了忽略那些不需要事务的方法,比如读取数据,可以设置read-only为 true. |
rollbackFor | 用于指定能够触发事务回滚的异常类型,可以指定多个异常类型. |
rollbackForClassName | 用于指定能够触发事务回滚的异常类型,可以指定多个异常类型. |
noRollbackFor | 抛出指定的异常类型,不回滚事务,也可以指定多个异常类型. |
noRollbackForClassName | 抛出指定的异常类型,不回滚事务,也可以指定多个异常类型. |
@Transactional 在异常被捕获的情况下,不会进行事务自动回滚, 验证以下代码是否会发⽣事务回滚:
@RestController
public class UserController {
@Resource
private UserService userService;
@RequestMapping("/save")
@Transactional
public Object save(User user) {
// 插⼊数据库
int result = userService.save(user);
try {
// 执⾏了异常代码(0不能做除数)
int i = 10 / 0;
} catch (Exception e) {
System.out.println(e.getMessage());
}
return result;
}
}
事务不会自动回滚解决方案:
解决⽅案1:对于捕获的异常,事务是会⾃动回滚的,因此解决⽅案1就是可以将异常重新抛出,具体实现如下:
@RequestMapping("/save")
@Transactional(isolation = Isolation.SERIALIZABLE)
public Object save(User user) {
// 插⼊数据库
int result = userService.save(user);
try {
// 执⾏了异常代码(0不能做除数)
int i = 10 / 0;
} catch (Exception e) {
System.out.println(e.getMessage());
// 将异常重新抛出去
throw e;
}
return result;
}
解决⽅案2:⼿动回滚事务,在⽅法中使⽤ TransactionAspectSupport.currentTransactionStatus() 可以得到当前的事务,然后设置回滚⽅法 setRollbackOnly 就可以实现回滚了,具体实现代码如下:
@RequestMapping("/save")
@Transactional(isolation = Isolation.SERIALIZABLE)
public Object save(User user) {
// 插⼊数据库
int result = userService.save(user);
try {
// 执⾏了异常代码(0不能做除数)
int i = 10 / 0;
} catch (Exception e) {
System.out.println(e.getMessage());
// ⼿动回滚事务
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return result;
}
@Transactional 是基于 AOP 实现的,AOP ⼜是使⽤动态代理实现的。如果⽬标对象实现了接⼝,默认情况下会采⽤ JDK 的动态代理,如果⽬标对象没有实现了接⼝,会使⽤ CGLIB 动态代理。
@Transactional 在开始执⾏业务之前,通过代理先开启事务,在执⾏成功之后再提交事务。如果中途遇到的异常,则回滚事务。
@Transactional 实现思路预览:
@Transactional 具体执⾏细节如下图所示:
事务有4 ⼤特性(ACID),原⼦性、持久性、⼀致性和隔离性,具体概念如下:
Read uncommitted
)、读提交 (read committed
)、可重复读 (repeatable read
) 和串⾏化 (Serializable
)。上⾯ 4 个属性,可以简称为 ACID。
原⼦性(Atomicity,或称不可分割性)
⼀致性(Consistency)
隔离性(Isolation,⼜称独⽴性)
持久性(Durability)
⽽这 4 种特性中,只有隔离性(隔离级别)是可以设置的。
为什么要设置事务的隔离级别?
设置事务的隔离级别是⽤来保障多个并发事务执⾏更可控,更符合操作者预期的。
Spring 中事务隔离级别可以通过 @Transactional 中的 isolation 属性进⾏设置,具体操作如下图所示:
- 脏读:⼀个事务读取到了另⼀个事务修改的数据之后,后⼀个事务⼜进⾏了回滚操作,从⽽导致第⼀个事务读取的数据是错误的。
- 不可重复读:⼀个事务两次查询得到的结果不同,因为在两次查询中间,有另⼀个事务把数据修改了。
- 幻读:⼀个事务两次查询中得到的结果集不同,因为在两次查询中另⼀个事务有新增了⼀部分数据。
在数据库中通过以下 SQL 查询全局事务隔离级别和当前连接的事务隔离级别:
select @@global.tx_isolation,@@tx_isolation;
以上 SQL 的执⾏结果如下:
即 MySQL 的隔离级别默认为 可重复读REPEATABLE READ (Oracle 默认为 读已提交READ COMMITTED)。
⽽ Spring 中事务隔离级别包含以下 5 种:
从上述介绍可以看出,相⽐于 MySQL 的事务隔离级别,Spring 的事务隔离级别只是多了⼀个Isolation.DEFAULT(以数据库的全局事务隔离级别为主)。
Spring 中事务隔离级别只需要设置 @Transactional ⾥的 isolation 属性即可,具体实现代码如下:
@RequestMapping("/save")
@Transactional(isolation = Isolation.SERIALIZABLE)
public Object save(User user) {
// 业务实现
}
Spring 事务传播机制定义了多个包含了事务的⽅法,相互调⽤时,事务是如何在这些⽅法间进⾏传递的。
事务隔离级别是保证多个并发事务执⾏的可控性的(稳定性的),而事务传播机制是保证一个事务在多个调用方法间的可控性的(稳定性的)。
事务隔离级别解决的是多个事务同时调⽤⼀个数据库的问题,如下图所示:
而事务传播机制解决的是⼀个事务在多个节点(⽅法)中传递的问题,如下图所示:
Spring 事务传播机制包含以下 7 种:
以上 7 种传播⾏为,可以根据是否⽀持当前事务分为以下 3 类:
以情侣关系为例来理解以上分类:
REQUIRED 和 NESTED 演示。
加入事务:
UserController:
@RequestMapping("/add2")
@Transactional(propagation = Propagation.REQUIRED)
public int add2(String username, String password) {
// 非空效验
if (!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
return 0;
}
int result = userService.add(username, password, null);
System.out.println("添加影响行数:" + result);
int result2 = userService.save(username, password, null);
System.out.println("添加影响行数:" + result2);
return result;
}
UserService:
@Transactional(propagation = Propagation.REQUIRED)
public int add(String username, String password, String photo) {
return userMapper.add(username, password, photo);
}
@Transactional(propagation = Propagation.REQUIRED)
public int save(String username, String password, String photo) {
try {
int result = 10 / 0;
} catch (Exception e) {
System.out.println("ex:" + e.getMessage());
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return userMapper.add(username, password, photo);
}
最终数据库一行数据都没有添加。
嵌套事务:
修改 UserService:
@Transactional(propagation = Propagation.REQUIRED)
public int add(String username, String password, String photo) {
return userMapper.add(username, password, photo);
}
@Transactional(propagation = Propagation.NESTED)
public int save(String username, String password, String photo) {
try {
int result = 10 / 0;
} catch (Exception e) {
System.out.println("ex:" + e.getMessage());
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return userMapper.add(username, password, photo);
}
最终数据库只添加了一行数据 (嵌套事务不影响整个事务)。
嵌套事务之所以能够实现部分事务的回滚,是因为事务中有⼀个保存点(savepoint)的概念。嵌套事务进⼊之后相当于新建了⼀个保存点,⽽滚回时只回滚到当前保存点,因此之前的事务是不受影响的。这⼀点可以在 MySQL 的官⽅⽂档汇总找到相应的资料:https://dev.mysql.com/doc/refman/5.7/en/savepoint.html
而 REQUIRED 是加⼊到当前事务中,并没有创建事务的保存点,因此出现了回滚就是整个事务回滚,这就是加⼊事务和嵌套事务的区别。
- 整个事务如果全部执⾏成功,⼆者的结果是⼀样的。
- 如果事务执⾏到⼀半失败了,那么加⼊事务整个事务会全部回滚;⽽嵌套事务会局部回滚,不会影响上⼀个⽅法中执⾏的结果。