Spring事务和事务传播机制

本节目标

  1. 掌握Spring事务的实现方式

  2. 掌握事务的传播机制

1. 事务回顾

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

1.1 什么是事务?

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

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

1.2 为什么需要事务?

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

比如转账操作:

第一步:A 账户 -100 元. 第二步:B 账户 +100 元

如果没有事务,第一步执行成功了, 第二步执行失败了, 那么A 账户的100 元就平白无故消失了. 如果使 用事务就可以解决这个问题, 让这一组操作要么一起成功, 要么一起失败

1.3 事务的操作

事务的操作主要有三步:

  1. 开启事start transaction/ begin (一组操作前开启事务)

  2. 提交事务: commit (这组操作全部成功, 提交事务)

  3. 回滚事务: rollback (这组操作中间任何一个操作出现异常, 回滚事务)

2. 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. 创建项目 spring-trans, 引入Spring Web, Mybatis, mysql等依赖

  2. 配置文件

    spring:
    datasource:
    url: jdbc:mysql://127.0.0.1:3306/trans_test?
    characterEncoding=utf8&useSSL=false
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
    mybatis:
    configuration: # 配置打印 MyBatis日志
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    map-underscore-to-camel-case: true #配

2.1 Spring 编程式事务(了解)

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

java 复制代码
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/user")
@RestController
public class UserController {
 // JDBC 事务管理器
 @Autowired
 private DataSourceTransactionManager dataSourceTransactionManager;
 // 定义事务属性
 @Autowired
 private TransactionDefinition transactionDefinition;
 @Autowired
 private UserService userService;
 @RequestMapping("/registry")
 public String registry(String name,String password){
 // 开启事务
 TransactionStatus transactionStatus = dataSourceTransactionManager
 .getTransaction(transactionDefinition);
 //用户注册
 userService.registryUser(name,password);
 //提交事务
 dataSourceTransactionManager.commit(transactionStatus);
 //回滚事务
 //dataSourceTransactionManager.rollback(transactionStatus);
 return "注册成功";
 }
}

2.2 Spring 声明式事务 @Transactional

1. 添加依赖

java 复制代码
<dependency>
 <groupId>org.springframework</groupId>
 <artifactId>spring-tx</artifactId>
</dependency>

2. 在需要事务的方法上添加 @Transactional 注解就可以实现了. 无需手动开启事务和提交事 务, 进入方法时自动开启事务, 方法执行完会自动提交事务, 如果中途发生了没有处理的异常会自动 回滚事务

java 复制代码
@RequestMapping("/trans")
@RestController
public class TransactionalController {
 @Autowired
 private UserService userService;
 @Transactional
 @RequestMapping("/registry")
public String registry(String name,String password){
 //用户注册
 userService.registryUser(name,password);
 return "注册成功";
 }
}

运行程序, 发现数据插入成功.

修改程序, 使之出现异常

java 复制代码
@Slf4j
@RequestMapping("/trans")
@RestController
public class TransactionalController {
 @Autowired
 private UserService userService;
 @Transactional
 @RequestMapping("/registry")
 public String registry(String name,String password){
 //用户注册
 userService.registryUser(name,password);
 log.info("用户数据插入成功");
 //强制程序抛出异常
 int a = 10/0;
 return "注册成功";
 }
}

发现虽然日志显示数据插入成功, 但数据库却没有新增数据, 事务进行了回滚.

@Transactional 作用

@Transactional 可以用来修饰方法或类

• 修饰方法时: 只有修饰public 方法时才生效(修饰其他方法时不会报错, 也不生效)[推荐]

• 修饰类时: 对 @Transactional 修饰的类中所有的 public 方法都生效

方法/类被 @Transactional 注解修饰时, 在目标方法执行开始之前, 会自动开启事务, 方法执行结束 之后, 自动提交事务.

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

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

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

java 复制代码
@Transactional
@RequestMapping("/registry")
public String registry(String name,String password){
 //用户注册
 userService.registryUser(name,password);
 log.info("用户数据插入成功");
 //对异常进行捕获
 try {
 //强制程序抛出异常
 int a = 10/0;
 }catch (Exception e){
 e.printStackTrace();
//throw e;
// 手动回滚事务 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
 }
 return "注册成功";
}

