【JavaEE】深入解析 Spring 事务与传播机制:实现方式与应用场景

server/2024/10/18 6:07:47/

目录

    • Spring事务和事务传播机制
      • 事务回顾
        • 什么是事务
        • 为什么需要事务
        • 事务的操作
      • Spring 中事务的实现
        • Spring 编程式事务(了解)
      • Spring 声明式事务 @Transactional
      • @Transactional 详解
        • rollbackFor
        • 事务隔离级别
          • MySQL 事务隔离级别(回顾)
          • Spring 事务隔离级别
        • Spring 事务传播机制
          • 什么是事务传播机制
          • 事务的传播机制有哪些
          • Spring 事务传播机制使⽤和各种场景演⽰
            • REQUIRED(加⼊事务)
            • REQUIRES_NEW(新建事务)
            • NEVER (不⽀持当前事务, 抛异常)
            • NESTED(嵌套事务)
            • NESTED 和 REQUIRED 有什么区别?
      • 总结


Spring事务和事务传播机制

  1. 掌握Spring事务的实现⽅式
  2. 掌握事务的传播机制

事务回顾

数据库阶段, 我们已经学习过事务了

什么是事务

事务是⼀组操作的集合, 是⼀个不可分割的操作.

即同时执行多个操作

事务会把所有的操作作为⼀个整体, ⼀起向数据库提交或者是撤销操作请求. 所以这组操作要么同时成功, 要么同时失败.

为什么需要事务

我们在进⾏程序开发时, 也会有事务的需求.

⽐如转账操作:

第⼀步:A 账⼾ -100 元.

第⼆步:B 账⼾ +100 元.

如果没有事务,第⼀步执⾏成功了, 第⼆步执⾏失败了, 那么A 账⼾的100 元就平⽩⽆故消失了. 如果使⽤事务就可以解决这个问题, 让这⼀组操作要么⼀起成功, 要么⼀起失败.

⽐如秒杀系统,

第⼀步: 下单成功

第⼆步: 扣减库存

下单成功后, 库存也需要同步减少. 如果下单成功, 库存扣减失败, 那么就会造成下单超出的情况. 所以就需要把这两步操作放在同⼀个事务中. 要么⼀起成功, 要么⼀起失败.

理解事务概念为主, 实际企业开发时, 并不是简单的通过事务来处理.

事务的操作

事务的操作主要有三步:

  1. 开启事务: start transaction/ begin (⼀组操作前开启事务)
  2. 提交事务: commit (这组操作全部成功, 提交事务)
  3. 回滚事务: rollback (这组操作中间任何⼀个操作出现异常, 回滚事务)
-- 开启事务
start transaction;-- 提交事务
commit;-- 回滚事务
rollback;

Spring 中事务的实现

前⾯我们讲了MySQL的事务操作, Spring对事务也进⾏了实现.

Spring 中的事务操作分为两类:

  1. 编程式事务(⼿动写代码操作事务).
  2. 声明式事务(利⽤注解⾃动开启和提交事务).[推荐]

在学习事务之前, 我们先准备数据和数据的访问代码

需求: ⽤⼾注册, 注册时在⽇志表中插⼊⼀条操作记录.

数据准备:

-- 创建数据库
DROP DATABASE IF EXISTS trans_test;CREATE DATABASE trans_test DEFAULT CHARACTER SET utf8mb4;-- 用户表
DROP TABLE IF EXISTS user_info;
CREATE TABLE user_info (`id` INT NOT NULL AUTO_INCREMENT,`user_name` VARCHAR (128) NOT NULL,`password` VARCHAR (128) NOT NULL,`create_time` DATETIME DEFAULT now(),`update_time` DATETIME DEFAULT now() ON UPDATE now(),PRIMARY KEY (`id`)
) ENGINE = INNODB DEFAULT CHARACTER
SET = utf8mb4 COMMENT = '用户表';-- 操作日志表
DROP TABLE IF EXISTS log_info;
CREATE TABLE log_info (`id` INT PRIMARY KEY auto_increment,`user_name` VARCHAR ( 128 ) NOT NULL,`op` VARCHAR ( 256 ) NOT NULL,`create_time` DATETIME DEFAULT now(),`update_time` DATETIME DEFAULT now() ON UPDATE now()
) DEFAULT charset 'utf8mb4';

