1.Spring 中事务的实现方式
Spring 中的操作主要分为两类:
编程式事务 (了解)
声明式事务
编程式事务就是手写代码操作事务, 而声明式事务是利用注解来自动开启和提交事务. 并且编程式事务用几乎不怎么用. 这就好比汽车的手动挡和自动挡, 如果有足够的的钱, 大部分人应该都会选择自动挡.
声明式事务也是如此, 它不仅好用, 还特别方便.
1.1 Spring 编程式事务 (了解)
编程式事务和 MySQL 中操作事务类似, 也是三个重要步骤:
开启事务
提交事务
回滚事务
【代码实现】
@RequestMapping("/user")
public class UserController1 {
@Autowired
private UserService userService;
@Autowired // JDBC 事务管理器
private DataSourceTransactionManager dataSourceTransactionManager;
@Autowired // 定义事务属性
private TransactionDefinition transactionDefinition;
@RequestMapping("/add")
public int add(String username, String password) {
// 非空校验
if(!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
return 0;
}
// 事务 [得到并开启事务]
TransactionStatus transactionStatus =
dataSourceTransactionManager.getTransaction(transactionDefinition);
int result = userService.add(username, password, null);
System.out.println("添加影响行数: " + result);
// 提交事务 or 回滚
dataSourceTransactionManager.rollback(transactionStatus);
// dataSourceTransactionManager.commit(transactionStatus);
return result;
}
}
DataSourceTransactionManager 和 TransactionDefinition 是 SpringBoot 内置的两个对象.
DataSourceTransactionManager : 用来获取事务(开启事务)、提交或回滚事务.
TransactionDefinition : 它是事务的属性,在获取事务的时候需要将这个对象传递进去从而获得⼀个事务 TransactionStatus.
【测试编式事务】
上述代码的主要要业务逻辑就是基于 MyBatis实现了一个新增方法, 接下来我们测试一下编程式事务中的回滚操作是否生效.
在测试之前先查看一下数据库中的用户信息 (userinfo) :
启动程序后, 在浏览器输入: 127.0.0.1:8080/user/add?username=李华&password=123
3. 此时我们看见控制台显示添加数据成功, 那么要知道代码中的回滚是否生效, 需要查看数据库是否真正的把数据添加进去了.
4. 发现数据库中并没有添加数据, 说明回滚操作生效了. 而提交事务 commit 就和普通的添加操作差不多, 下来可以自己试一下.
1.2 Spring 声明式事务
声明式事务的实现相较于编程式事务来说, 就要简单太多了, 只需要在需要的方法上添加 @Transactional注解就可以实现了.
@Transactional 注解的作用:
当进入方法的时候, 它就会自动开启事务, 当方法结束后, 它就会自动提交事务. 说白了它就是 AOP 的一个环绕通知. 只要加了 @Transactional 注解的方法, 都有一个事务的 AOP , 这都是 Spring 帮我们封装好的.
@Transactional 注解的执行流程:
1. 方法执行之前, 先开启事务, 当方法成功执行完成之后会自动提交事务.
2. 如果方法在执行过程中发生了异常, 那么事务会自动回滚.
【代码实现】
@RequestMapping("/add2")
@Transactional // 声明式事务
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);
return result;
}
对于方法执行成功的情况就不测试了, 它和普通的插入数据没有多大区别, 重点在于理解 @Transactional注解的含义和作用即可.
【异常情况一】
@RequestMapping("/add2")
@Transactional // 声明式事务
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 num = 10 / 0;
return result;
}
当我们写出 int num = 10 / 0; 这样一条语句的时候, 看看 @Transactional 是否会进行回滚操作:
启动程序, 浏览器访问 : 127.0.0.1:8080/user/add2?username=王五&password=123
此时程序已经报错了, 并且打印了添加成功语句, 是否真正添加成功, 还是说进行了回滚操作, 就要查询数据库:
发现数据库中并没有王五这条数据, 说明在发生异常的时候, @Transactional 注解帮我们做了回滚操作.
【异常情况二】
对于上述代码抛出异常后, @Transactional 注解帮我们进行回滚, 这一点很好理解, 那么如果我们将这个异常捕获了, @Transactional 注解是否还会进行回滚操作呢 >>
@RequestMapping("/add2")
@Transactional // 声明式事务
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);
try {
int num = 10 / 0;
} catch (Exception e) {
}
return result;
}
执行结果: 此时程序没有发生报错了.
为了验证是否进行回滚, 继续查询数据库
此时我们发现, @Transactional 注解并没有进行回滚操作, 而是提交了事务. 这是为什么 ??
因为当我们捕捉到异常的时候, Spring 框架会认为我们有能力处理, 所以就不会进行回滚, 而当发生异常我们不处理的时候, Spring 框架就会采取保守的做法, 他知道我们没有能力去处理这个异常, 所以就会帮我们回滚. 所以当出现异常的时候, 我们要根据这个异常是否被处理来判断最终是提交数据了, 还是进了回滚操作.
1.2.1 声明式事务的手动回滚
当第二种异常情况, 捕获异常之后, 事务并没有进行回滚, 我们是需要做出一些处理的. 既然程序发生了异常, 我们一般就需要进行回滚操作的. 对于这种捕获异常的情况,我们有两种方式进行回滚:
将异常继续抛出.
通过代码手动回滚事务.
【代码示例】- 将异常继续抛出
@RequestMapping("/add2")
@Transactional // 声明式事务
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);
try {
int num = 10 / 0;
} catch (Exception e) {
throw e; // 将异常继续抛出
}
return result;
}
测试代码是否回滚,还是和前面一样的操作,就不赘述了. 代码的最终执行结果肯定是进行了回滚操作.
【代码示例】- 手动回滚事务
@RequestMapping("/add2")
@Transactional // 声明式事务
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);
try {
int num = 10 / 0;
} catch (Exception e) {
// throw e;
System.out.println("程序发生异常: " + e.getMessage());
// 手动回滚事务 [得到当前事务并设置回滚] - 通过事务的切面拿到当前事务, 再设置回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return result;
}
手动回滚事务 : 通过事务的 AOP 拿到当前的事务, 然后设置回滚.
这种方式来处理事务的回滚, 显得更加优雅, 更推荐使用.
1.2.2 @Transactional 的工作原理
@Transactional 是基于 AOP实现的, AOP又是基于动态代理实现的 (JDK, CGLIB), 它在开始执行业务之前, 会通过代理实现开启事务, 在执行成功之后再提交事务, 如果中途出现了异常, 就会回滚事务.
@Transactional 实现思路
切面会拦截所有加了 @Transactional 注解的方法, 于是切点就有了, 然后开启事务与提交事务/回滚事务之间相当于是一个环绕通知.
2.事务隔离级别
在学 MySQL 的时候, 我们就已经知道了事务有四大特性: (ACID)
原子性 (Atomicity),
持久性(Consistency),
一致性 (Isolation) ,
隔离性 (Durability);
具体的概念在这篇博客中已经做过说明. - MySQL 事务的四大特性.
这四种特性中只有隔离性是可以设置的, 那么为什么要设置事务的隔离级别呢 ??
-- 为了保障多个并发事务执行更可控, 更符合操作者的预期.
2.1 Spring 中设置事务的隔离级别
MySQL 中事务的隔离级别分为四种:
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
读未提交 (READ UNCOMMITTED) | √ | √ | √ |
读已提交 (READ COMMITTED) | × | √ | √ |
可重复读 (REPEATABLE READ) | × | × | √ |
串行化 (SERIALIZABLE) | × | × | × |
MySQL 默认事务的隔离级别 : 可重复读, 可通过命令 select @@global.tx_isolation,@@tx_isolation; 来进行查看.
1. 脏读 : 一个事务A,在执行的过程中,对数据进行了一系列修改,在提交到数据库之前(完成事务之前),另一个事务B,读取了对应的数据,此时这个B读到的数据都是一些临时的结果,后续可能马上就被A给改了,此时B的读取行为就是"脏读"!
2. 不可重复读 : 事务A提交了事务之后,事务B才开始读(读的时候加了锁),然后B在执行的过程中,A再次开启了事务, 修改了 B 读取的数据,此时B执行中,就导致两次读取操作结果可能就不一致!(侧重于修改)
3. 幻读 : 事务B读取过程中,事务A进行了更新操作 ( 新增/删除/修改),没有直接影响B正在读取的数据,但是影响到了B读取的结果集,事务B两次读取到的结果集不一样,这个就是幻读!幻读相当于不可重复读的特殊情况。(侧重于新增和删除)
2.1.1 Spring 中事务的隔离级别
Spring 中事务的隔离级别有五个, MySQL 中的四个加上 DEFAULT 级别;
Isolation.DEFAULT : 以连接的数据库的事务隔离级别为主.(以数据库的全局事务隔离级别为主)
在 Spring 中如何设置事务的隔离级别>>>
@RequestMapping("/add")
@Transactional(isolation = Isolation.DEFAULT)
public int add(String username, String password) {
// 业务逻辑
}
3. Spring 事务的传播机制
什么是事务的传播机制 ??
在回答这个问题前, 先给大家举个例子 >>
1. 抛开事务的传播机制不说, 我们的业务就像 UU 跑腿一样, 你在你家附近买了一样东西, 然后让 UU跑腿的人送到你的手里, 如果送到了就没事 (commit), 如果没送到, 就可以找到对应跑腿的人 (或店家) 进行相应的赔偿 (rollback).
2. 可是实际生活中, 在网上买东西的人相对来说要多一些, 如果你在深圳, 但是在北京的一家网店上买了东西, 这时候就不可能叫 UU跑腿来送到你手里了. 那么快递一般都要经过很多个运输点, 这多个运输点对应着多批人, 如果你的快递在中途被弄丢了, 他们应该要怎样赔偿, 由谁来赔偿, 这就需要牵扯到了多个事务之间的传播机制了.
事务的隔离级别 : 解决的是多个事务同时调用数据库的问题!
而事务的传播机制解决的是一个事务在多个节点 (方法) 中传递问题!
上述例子将的就是上图中多个方法调用的时候, 发生异常应该要怎么去处理, 这就是传播机制的意义所在!
3.1 事务传播机制的级别
事务传播机制的级别分为 7 种:
1. Propagation.REQUIRED : 默认的事务传播级别, 它表示如果当前存在事务,则加入该事务; 如果当前没有事务, 则创建⼀个新的事务.
2. Propagation.SUPPORTS : 如果当前存在事务, 则加入该事务;如果当前没有事务,则以非事务的
方式继续运行.
3. Propagation.MANDATORY : (mandatory:强制性) 如果当前存在事务,则加入该事务; 如果当
前没有事务,则抛出异常.
4. Propagation.REQUIRES_NEW:表示创建一个新的事务, 如果当前存在事务, 则把当前事务挂
起. 也就是说不管外部方法是否开启事务, Propagation.REQUIRES_NEW 修饰的内部方法会新开
启自己的事务, 且开启的事务相互独立, 互不干扰.
5. Propagation.NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起.
6. Propagation.NEVER:以非事务方式运行,如果当前存在事务,则抛出异常.
7. Propagation.NESTED:如果当前存在事务,则 创建⼀个事务作为当前事务的嵌套事务来运行;如
果当前没有事务,则该取值等价于 Propagation.REQUIRED
这 7 中事务传播级别又可以分为三大类:
如果对于这三类事务传播机制, 不太理解的话, 下面把事务比做房子, 举一个生活中的例子 :
😁😁😁😁😁😁😁😁😁
支持当前事务 (普通伴侣)
REQUIRED (需要有) : 有房子就一起住, 没房子就一起赚钱买房子. (愿意陪你吃苦, 但一定要有房子)
SUPPORTS (可以有) : 有房子就一起住, 没房子就租房子住. (随缘的, 没房子也无所谓)
MANDATORY (强制有) : 有房子一起住, 没房子就分手. (不愿陪你吃苦)
不支持当前事务 (强势型伴侣)
REQUIRES_NEW : 不要你的房子, 咱们必须一起赚钱买房子. (看不上你的房子, 必须买新房子)
NOT_SUPPORTED : 不要你的房子, 咱们必须一起租房子. (不住你的房子, 必须租房子)
NEVER : 必须一起租房子, 你要有房子就分手. (看不上你的房子, 还得陪你环房贷)
嵌套事务 (懂事型伴侣)
NESTED : 有房子就以房子为根据地做点小生意, 赚钱了就继续发展, 赔钱至少还有房子; 如果没房子就一起赚钱买房子. (无风险创业, 保本懂事型伴侣)
对于上述3 类事务传播机制, 主要就是 REQUIRED (默认级别) 和 NESTED (嵌套事务) 不好区分>>
1. REQUIRED (默认级别) : 一荣俱荣, 一损俱损. 如果当前有事务, 执行过程中, 如果抛出异常, 那么就一起回滚, 如果否则一起提交.
2. NESTED (嵌套事务) : 如果当前有事务, 创建一个事务作为当前的嵌套事务来执行, 相当于在当前事务这里有一个保存点, 如果执行过程中嵌套事务抛出异常, 就回滚到保存点, 只回滚嵌套事务(局部回滚), 不会影响上一个方法中执行的结果.
【代码实现】 针对默认级别和嵌套事务的一个代码实现 >>
下面的代码针对 add 方法和 save 方法做了一个事务默认传播级别的测验, 两个方法都是添加方法, 如果途中没有抛异常, 数据库库就会新增两条数据, 否则一条也不新增.
Controller :
@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;
}
Service :
save 方法中有一个除 0 异常 >>
@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);
}
数据库当前信息 :
浏览器访问 add2 方法, 并传入参数, username=老六&password=123:
可以看到控制台打印了两次添加影响行数 : 1, 且出现一个除 0 异常, 那么是否真正插入到数据库中了, 需要查看一下数据库>>
发现并没有, 和上次查询的结果还是一样的. 所以符合我们的预期. (查看细致过程可以打断点进行调试)
【测试NESTED】
还是上述两个方法, 只不过把 Service 种的 save 方法的事务传播级别改为 NESTED.
浏览器访问 add2 方法, 并传入参数 usename=老六&passowrd=123
此时控制台依然打印了两次添加影响行数 : 1, 查询数据库验证插入情况 :
发现 add 方法的新增成功了, 而 save 方法的的新增回滚了, 也就是回滚到保存点, 这也符合我们的预期. (细致过程可以通过打断点的方式进行调试查看).
本篇文章就到这里了, 谢谢观看!!