spring事务传播机制代码演示

512 阅读7分钟

spring事务传播,我在面试中被问到很多次,所以有必要学习一下。还有分布式事务,真的是问惨了~~~

(一) 先看下spring中有哪些事务传播级别

1. REQUIRED ( Spring默认的事务传播类型 )

  • 支持当前事务,如果当前没有事务,则新建事务
  • 如果当前存在事务,则加入当前事务,合并成一个事务

2. SUPPORTS

  • 如果当前存在事务,则加入事务
  • 如果当前不存在事务,则以非事务方式运行,这个和不写没区别

3. MANDATORY

  • 如果当前存在事务,则运行在当前事务中
  • 如果当前无事务,则抛出异常,即父级方法必须有事务

4. REQUIRES_NEW

  • 新建事务,如果当前存在事务,则把当前事务挂起
  • 这个(REQUIRES_NEW)会独立提交事务,不受调用者的事务影响,父级异常,它也是正常提交

5. NOT_SUPPORTED

  • 以非事务方式运行
  • 如果当前(调用者)存在事务,则把当前事务挂起

6. NEVER

  • 以非事务方式运行,如果当前存在事务,则抛出异常,即父级方法必须无事务 霸道 哈哈~

7. NESTED

  • 如果当前存在事务,它将会成为父级事务的一个子事务,方法结束后并没有提交,只有等父事务结束才提交
  • 如果当前没有事务,则新建事务
  • 如果它异常,父级可以捕获它的异常而不进行回滚,正常提交
  • 但如果父级异常,它必然回滚,这也是和 REQUIRES_NEW 的主要区别 (REQUIRES_NEW是 如果父级异常的话,他任然会执行事务)

(二) 准备环境和代码 代码均上传到了github 戳这里

建两张表 DDL如下:

CREATE TABLE `table_a` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `a_description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
  `enabled` tinyint NOT NULL DEFAULT '0',
  `cuser_id` int NOT NULL DEFAULT '0',
  `uuser_id` int NOT NULL DEFAULT '0',
  `ctime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `utime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=34 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

CREATE TABLE `table_b` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `b_description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
  `enabled` tinyint NOT NULL DEFAULT '0',
  `cuser_id` int NOT NULL DEFAULT '0',
  `uuser_id` int NOT NULL DEFAULT '0',
  `ctime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `utime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=35 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

创建对相应的DO mapper service 略。

(三) 用代码说话

1. REQUIRED ( Spring默认的事务传播类型 )

  • 支持当前事务,如果当前没有事务,则新建事务
  • 如果当前存在事务,则加入当前事务,合并成一个事务

image.png

可以看到 a和b表都没有数据,说明saveB方法中的异常不仅影响SAVEb方法,还会使得saveA方法也回滚 image.png 上个示例印证了 如果当前存在事务,则加入当前事务,合并成一个事务

下边我们把saveA方法的事务注解注释掉看看

image.png

可以看到a表成功插入,b表没有数据 (这个很好理解 因为saveA没加事务嘛) (注意此处的两个方法不再同一个类中,如果在同一个类中的话,saveB将不会生效 具体原因见 另一篇文章: juejin.cn/user/123990…)

image.png

这个印证了 如果当前没有事务,则自己新建一个事务 因为saveA没有事务,所以saveB新建了个事务。(注意可这里的当前可不是指saveA方法啊)

2. SUPPORTS

  • 如果当前存在事务,则加入事务
  • 如果当前不存在事务,则以非事务方式运行,这个和不写没区别

image.png

a表数据: image.png

b表数据: image.png

可以看到 a插入成功,虽然b插入失败,但是并没有回滚,异常前的那条数据依然插入成功,说明和没加事务是一样的。也就印证了 (如果当前(也就是saveA,因为我单元测试是调用的saveA方法)不存在事务,则以非事务方式运行)

而如果saveA方法上有事务且是默认传播(REQUIRED)时候 出现异常的情况下,a和b表的数据都会回滚

我们来看下saveA上有事务的情况

image.png

image.png 这个就印证了 SUPPORTS级别下 (如果当前(调用者)存在事务,则加入事务) 这个规则

我们看下如果saveA方法上的传播级别是 SUPPORTS 的话