代码准备:

  1. 创建项⽬ trans , 引⼊Spring Web, Mybatis, mysql, lombok等依赖

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.17</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.Hsu</groupId><artifactId>trans</artifactId><version>0.0.1-SNAPSHOT</version><name>trans</name><description>学习事务</description><properties><java.version>8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.3.1</version></dependency><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter-test</artifactId><version>2.3.1</version><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></build></project>
  1. 配置⽂件
# 数据库连接配置
spring:datasource:url: jdbc:mysql://127.0.0.1:3306/trans_test?characterEncoding=utf8&useSSL=falseusername: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 配置打印 MyBatis⽇志map-underscore-to-camel-case: true # 配置驼峰⾃动转换
  1. 实体类
@Data
public class UserInfo {private Integer id;private String userName;private String password;private Date createTime;private Date updateTime;
}
@Data
public class LogInfo {private Integer id;private String userName;private String op;private Date createTime;private Date updateTime;
}
  1. Mapper
@Mapper
public interface UserInfoMapper {@Insert("insert into user_info (user_name,password) values(#{userName},#{password})")Integer insert(String userName, String password);
}
public interface LogInfoMapper {@Insert("insert into log_info (user_name,op) values(#{userName},#{op})")Integer insert(String userName, String op);
}
  1. Service
@Service
public class UserService {@Autowiredprivate UserInfoMapper userInfoMapper;public Integer insertUser(String userName,String password){return userInfoMapper.insert(userName,password);}
}
@Service
public class LogService {@Autowiredprivate LogInfoMapper logInfoMapper;public Integer insertLog(String userName, String op){return logInfoMapper.insert(userName,op);}
}
  1. Contrller
@RestController
@RequestMapping("/user")
public class UserController {@Autowiredprivate UserService userService;@RequestMapping("/registry")public String registry(String userName,String password){Integer result= userService.insertUser(userName,password);return "注册成功";}
}
Spring 编程式事务(了解)

Spring ⼿动操作事务和上⾯ MySQL 操作事务类似, 有 3 个重要操作步骤

  • 开启事务(获取事务)
  • 提交事务
  • 回滚事务

SpringBoot 内置了两个对象:

  1. DataSourceTransactionManager 事务管理器. ⽤来获取事务(开启事务), 提交或回滚事务的

  2. TransactionDefinition 是事务的属性, 在获取事务的时候需要将 TransactionDefinition 传递进去从⽽获得⼀个事务 TransactionStatus

