飞道的博客

Spring 事务和事务的传播机制

292人阅读  评论(0)

1.Spring 中事务的实现方式

Spring 中的操作主要分为两类:

  • 编程式事务 (了解)

  • 声明式事务

编程式事务就是手写代码操作事务, 而声明式事务是利用注解来自动开启和提交事务. 并且编程式事务用几乎不怎么用. 这就好比汽车的手动挡和自动挡, 如果有足够的的钱, 大部分人应该都会选择自动挡.
声明式事务也是如此, 它不仅好用, 还特别方便.

1.1 Spring 编程式事务 (了解)

编程式事务和 MySQL 中操作事务类似, 也是三个重要步骤:

  1. 开启事务

  1. 提交事务

  1. 回滚事务

【代码实现】


   
  1. @RequestMapping("/user")
  2. public class UserController1 {
  3. @Autowired
  4. private UserService userService;
  5. @Autowired // JDBC 事务管理器
  6. private DataSourceTransactionManager dataSourceTransactionManager;
  7. @Autowired // 定义事务属性
  8. private TransactionDefinition transactionDefinition;
  9. @RequestMapping("/add")
  10. public int add (String username, String password) {
  11. // 非空校验
  12. if(!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
  13. return 0;
  14. }
  15. // 事务 [得到并开启事务]
  16. TransactionStatus transactionStatus =
  17. dataSourceTransactionManager.getTransaction(transactionDefinition);
  18. int result = userService.add(username, password, null);
  19. System.out.println( "添加影响行数: " + result);
  20. // 提交事务 or 回滚
  21. dataSourceTransactionManager.rollback(transactionStatus);
  22. // dataSourceTransactionManager.commit(transactionStatus);
  23. return result;
  24. }
  25. }
DataSourceTransactionManager 和 TransactionDefinition 是 SpringBoot 内置的两个对象.
DataSourceTransactionManager : 用来获取事务(开启事务)、提交或回滚事务.
TransactionDefinition : 它是事务的属性,在获取事务的时候需要将这个对象传递进去从而获得⼀个事务 TransactionStatus.

【测试编式事务】

上述代码的主要要业务逻辑就是基于 MyBatis实现了一个新增方法, 接下来我们测试一下编程式事务中的回滚操作是否生效.

  1. 在测试之前先查看一下数据库中的用户信息 (userinfo) :

  1. 启动程序后, 在浏览器输入: 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. 如果方法在执行过程中发生了异常, 那么事务会自动回滚.

【代码实现】


   
  1.     @RequestMapping("/add2")
  2. @Transactional // 声明式事务
  3. public int add2 (String username, String password) {
  4. // 非空校验
  5. if(!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
  6. return 0;
  7. }
  8. int result = userService.add(username, password, null);
  9. System.out.println( "添加影响行数: " + result);
  10. return result;
  11. }

对于方法执行成功的情况就不测试了, 它和普通的插入数据没有多大区别, 重点在于理解 @Transactional注解的含义和作用即可.

【异常情况一】


   
  1.     @RequestMapping("/add2")
  2. @Transactional // 声明式事务
  3. public int add2 (String username, String password) {
  4. // 非空校验
  5. if(!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
  6. return 0;
  7. }
  8. int result = userService.add(username, password, null);
  9. System.out.println( "添加影响行数: " + result);
  10.         int num = 10 / 0;
  11. return result;
  12. }

当我们写出 int num = 10 / 0; 这样一条语句的时候, 看看 @Transactional 是否会进行回滚操作:

启动程序, 浏览器访问 : 127.0.0.1:8080/user/add2?username=王五&password=123

此时程序已经报错了, 并且打印了添加成功语句, 是否真正添加成功, 还是说进行了回滚操作, 就要查询数据库:

发现数据库中并没有王五这条数据, 说明在发生异常的时候, @Transactional 注解帮我们做了回滚操作.

【异常情况二】

对于上述代码抛出异常后, @Transactional 注解帮我们进行回滚, 这一点很好理解, 那么如果我们将这个异常捕获了, @Transactional 注解是否还会进行回滚操作呢 >>


   
  1.     @RequestMapping("/add2")
  2. @Transactional // 声明式事务
  3. public int add2 (String username, String password) {
  4. // 非空校验
  5. if(!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
  6. return 0;
  7. }
  8. int result = userService.add(username, password, null);
  9. System.out.println( "添加影响行数: " + result);
  10. try {
  11. int num = 10 / 0;
  12. } catch (Exception e) {
  13. }
  14. return result;
  15. }

执行结果: 此时程序没有发生报错了.

为了验证是否进行回滚, 继续查询数据库

此时我们发现, @Transactional 注解并没有进行回滚操作, 而是提交了事务. 这是为什么 ??

因为当我们捕捉到异常的时候, Spring 框架会认为我们有能力处理, 所以就不会进行回滚, 而当发生异常我们不处理的时候, Spring 框架就会采取保守的做法, 他知道我们没有能力去处理这个异常, 所以就会帮我们回滚. 所以当出现异常的时候, 我们要根据这个异常是否被处理来判断最终是提交数据了, 还是进了回滚操作.

1.2.1 声明式事务的手动回滚

当第二种异常情况, 捕获异常之后, 事务并没有进行回滚, 我们是需要做出一些处理的. 既然程序发生了异常, 我们一般就需要进行回滚操作的. 对于这种捕获异常的情况,我们有两种方式进行回滚:

  • 将异常继续抛出.

  • 通过代码手动回滚事务.