3. @Transactional 详解

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

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

  1. rollbackFor: 异常回滚属性. 指定能够触发事务回滚的异常类型. 可以指定多个异常类型

  2. Isolation: 事务的隔离级别. 默认值为 Isolation.DEFAULT

  3. propagation: 事务的传播机制. 默认值为 Propagation.REQUIRED

3.1 rollbackFor

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

我们上面为了演示事务回滚, 手动设置了程序异常

java 复制代码
int a = 10/0;

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

java 复制代码
@Transactional
@RequestMapping("/r2")
public String r2(String name,String password) throws IOException {
 //用户注册
 userService.registryUser(name,password);
 log.info("用户数据插入成功");
 if (true){
 throw new IOException();
 }
 return "r2";
}

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

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

java 复制代码
@Transactional(rollbackFor = Exception.class)
@RequestMapping("/r2")
public String r2(String name,String password) throws IOException {
 //用户注册
 userService.registryUser(name,password);
 log.info("用户数据插入成功");
 if (true){
 throw new IOException();
 }
 return "r2";
}

• 在Spring的事务管理中,默认只在遇到运行时异常RuntimeException和Error时才会回滚.

• 如果需要回滚指定类型的异常, 可以通过rollbackFor属性来指定

3.2 事务隔离级别

3.2.1 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

java 复制代码
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 属性进行设置

java 复制代码
@Transactional(isolation = Isolation.READ_COMMITTED)
@RequestMapping("/r3")
public String r3(String name,String password) throws IOException {
 //... 代码省略
 return "r3";
}

3.3 Spring 事务传播机制

3.3.1 什么是事务传播机制

事务传播机制就是: 多个事务方法存在调用关系时, 事务是如何在这些方法间进行传播的

  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

java 复制代码
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;
 }
}

3.3.2 Spring 事务传播机制使用和各种场景演示

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

  1. REQUIRED(默认值)

  2. REQUIRES_NEW

3.3.2.1 REQUIRED(加入事务)

看下面代码实现:

  1. 用户注册, 插入一条数据

  2. 记录操作日志, 插入一条数据(出现异常)

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

java 复制代码
@RequestMapping("/propaga")
@RestController
public class PropagationController {
 @Autowired
 private UserService userService;
 @Autowired
 private LogService logService;
 @Transactional(propagation = Propagation.REQUIRED)
 @RequestMapping("/p1")
 public String r3(String name,String password){
 //用户注册
 userService.registryUser(name,password);
 //记录操作日志
 logService.insertLog(name,"用户注册");
 return "r3";
 }
}

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

java 复制代码
@Slf4j
@Service
public class UserService {
 @Autowired
 private UserInfoMapper userInfoMapper;
 @Transactional(propagation = Propagation.REQUIRED)
 public void registryUser(String name,String password){
 //插入用户信息
 userInfoMapper.insert(name,password);
 }
}
java 复制代码
@Slf4j
@Service
public class LogService {
 @Autowired
 private LogInfoMapper logInfoMapper;
 @Transactional(propagation = Propagation.REQUIRED)
 public void insertLog(String name,String op){
 int a=10/0;
 //记录用户操作
 logInfoMapper.insertLog(name,"用户注册");
 }
}

运行程序, 发现数据库没有插入任何数据

3.3.2.2 REQUIRES_NEW(新建事务)

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

java 复制代码
@Service
public class UserService {
 @Autowired
 private UserInfoMapper userInfoMapper;
 @Transactional(propagation = Propagation.REQUIRES_NEW)
 public void registryUser(String name,String password){
//插入用户信息
 userInfoMapper.insert(name,password);
 }
}
java 复制代码
@Service
public class LogService {
 @Autowired
 private LogInfoMapper logInfoMapper;
 @Transactional(propagation = Propagation.REQUIRES_NEW)
 public void insertLog(String name,String op){
 int a=10/0;
 //记录用户操作
 logInfoMapper.insertLog(name,"用户注册");
 }
}