我们还是根据代码的实现来学习:

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {@Autowiredprivate DataSourceTransactionManager dataSourceTransactionManager;@Autowiredprivate TransactionDefinition transactionDefinition;@Autowiredprivate UserService userService;@RequestMapping("/registry")public String registry(String userName,String password){//开启事务TransactionStatus transaction = dataSourceTransactionManager.getTransaction(transactionDefinition);Integer result= userService.insertUser(userName,password);log.info("用户插入成功,result:"+result);//提交事务dataSourceTransactionManager.commit(transaction);//回滚事务//dataSourceTransactionManager.rollback(transaction);return "注册成功";}
}

观察事务提交

//提交事务
dataSourceTransactionManager.commit(transactionStatus);

运⾏程序: http://127.0.0.1:8080/user/registry?userName=zhangsan&password=123456

观察数据库的结果, 数据插⼊成功.

观察事务回滚

//回滚事务
dataSourceTransactionManager.rollback(transactionStatus);

运⾏程序:

观察数据库, 虽然程序返回"注册成功", 但数据库并没有新增数据.

以上代码虽然可以实现事务, 但操作也很繁琐, 有没有更简单的实现⽅法呢?

接下来我们学习声明式事务

Spring 声明式事务 @Transactional

声明式事务的实现很简单, 只需要在需要事务的⽅法上添加 @Transactional 注解就可以实现了.⽆需⼿动开启事务和提交事务, 进⼊⽅法时⾃动开启事务, ⽅法执⾏完会⾃动提交事务, 如果中途发⽣了没有处理的异常会⾃动回滚事务.

@Transactional所在的方法没有抛出异常时,事务就提交;

抛出异常了,事务就回滚

我们来看代码实现:

/*** 正常操作,没有抛出异常*/
@Slf4j
@RequestMapping("trans")
@RestController
public class TransController {@Autowiredprivate UserService userService;@Transactional@RequestMapping("/registry")public String registry(String userName,String password){Integer result= userService.insertUser(userName,password);log.info("用户插入成功,result:"+result);return "注册成功";}
}

运⾏程序, 发现数据插⼊成功.

修改程序, 使之出现异常

/*** 抛出异常* 程序报错,没有捕获*/
@Transactional
@RequestMapping("/registry2")
public String registry2(String userName,String password){Integer result= userService.insertUser(userName,password);log.info("用户插入成功,result:"+result);int a=10/0;return "注册成功";
}

运⾏程序:

发现虽然⽇志显⽰数据插⼊成功, 但数据库却没有新增数据, 事务进⾏了回滚.

我们⼀般会在业务逻辑层当中来控制事务, 因为在业务逻辑层当中, ⼀个业务功能可能会包含多个数据访问的操作. 在业务逻辑层来控制事务, 我们就可以将多个数据访问操作控制在⼀个事务范围内.

上述代码在Controller中书写, 只是为了⽅便学习.

@Transactional 作⽤

@Transactional 可以⽤来修饰⽅法或类:

  • 修饰⽅法时: 只有修饰public ⽅法时才⽣效(修饰其他⽅法时不会报错, 也不⽣效)[推荐]
  • 修饰类时: 对 @Transactional 修饰的类中所有的 public ⽅法都⽣效.

⽅法/类被 @Transactional 注解修饰时, 在⽬标⽅法执⾏开始之前, 会⾃动开启事务, ⽅法执⾏结束之后, ⾃动提交事务.

如果在⽅法执⾏过程中, 出现异常, 且异常未被捕获, 就进⾏事务回滚操作.

如果异常被程序捕获, ⽅法就被认为是成功执⾏, 依然会提交事务.

修改上述代码, 对异常进⾏捕获

/*** 程序报错,但捕获了*/
@Transactional
@RequestMapping("/registry3")
public String registry3(String userName,String password){Integer result= userService.insertUser(userName,password);log.info("用户插入成功,result:"+result);try{int a=10/0;}catch (Exception e){log.error("程序出错");}return "注册成功";
}

运⾏程序, 发现虽然程序出错了, 但是由于异常被捕获了, 所以事务依然得到了提交.

如果需要事务进⾏回滚, 有以下两种⽅式:

  1. 重新抛出异常
/*** 程序报错,但捕获了,又抛出了*/
@Transactional
@RequestMapping("/registry4")
public String registry4(String userName,String password){Integer result= userService.insertUser(userName,password);log.info("用户插入成功,result:"+result);try{int a=10/0;}catch (Exception e){throw e;}return "注册成功";
}
  1. ⼿动回滚事务

使⽤ TransactionAspectSupport.currentTransactionStatus() 得到当前的事务, 并使⽤ setRollbackOnly 设置 setRollbackOnly

/*** 程序报错,手动回滚*/
@Transactional
@RequestMapping("/registry5")
public String registry5(String userName,String password){Integer result= userService.insertUser(userName,password);log.info("用户插入成功,result:"+result);try{int a=10/0;}catch (Exception e){log.error("程序出错");//当前事务回滚TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();}return "注册成功";
}

@Transactional 详解

通过上⾯的代码, 我们学习了 @Transactional 的基本使⽤. 接下来我们学习 @Transactional注解的使⽤细节.

我们主要学习 @Transactional 注解当中的三个常⻅属性:

  1. rollbackFor: 异常回滚属性. 指定能够触发事务回滚的异常类型. 可以指定多个异常类型
  2. Isolation: 事务的隔离级别. 默认值为 Isolation.DEFAULT
  3. propagation: 事务的传播机制. 默认值为 Propagation.REQUIRED
rollbackFor

@Transactional 默认只在遇到运⾏时异常和Error才会回滚, ⾮运⾏时异常不回滚. 即Exception的⼦类中, 除了RuntimeException及其⼦类.

在这里插入图片描述

我们上⾯为了演⽰事务回滚, ⼿动设置了程序异常

int a = 10/0;

接下来我们把异常改为如下代码

/*** 程序报错,但捕获了* 又抛出了一个别的异常*/
@Transactional
@RequestMapping("/registry6")
public String registry6(String userName,String password) throws IOException {Integer result= userService.insertUser(userName,password);log.info("用户插入成功,result:"+result);try{int a=10/0;}catch (Exception e){log.error("程序出错");throw new IOException();}return "注册成功";
}

当前表中数据:

在这里插入图片描述

运⾏程序 http://127.0.0.1:8080/trans/registry6?userName=zhangsan&password=123456

发现虽然程序抛出了异常, 但是事务依然进⾏了提交.

运⾏后表中数据:

在这里插入图片描述

如果我们需要所有异常都回滚, 需要来配置 @Transactional 注解当中的 rollbackFor 属性, 通过 rollbackFor 这个属性指定出现何种异常类型时事务进⾏回滚.

@Transactional(rollbackFor = Exception.class)

/*** 程序报错,但捕获了* 又抛出了一个别的异常*/
@Transactional(rollbackFor = Exception.class)
@RequestMapping("/registry7")
public String registry7(String userName,String password) throws IOException {Integer result= userService.insertUser(userName,password);log.info("用户插入成功,result:"+result);try{int a=10/0;}catch (Exception e){log.error("程序出错");throw new IOException();}return "注册成功";
}

当前表中数据:

在这里插入图片描述

运⾏程序 http://127.0.0.1:8080/trans/registry6?userName=zhangsan&password=123456

发现虽然程序抛出了异常, 但是事务依然进⾏了提交.

运⾏后表中数据:

在这里插入图片描述

结论:

  • 在Spring的事务管理中,默认只在遇到运⾏时异常RuntimeException和Error时才会回滚.
  • 如果需要回滚指定类型的异常, 可以通过rollbackFor属性来指定.
事务隔离级别
MySQL 事务隔离级别(回顾)

SQL 标准定义了四种隔离级别, MySQL 全都⽀持. 这四种隔离级别分别是:

  1. 读未提交(READ UNCOMMITTED): 读未提交, 也叫未提交读. 该隔离级别的事务可以看到其他事务中未提交的数据.

因为其他事务未提交的数据可能会发⽣回滚, 但是该隔离级别却可以读到, 我们把该级别读到的数据称之为脏数据, 这个问题称之为脏读.

  1. 读提交(READ COMMITTED): 读已提交, 也叫提交读. 该隔离级别的事务能读取到已经提交事务的数据,

该隔离级别不会有脏读的问题.但由于在事务的执⾏中可以读取到其他事务提交的结果, 所以在不同时间的相同 SQL 查询可能会得到不同的结果, 这种现象叫做不可重复读

  1. 可重复读(REPEATABLE READ): 事务不会读到其他事务对已有数据的修改, 即使其他事务已提交. 也就可以确保同⼀事务多次查询的结果⼀致, 但是其他事务新插⼊的数据, 是可以感知到的. 这也就引发了幻读问题. 可重复读, 是 MySQL 的默认事务隔离级别.

⽐如此级别的事务正在执⾏时, 另⼀个事务成功的插⼊了某条数据, 但因为它每次查询的结果都是⼀样的, 所以会导致查询不到这条数据, ⾃⼰重复插⼊时⼜失败(因为唯⼀约束的原因). 明明在事务中查询不到这条信息,但⾃⼰就是插⼊不进去, 这个现象叫幻读.

  1. 串⾏化(SERIALIZABLE): 序列化, 事务最⾼隔离级别. 它会强制事务排序, 使之不会发⽣冲突, 从⽽解决了脏读, 不可重复读和幻读问题, 但因为执⾏效率低, 所以真正使⽤的场景并不多.

在这里插入图片描述

数据库中通过以下 SQL 查询全局事务隔离级别和当前连接的事务隔离级别:

select @@global.tx_isolation,@@tx_isolation;

在这里插入图片描述

Spring 事务隔离级别

Spring 中事务隔离级别有5 种:

  1. Isolation.DEFAULT : 以连接的数据库的事务隔离级别为主.
  2. Isolation.READ_UNCOMMITTED : 读未提交, 对应SQL标准中 READ UNCOMMITTED
  3. Isolation.READ_COMMITTED : 读已提交,对应SQL标准中 READ COMMITTED
  4. Isolation.REPEATABLE_READ : 可重复读, 对应SQL标准中 REPEATABLE READ
  5. Isolation.SERIALIZABLE : 串⾏化, 对应SQL标准中 SERIALIZABLE
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;}
}