【代码示例】- 将异常继续抛出


   
  1.     @RequestMapping("/add2")
  2. @Transactional // 声明式事务
  3. public int add2 (String username, String password) {
  4. // 非空校验
  5. if(!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
  6. return 0;
  7. }
  8. int result = userService.add(username, password, null);
  9. System.out.println( "添加影响行数: " + result);
  10. try {
  11. int num = 10 / 0;
  12. } catch (Exception e) {
  13. throw e; // 将异常继续抛出
  14. }
  15. return result;
  16. }

测试代码是否回滚,还是和前面一样的操作,就不赘述了. 代码的最终执行结果肯定是进行了回滚操作.

【代码示例】- 手动回滚事务


   
  1.     @RequestMapping("/add2")
  2. @Transactional // 声明式事务
  3. public int add2 (String username, String password) {
  4. // 非空校验
  5. if(!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
  6. return 0;
  7. }
  8. int result = userService.add(username, password, null);
  9. System.out.println( "添加影响行数: " + result);
  10. try {
  11. int num = 10 / 0;
  12. } catch (Exception e) {
  13. // throw e;
  14. System.out.println( "程序发生异常: " + e.getMessage());
  15. // 手动回滚事务 [得到当前事务并设置回滚] - 通过事务的切面拿到当前事务, 再设置回滚
  16. TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
  17. }
  18. return result;
  19. }
手动回滚事务 : 通过事务的 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 中如何设置事务的隔离级别>>>


   
  1. @RequestMapping("/add")
  2. @Transactional(isolation = Isolation.DEFAULT)
  3. public int add (String username, String password) {
  4. // 业务逻辑
  5. }

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 中事务传播级别又可以分为三大类:

如果对于这三类事务传播机制, 不太理解的话, 下面把事务比做房子, 举一个生活中的例子 :

😁😁😁😁😁😁😁😁😁

  1. 支持当前事务 (普通伴侣)

  • REQUIRED (需要有) : 有房子就一起住, 没房子就一起赚钱买房子. (愿意陪你吃苦, 但一定要有房子)

  • SUPPORTS (可以有) : 有房子就一起住, 没房子就租房子住. (随缘的, 没房子也无所谓)

  • MANDATORY (强制有) : 有房子一起住, 没房子就分手. (不愿陪你吃苦)

  1. 不支持当前事务 (强势型伴侣)

  • REQUIRES_NEW : 不要你的房子, 咱们必须一起赚钱买房子. (看不上你的房子, 必须买新房子)

  • NOT_SUPPORTED : 不要你的房子, 咱们必须一起租房子. (不住你的房子, 必须租房子)

  • NEVER : 必须一起租房子, 你要有房子就分手. (看不上你的房子, 还得陪你环房贷)

  1. 嵌套事务 (懂事型伴侣)

  • NESTED : 有房子就以房子为根据地做点小生意, 赚钱了就继续发展, 赔钱至少还有房子; 如果没房子就一起赚钱买房子. (无风险创业, 保本懂事型伴侣)

对于上述3 类事务传播机制, 主要就是 REQUIRED (默认级别) NESTED (嵌套事务) 不好区分>>

1. REQUIRED (默认级别) : 一荣俱荣, 一损俱损. 如果当前有事务, 执行过程中, 如果抛出异常, 那么就一起回滚, 如果否则一起提交.
2. NESTED (嵌套事务) : 如果当前有事务, 创建一个事务作为当前的嵌套事务来执行, 相当于在当前事务这里有一个保存点, 如果执行过程中嵌套事务抛出异常, 就回滚到保存点, 只回滚嵌套事务(局部回滚), 不会影响上一个方法中执行的结果.

【代码实现】 针对默认级别和嵌套事务的一个代码实现 >>

下面的代码针对 add 方法和 save 方法做了一个事务默认传播级别的测验, 两个方法都是添加方法, 如果途中没有抛异常, 数据库库就会新增两条数据, 否则一条也不新增.

Controller :


   
  1.     @RequestMapping("/add2")
  2. @Transactional(propagation = Propagation.REQUIRED)
  3. public int add2 (String username, String password) {
  4. // 非空校验
  5. if(!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
  6. return 0;
  7. }
  8. int result = userService.add(username, password, null);
  9. System.out.println( "添加影响行数: " + result);
  10. int result2 = userService.save(username, password, null);
  11. System.out.println( "添加影响行数: " + result2);
  12. return result;
  13. }

Service :

save 方法中有一个除 0 异常 >>


   
  1.     @Transactional(propagation = Propagation.REQUIRED)
  2. public int add (String username, String password, String photo) {
  3. return userMapper.add(username, password, photo);
  4. }
  5. @Transactional(propagation = Propagation.REQUIRED)
  6. public int save (String username, String password, String photo) {
  7. try {
  8. int result = 10 / 0;
  9. } catch (Exception e) {
  10. System.out.println( "ex: " + e.getMessage());
  11. TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
  12. }
  13. return userMapper.add(username, password, photo);
  14. }

数据库当前信息 :

浏览器访问 add2 方法, 并传入参数, username=老六&password=123:

可以看到控制台打印了两次添加影响行数 : 1, 且出现一个除 0 异常, 那么是否真正插入到数据库中了, 需要查看一下数据库>>

发现并没有, 和上次查询的结果还是一样的. 所以符合我们的预期. (查看细致过程可以打断点进行调试)

【测试NESTED】

还是上述两个方法, 只不过把 Service 种的 save 方法的事务传播级别改为 NESTED.

浏览器访问 add2 方法, 并传入参数 usename=老六&passowrd=123

此时控制台依然打印了两次添加影响行数 : 1, 查询数据库验证插入情况 :

发现 add 方法的新增成功了, 而 save 方法的的新增回滚了, 也就是回滚到保存点, 这也符合我们的预期. (细致过程可以通过打断点的方式进行调试查看).


本篇文章就到这里了, 谢谢观看!!


转载:https://blog.csdn.net/xaiobit_hl/article/details/128616530
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场