image.png

那么数据库中

image.png

image.png

可以看到 和没加事务一样 呵呵

3. MANDATORY

  • 如果当前存在事务,则运行在当前事务中
  • 如果当前无事务,则抛出异常,即父级方法必须有事务

image.png

image.png 注意a插入成功,b无数据,这里b没有数据并不是事务的回滚,而是a没有事务,a的insert正常执行,而b的方法在调用时候就失败了。所以不会入库。

也就印证了 (如果当前无事务,则抛出异常,即父级方法必须有事务) 而如果在saveA方法进行事务声明,并且设置为REQUIRED,则执行saveB时就会使用saveA已经开启的事务,遇到异常就正常的回滚了. 即 (如果当前存在事务,则运行在当前事务中)

4. REQUIRES_NEW

  • 新建事务,如果当前存在事务,则把当前事务挂起
  • 这个(REQUIRES_NEW)会独立提交事务,不受调用者的事务影响,父级异常,它也是正常提交

image.png

image.png 从结果上来看 印证了 REQUIRES_NEW 会独立提交事务,不受调用者的事务影响,父级(调用者)异常,它也是正常提交

5. NOT_SUPPORTED

  • 以非事务方式运行
  • 如果当前(调用者)存在事务,则把当前事务挂起

image.png

看下结果

image.png

该场景的执行结果就是 ‘第一次插入A表描述’ 和’第二次插入B表描述‘ 没有存储,而‘第一次插入B表描述’存储成功。原因: saveA有事务,而saveB不使用事务(NOT_SUPPORTED),所以执行中saveB的第一条数据存储成功,然后抛出异常,此时saveA检测到异常事务发生回滚,但是由于saveB不在事务中,所以只有saveA的insert发生了回滚,最终只有saveB的第一个insert存储成功,而saveA的insert (回滚了)和saveB的 第二个insert(这一步根本没走到)都没有存储成功。

6. NEVER

  • 以非事务方式运行,如果当前存在事务,则抛出异常,即父级方法必须无事务 霸道 哈哈~

image.png

看下结果: image.png

7. NESTED

  • 如果当前存在事务,它将会成为父级事务的一个子事务,方法结束后并没有提交,只有等父事务结束才提交
  • 如果当前没有事务,则新建事务
  • 如果它异常,父级可以捕获它的异常而不进行回滚,正常提交
  • 但如果父级异常,它必然回滚,这也是和 REQUIRES_NEW 的主要区别 (REQUIRES_NEW是 如果父级异常的话,他任然会执行事务)

这里需要注意两点:

  • 和REQUIRES_NEW的区别

REQUIRES_NEW是新建一个事务并且新开启的这个事务与原有事务无关,而NESTED则是当前存在事务时(我们把当前事务称之为父事务)会开启一个嵌套事务(称之为一个子事务)。
在NESTED情况下父事务回滚时,子事务也会回滚,而在REQUIRES_NEW情况下,原有事务回滚,不会影响新开启的事务 (REQUIRES_NEW本质是新创建一个事务,事务之间是隔离的,所以肯定不会影响啦)。

  • 和REQUIRED的区别

REQUIRED情况下,调用方存在事务时,则被调用方和调用方使用同一事务,那么被调用方出现异常时,由于共用一个事务,所以无论调用方是否catch其异常,事务都会回滚
而在NESTED情况下,被调用方发生异常时,调用方可以catch其异常,这样只有子事务回滚,父事务不受影响

该场景下,所有数据都不会存入数据库,因为在saveA发生异常时,父事务回滚则子事务也跟着回滚了,可以与 REQUIRES_NEW中的示例 比较看一下,就能找出与REQUIRES_NEW的不同

image.png

可以看到 都没有数据 (因为父事务发生异常回滚,嵌套事务也会回滚) image.png

将异常移到saveB中看下 image.png

image.png

将异常移到saveB中并在saveA中catch看下

image.png

看下结果 image.png

可以看到 父事务catch后,可以正常提交父事务的内容,但是嵌套事务中肯定是回滚了。

本文完
明天决心把分布式事务学学,真的是被这个绊倒过很多次了。古人不是说了 ”不能被同一块石头绊倒“ 吗? 对不对?