Spring 中事务隔离级别可以通过 @Transactional 中的 isolation 属性进⾏设置

    @Transactional(isolation = Isolation.DEFAULT)@RequestMapping("/registry8")public String registry8(String userName, String password) throws IOException {Integer result = userService.insertUser(userName, password);log.info("用户插入成功,result:" + result);try {int a = 10 / 0;} catch (Exception e) {log.error("程序出错");}return "注册成功";}
Spring 事务传播机制
什么是事务传播机制

事务传播机制就是: 多个事务⽅法存在调⽤关系时, 事务是如何在这些⽅法间进⾏传播的.

⽐如有两个⽅法A, B都被 @Transactional 修饰, A⽅法调⽤B⽅法

A⽅法运⾏时, 会开启⼀个事务. 当A调⽤B时, B⽅法本⾝也有事务, 此时B⽅法运⾏时, 是加⼊A的事务, 还是创建⼀个新的事务呢?

这个就涉及到了事务的传播机制.

⽐如公司流程管理

执⾏任务之前, 需要先写执⾏⽂档, 任务执⾏结束, 再写总结汇报

在这里插入图片描述

此时A部⻔有⼀项⼯作, 需要B部⻔的⽀援, 此时B部⻔是直接使⽤A部⻔的⽂档, 还是新建⼀个⽂档呢?