运行程序, 发现用户数据插入成功了, 日志表数据插入失败

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

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

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

java 复制代码
@Slf4j
@Service
public class UserService {
 @Autowired
 private UserInfoMapper userInfoMapper;
 @Transactional(propagation = Propagation.NEVER)
 public void registryUser(String name,String password){
 //插入用户信息
 userInfoMapper.insert(name,password);
 }
}

程序执行报错, 没有数据插入

3.3.2.4 NESTED(嵌套事务)

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

java 复制代码
@Slf4j
@Service
public class UserService {
 @Autowired
 private UserInfoMapper userInfoMapper;
 @Transactional(propagation = Propagation.NESTED)
 public void registryUser(String name,String password){
 //插入用户信息
 userInfoMapper.insert(name,password);
 }
}
java 复制代码
@Slf4j
@Service
public class LogService {
 @Autowired
 private LogInfoMapper logInfoMapper;
 @Transactional(propagation = Propagation.NESTED)
 public void insertLog(String name,String op){
 int a=10/0;
 //记录用户操作
 logInfoMapper.insertLog(name,"用户注册");
 }
}

运行程序, 发现没有任何数据插入

  1. Controller 中p1 方法开始事务

  2. UserService用户注册, 插入一条数据 (嵌套p1事务)

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

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

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

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

3.3.2.5 NESTED和REQUIRED 有什么区别

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

java 复制代码
@Service
public class LogService {
 @Autowired
 private LogInfoMapper logInfoMapper;
 @Transactional(propagation = Propagation.NESTED)
 public void insertLog(String name,String op){
 try {
 int a=10/0;
 } catch (Exception e){
 //回滚当前事务

TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
 }
 //记录用户操作
 logInfoMapper.insertLog(name,"用户注册");
 }
}

重新运行程序, 发现用户表数据添加成功, 日志表添加失败.

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

对比REQUIRED

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

java 复制代码
@Service
public class UserService {
@Autowired
 private UserInfoMapper userInfoMapper;
 @Transactional(propagation = Propagation.REQUIRED)
 public void registryUser(String name,String password){
 //插入用户信息
 userInfoMapper.insert(name,password);
 }
}
java 复制代码
@Service
public class LogService {
 @Autowired
 private LogInfoMapper logInfoMapper;
 @Transactional(propagation = Propagation.REQUIRED)
 public void insertLog(String name,String op){
 try {
 int a=10/0;
 } catch (Exception e){
 //回滚当前事务

TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
 }
 //记录用户操作
 logInfoMapper.insertLog(name,"用户注册");
 }
}

重新运行程序, 发现用户表和日志表的数据添加都失败了.

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

NESTED和REQUIRED区别

• 整个事务如果全部执行成功, 二者的结果是一样的.

• 如果事务一部分执行成功,REQUIRED加入事务会导致整个事务全部回滚. NESTED嵌套事务可以实 现局部回滚, 不会影响上一个方法中执行的结果

总结

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

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

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

相关推荐
forEverPlume1 小时前
Go语言怎么做链路追踪_Go语言分布式链路追踪教程【精选】
jvm·数据库·python
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题】【Java基础篇】第34题:String、StringBuffer和StringBuilder的区别是什么
java·后端·面试
晓庆的故事簿1 小时前
JAVA搭配RabbitMQ使用
java·rabbitmq·java-rabbitmq
折哥的程序人生 · 物流技术专研1 小时前
第3篇:为何要配置环境变量?
java·开发语言·后端·面试
abc123456sdggfd1 小时前
golang如何操作Elasticsearch搜索引擎_golang操作Elasticsearch方法
jvm·数据库·python
lifewange1 小时前
UPDATE ... SET 多字段赋值
数据库
渔民小镇2 小时前
4 行代码接入 Spring —— ionet 的生态融合之道
java·服务器·分布式·游戏
百度安全2 小时前
HugeGraph 晋升 Apache 顶级项目 百度安全持续筑牢 AI 时代图数据基础设施
数据库·人工智能·安全·知识图谱
_376271532 小时前
JavaScript中闭包结合代理模式Proxy实现数据监听
jvm·数据库·python