事务隔离级别解决的是多个事务同时调⽤⼀个数据库的问题

在这里插入图片描述

事务传播机制解决的是⼀个事务在多个节点(⽅法)中传递的问题

在这里插入图片描述

事务的传播机制有哪些

@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 : 以⾮事务⽅式运⾏, 如果当前存在事务, 则抛出异常.

  7. Propagation.NESTED : 如果当前存在事务, 则创建⼀个事务作为当前事务的嵌套事务来运⾏.如果当前没有事务, 则该取值等价于 PROPAGATION_REQUIRED .

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;}
}

⽐如⼀对新⼈要结婚了, 关于是否需要房⼦

  1. Propagation.REQUIRED : 需要有房⼦. 如果你有房, 我们就⼀起住, 如果你没房, 我们就⼀起买房. (如果当前存在事务, 则加⼊该事务. 如果当前没有事务, 则创建⼀个新的事务)

  2. Propagation.SUPPORTS : 可以有房⼦. 如果你有房, 那就⼀起住. 如果没房, 那就租房. (如果当前存在事务, 则加⼊该事务. 如果当前没有事务, 则以⾮事务的⽅式继续运⾏)

  3. Propagation.MANDATORY : 必须有房⼦. 要求必须有房, 如果没房就不结婚. (如果当前存在事务, 则加⼊该事务. 如果当前没有事务, 则抛出异常)

  4. Propagation.REQUIRES_NEW : 必须买新房. 不管你有没有房, 必须要两个⼈⼀起买房. 即使有房也不住. (创建⼀个新的事务. 如果当前存在事务, 则把当前事务挂起)

  5. Propagation.NOT_SUPPORTED : 不需要房. 不管你有没有房, 我都不住, 必须租房.(以⾮事务⽅式运⾏, 如果当前存在事务, 则把当前事务挂起)

  6. Propagation.NEVER : 不能有房⼦. (以⾮事务⽅式运⾏, 如果当前存在事务, 则抛出异常)

  7. Propagation.NESTED : 如果你没房, 就⼀起买房. 如果你有房, 我们就以房⼦为根据地,做点下⽣意. (如果如果当前存在事务, 则创建⼀个事务作为当前事务的嵌套事务来运⾏. 如果当前没有事务, 则该取值等价于 PROPAGATION_REQUIRED )

Spring 事务传播机制使⽤和各种场景演⽰

对于以上事务传播机制,我们重点关注以下两个就可以了:

  1. REQUIRED(默认值)
  2. REQUIRES_NEW
REQUIRED(加⼊事务)

看下⾯代码实现:

  1. ⽤⼾注册, 插⼊⼀条数据
  2. 记录操作⽇志, 插⼊⼀条数据(出现异常)

观察 propagation = Propagation.REQUIRED 的执⾏结果

其实可加可不加,默认值而已

@Slf4j
@RestController
@RequestMapping("/proga")
public class ProController {@Autowiredprivate UserService userService;@Autowiredprivate LogService logService;/*** 正常操作,没有抛出异常*/@Transactional(propagation = Propagation.REQUIRED)@RequestMapping("/p1")public String registry(String userName, String password) {Integer result = userService.insertUser(userName, password);log.info("用户插入成功,result:" + result);Integer result2 = logService.insertLog(userName, "用户自行注册");log.info("日志表插入成功,result2:" + result2);return "注册成功";}
}

对应的UserService和LogService都添加上 @Transactional(propagation = Propagation.REQUIRED)

其实可加可不加,默认值而已

    @Transactionall(propagation = Propagation.REQUIRED)public Integer insertUser(String userName,String password){return userInfoMapper.insert(userName,password);}
    @Transactionall(propagation = Propagation.REQUIRED)public Integer insertLog(String userName, String op){return logInfoMapper.insert(userName,op);}

运⾏程序,http://127.0.0.1:8080/proga/p1?userName=zhangsan&password=123456,发现数据库没有插⼊任何数据.

流程描述:

  1. p1 ⽅法开始事务
  2. ⽤⼾注册, 插⼊⼀条数据 (执⾏成功) (和p1 使⽤同⼀个事务)
  3. 记录操作⽇志, 插⼊⼀条数据(出现异常, 执⾏失败) (和p1 使⽤同⼀个事务)
  4. 因为步骤3出现异常, 事务回滚. 步骤2和3使⽤同⼀个事务, 所以步骤2的数据也回滚了.
REQUIRES_NEW(新建事务)

将上述 UserService 和 LogService 中相关⽅法事务传播机制改为 Propagation.REQUIRES_NEW

@Service
public class UserService {@Autowiredprivate UserInfoMapper userInfoMapper;@Transactional(propagation = Propagation.REQUIRES_NEW)public Integer insertUser(String userName,String password){return userInfoMapper.insert(userName,password);}
}
@Service
public class LogService {@Autowiredprivate LogInfoMapper logInfoMapper;@Transactional(propagation = Propagation.REQUIRES_NEW)public Integer insertLog(String userName, String op){int a=10/0;return logInfoMapper.insert(userName,op);}
}

运⾏程序, 发现⽤⼾数据插⼊成功了, ⽇志表数据插⼊失败.

LogService ⽅法中的事务不影响 UserService 中的事务.

当我们不希望事务之间相互影响时, 可以使⽤该传播⾏为.

NEVER (不⽀持当前事务, 抛异常)

修改UserService 中对应⽅法的事务传播机制为 Propagation.NEVER

@Service
public class UserService {@Autowiredprivate UserInfoMapper userInfoMapper;@Transactional(propagation = Propagation.NEVER)public Integer insertUser(String userName,String password){return userInfoMapper.insert(userName,password);}
}

程序执⾏报错, 没有数据插⼊.

这里 UserService 不允许有事务,但我们在 ProController 中的@Transactional发现了事务,因此程序执行报错

NESTED(嵌套事务)

将上述 UserService 和 LogService 中相关⽅法事务传播机制改为 Propagation.NESTED

@Service
public class UserService {@Autowiredprivate UserInfoMapper userInfoMapper;@Transactional(propagation = Propagation.NESTED)public Integer insertUser(String userName,String password){return userInfoMapper.insert(userName,password);}
}
@Service
public class LogService {@Autowiredprivate LogInfoMapper logInfoMapper;@Transactional(propagation = Propagation.NESTED)public Integer insertLog(String userName, String op){int a=10/0;return logInfoMapper.insert(userName,op);}
}

运⾏程序, 发现没有任何数据插⼊.

流程描述:

  1. ProController 中 p1 ⽅法开始事务

  2. UserService ⽤⼾注册, 插⼊⼀条数据 (嵌套p1事务)

  3. LogService 记录操作⽇志, 插⼊⼀条数据(出现异常, 执⾏失败) (嵌套p1事务, 回滚当前事务, 数据添加失败)

  4. 由于是嵌套事务, LogService 出现异常之后, 往上找调⽤它的⽅法和事务, 所以⽤⼾注册也失败了.

  5. 最终结果是两个数据都没有添加

事务全部成功时就都成功了;

事务有一个报错那就都失败了

p1事务可以认为是⽗事务, 嵌套事务是⼦事务. ⽗事务出现异常, ⼦事务也会回滚, ⼦事务出现异常, 如果不进⾏处理, 也会导致⽗事务回滚.

NESTED 和 REQUIRED 有什么区别?

我们在 LogService 进⾏当前事务回滚, 修改 LogService 代码如下:

@Service
public class LogService {@Autowiredprivate LogInfoMapper logInfoMapper;@Transactional(propagation = Propagation.NESTED)public Integer insertLog(String userName, String op){try {int a = 10 / 0;}catch (Exception e){TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();}return logInfoMapper.insert(userName,op);}
}

重新运⾏程序, 发现⽤⼾表数据添加成功, ⽇志表添加失败.

LogService 中的事务已经回滚, 但是嵌套事务不会回滚嵌套之前的事务, 也就是说嵌套事务可以实现部分事务回滚.

对⽐REQUIRED

把 NESTED 传播机制改为 REQUIRED, 修改代码如下:

@Service
public class UserService {@Autowiredprivate UserInfoMapper userInfoMapper;@Transactional(propagation = Propagation.REQUIRED)public Integer insertUser(String userName,String password){return userInfoMapper.insert(userName,password);}
}
@Service
public class LogService {@Autowiredprivate LogInfoMapper logInfoMapper;@Transactional(propagation = Propagation.REQUIRED)public Integer insertLog(String userName, String op){try {int a = 10 / 0;}catch (Exception e){TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();}return logInfoMapper.insert(userName,op);}
}

重新运⾏程序, 发现⽤⼾表和⽇志表的数据添加都失败了.

REQUIRED 如果回滚就是回滚所有事务, 不能实现部分事务的回滚. (因为属于同⼀个事务)

NESTED和REQUIRED区别

  • 整个事务如果全部执⾏成功, ⼆者的结果是⼀样的.
  • 如果事务⼀部分执⾏成功, REQUIRED加⼊事务会导致整个事务全部回滚. NESTED嵌套事务可以实现局部回滚, 不会影响上⼀个⽅法中执⾏的结果.

嵌套事务之所以能够实现部分事务的回滚, 是因为事务中有⼀个保存点(savepoint)的概念, 嵌套事务

进⼊之后相当于新建了⼀个保存点, ⽽滚回时只回滚到当前保存点.

资料参考: https://dev.mysql.com/doc/refman/5.7/en/savepoint.html

在这里插入图片描述

REQUIRED 是加⼊到当前事务中, 并没有创建事务的保存点, 因此出现了回滚就是整个事务回滚, 这就是嵌套事务和加⼊事务的区别.

总结

  1. Spring中使⽤事务, 有两种⽅式: 编程式事务(⼿动操作)和声明式事务. 其中声明式事务使⽤较多,在⽅法上添加 @Transactional 就可以实现了

  2. 通过 @Transactional(isolation = Isolation.SERIALIZABLE) 设置事务的隔离级别. Spring 中的事务隔离级别有 5 种

  3. 通过 @Transactional(propagation = Propagation.REQUIRED) 设置事务的传播机制, Spring 中的事务传播级别有 7 种, 重点关注 REQUIRED (默认值) 和 REQUIRES_NEW


http://www.ppmy.cn/server/110701.html

相关文章

第3章-03-Python库Requests安装与讲解

Python库Requests的安装与讲解 &#x1f3c6;作者简介&#xff0c;黑夜开发者&#xff0c;CSDN领军人物&#xff0c;全栈领域优质创作者✌&#xff0c;CSDN博客专家&#xff0c;阿里云社区专家博主&#xff0c;2023年CSDN全站百大博主。 &#x1f3c6;数年电商行业从业经验&…

WordNet介绍——一个英语词汇数据库

传统语义知识库最常见的更新方法是依赖人工手动更新&#xff0c;使用这种更新方法的语义知识库包括最早的 WordNet、FrameNet和 ILD&#xff0c;以及包含丰富内容的 ConceptNet和 DBPedia。此类语义知识库的特点是以单词作为语义知识库的基本构成元素&#xff0c;以及使用预先设…

WPF ToolkitMVVM RelayCommand

ObservableProperty 特性 字段是一般小写 [ObservableProperty] private string?userName;//View 中 绑定 是 大写 UserNameRelayCommand 特性 要大写 [RelayCommand]private void GetName(){} //View 中绑定 GetNameCommand 方法Command》》》CanExecute …

OceanMind海睿思参加2024数博会“数据要素赋能生态”活动,获两项数据要素优秀产品认证

近日&#xff0c;2024数博会“数据要素赋能生态”交流活动在贵阳国际生态会议中心成功举办&#xff0c;中新赛克海睿思作为国内数据要素产业优秀服务商代表受邀参加并荣获两项数据要素优秀产品认证。 作为2024数博会的重要组成部分&#xff0c;本次交流活动由北京赛迪出版传媒有…

华为云低代码AstroZero技巧教学3:智能计算商品费用,轻松实现高效数据处理

公司经营过程中&#xff0c;多个场景会涉及到计算商品花费。以企业内部行政采购为例&#xff0c;在统计相关采购清单中&#xff0c;会涉及到诸多数据统计及计算。如采购商品种类、数量、单价以及其他附加成本&#xff08;运输费用&#xff0c;装卸费用&#xff0c;包装费用&…

国标GB28181视频监控EasyCVR视频汇聚平台国标注册被陌生IP入侵如何处理?

GB28181国标/GA/T1400协议/安防综合管理系统EasyCVR视频汇聚平台能在复杂的网络环境中&#xff0c;将前端设备统一集中接入与汇聚管理。智慧安防/视频存储/视频监控/视频汇聚EasyCVR平台可以提供实时远程视频监控、视频录像、录像回放与存储、告警、语音对讲、云台控制、平台级…

ARMxy工业控制器搭载 Mini PCIe加密安全芯片工业控制拓展之旅

在当今高度数字化和智能化的工业领域&#xff0c;数据采集的准确性、实时性和全面性对于优化生产流程、提高产品质量以及保障生产安全至关重要。ARM 工业控制器以其高效能、低功耗和出色的稳定性&#xff0c;成为了工业自动化领域的重要组成部分。而其中的 Mini PCIe 接口更是为…

MTK Camera Debug,adb 读写寄存器操作

在Camera BringUp阶段&#xff0c;遇到问题的时候&#xff0c;我们有时需要读取或者写入某个寄存器的值来进行debug。在mtk 平台&#xff0c;通过adb命令就能实现。 一、读取/写入 某个寄存器地址的值 下面的例子&#xff0c;针对主摄camera sensor进行操作。操作节点是/proc/d…