当前位置: 首页 > web >正文

Spring 事务和事务传播机制

文章目录

  • 事务回顾
  • Spring 中事务的实现
    • Spring 编程式事务 (了解)
    • Spring 声明式事务 (@Transactional)
  • @Transactional注解在什么情况下事务不生效?
  • @Transactional 属性
    • rollbackFor
    • 事务隔离级别
    • Spring 事务传播机制

事务回顾

事务将一组相关操作封装为一个不可分割的整体。这组操作在执行过程中,要么全部成功提交并持久化到数据库,要么在任一环节失败时整体撤销(回滚),以此确保数据一致性和完整性

为什么需要事务?

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

比如转账操作:
第一步:A 账户 - 100 元
第二步:B 账户 +100 元

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

MySQL事务的操作

在 MySQL 中,事务的操作可概括为三个关键环节:

  1. 开启事务:通过 START TRANSACTION 显式启动事务,后续操作纳入同一事务上下文
  2. 执行业务操作:在事务内执行增删改查等操作,所有变更会先写入内存,此时数据变更对其他事务的可见性由 ​​隔离级别​​ 决定
  3. 确认结果并收尾:
    • 若所有操作均成功完成,执行 COMMIT 提交事务,将内存中所有变更一次性持久化到数据库,事务结束
    • 若过程中出现异常或不符合预期,执行 ROLLBACK 回滚事务,撤销所有已执行的操作,数据库状态恢复到事务开启前
-- 1. 开启事务(后续操作纳入事务管理)
START TRANSACTION;  -- 2. 执行一组操作(DML语句:INSERT/UPDATE/DELETE)
UPDATE account SET balance = balance - 100 WHERE id = 1;  -- A账户扣100元
UPDATE account SET balance = balance + 100 WHERE id = 2;  -- B账户加100元-- 3. 判断是否提交或回滚
-- 若所有操作成功,提交事务(修改永久生效)
COMMIT;-- 若中间有任何操作失败(如SQL错误、业务逻辑异常),回滚事务(所有修改撤销)
-- ROLLBACK;

Spring 中事务的实现

Spring 对事务也进行了实现,事务操作分为两类:

  1. 编程式事务 (手动写代码操作事务)
  2. 声明式事务 (利用注解自动开启和提交事务)

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

数据准备:

-- 创建数据库
DROP DATABASE IF EXISTS trans_test;
CREATE DATABASE trans_test DEFAULT CHARACTER SET utf8mb4;use trans_test;-- 用户表
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 = '用户表';

代码准备:

  1. 创建 Spring Boot 项目并引入 Spring Web、Lombok、MyBatis、MySQL 依赖
  2. 配置文件
# 数据库连接配置
spring:datasource:url: jdbc:mysql://127.0.0.1:3306/trans_test?characterEncoding=utf8&useSSL=falseusername: root # 连接数据库的用户名password: '123456' # 连接数据库的密码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
  1. 实体类
import lombok.Data;
import java.util.Date;@Data
public class UserInfo {private Integer id;private String userName;private String password;private Date createTime;private Date updateTime;
}
  1. Mapper
@Mapper
public interface UserInfoMapper {@Insert("insert into user_info(`user_name`,`password`) values(#{name},#{password})")Integer insert(String name, String password);
}
  1. Service
@Service
public class UserService {@Autowiredprivate UserInfoMapper userInfoMapper;public void registryUser(String name, String password) {// 插入用户信息userInfoMapper.insert(name, password);}
}
  1. Controller
@RestController
public class UserController {@Autowiredprivate UserService userService;@RequestMapping("/registry")public String registry(String name, String password) {//用户注册userService.registryUser(name, password);return "注册成功";}
}

Spring 编程式事务 (了解)

Spring 的编程式事务通过手动编码方式实现事务控制,其核心操作逻辑与 MySQL 原生事务类似,主要包含三个关键步骤:

  1. 开启事务
  2. 执行业务操作
  3. 事务收尾(提交或回滚):
    • 若所有业务操作均正常完成,提交事务,使所有变更永久生效
    • 若执行过程中出现异常(如业务校验失败、数据库错误等),回滚事务,撤销所有已执行的操作,恢复到事务开启前的状态

代码示例如下:

@RestController
public class UserController {// SpringBoot 内置了两个对象:// JDBC 事务管理器(用于手动管理事务(开启事务、提交、回滚))@Autowiredprivate DataSourceTransactionManager dataSourceTransactionManager;// 定义事务属性@Autowiredprivate TransactionDefinition transactionDefinition;@Autowiredprivate UserService userService;@RequestMapping("/registry")public String registry(String name, String password) {// 开启事务TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);try {// 用户注册userService.registryUser(name, password);// 提交事务(没有异常时提交)dataSourceTransactionManager.commit(transactionStatus);return "注册成功";} catch (Exception e) {// 回滚事务(发生异常时回滚)dataSourceTransactionManager.rollback(transactionStatus);return "注册失败: " + e.getMessage();}}
}

运行程序,访问 http://127.0.0.1:8080/registry?name=admin&password=admin

在这里插入图片描述

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

模拟异常场景:

        try {// 用户注册userService.registryUser(name, password);int a = 10/0;// 提交事务(没有异常时提交)dataSourceTransactionManager.commit(transactionStatus);return "注册成功";} catch (Exception e) {// 回滚事务(发生异常时回滚)dataSourceTransactionManager.rollback(transactionStatus);return "注册失败: " + e.getMessage();}

在这里插入图片描述

数据库并没有新增数据

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

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

Spring 声明式事务 (@Transactional)

声明式事务的实现很简单,只要在需要事务的方法上添加 @Transactional 注解就可以了。无需手动开启事务和提交/回滚事务,进入方法时自动开启事务,方法正常执行完会自动提交事务,如果发生 ERROR 或者 有未捕获的 RuntimeException 及其子类(默认行为)会自动回滚事务

我们来看代码实现:

@RestController
public class UserController {@Autowiredprivate UserService userService;@Transactional@RequestMapping("/registry")public String registry(String name, String password) {//用户注册userService.registryUser(name, password);return "注册成功";}
}

运行程序,访问接口发现数据插入成功

修改程序, 使之出现异常

@RestController
public class UserController {@Autowiredprivate UserService userService;@Transactional@RequestMapping("/registry")public String registry(String name, String password) {//用户注册userService.registryUser(name, password);//强制程序抛出异常int a = 10/0;return "注册成功";}
}

运行程序,访问接口发现事务进行了回滚

我们一般在业务逻辑层(Service层)控制事务,因为在业务逻辑层,一个业务功能可能会包含多个数据操作。在业务逻辑层来控制事务,我们就可以将多个数据操作控制在一个事务范围内。上述代码在 Controller 中书写,只是为了方便学习

@Transactional注解在什么情况下事务不生效?

  1. 标注了@Transactional的方法里面的异常被捕获了
  2. 标注了@Transactional的方法发生了非 Error 或者 RuntimeException
  3. 若是错误的配置以下三种 Propagation,事务将不会发生回滚:
    • Propagation.SUPPORTS
    • Propagation.NOT_SUPPORTED
    • Propagation.NEVER
  4. 应用在非 public、static、final 方法上时
  5. 数据库引擎不支持事务
  6. 调用方法A,A内部调用方法B,A没有@Transaction注解而B有@Transactional注解
  7. 标注了@Transactional的方法发生的异常不是rollbackFor指定的类型或子类
  8. 若注解同时指定了 rollbackFor 和 noRollbackFor,且两个属性包含相同或有继承关系的异常类型,noRollbackFor 会覆盖 rollbackFor,导致对应异常不回滚
  9. 若 @Transactional 所在的类未被 Spring 容器扫描并实例化为 Bean,则 AOP 无法生成代理对象,事务自然不生效

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

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

@RestController
public class UserController {@Autowiredprivate UserService userService;@Transactional@RequestMapping("/registry")public String registry(String name, String password) {//用户注册userService.registryUser(name, password);//对异常进行捕获try {int a = 10/0;} catch (ArithmeticException e) {e.printStackTrace();}return "注册成功";}
}

运行程序,访问接口发现事务依然得到了提交

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

  1. 重新抛出异常
@RestController
public class UserController {@Autowiredprivate UserService userService;@Transactional@RequestMapping("/registry")public String registry(String name, String password) {//用户注册userService.registryUser(name, password);try {int a = 10/0;} catch (ArithmeticException e) {//将异常重新抛出去throw e;}return "注册成功";}
}
  1. 手动回滚事务

使用 TransactionAspectSupport.currentTransactionStatus() 获取当前事务的状态对象,并调用其 setRollbackOnly() 方法手动标记事务需要回滚

@RestController
public class UserController {@Autowiredprivate UserService userService;@Transactional@RequestMapping("/registry")public String registry(String name, String password) {//用户注册userService.registryUser(name, password);try {int a = 10/0;} catch (ArithmeticException e) {// 手动回滚事务TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();}return "注册成功";}
}

@Transactional 属性

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

  • 修饰方法时:只有修饰 public 方法时才生效 (修饰其他方法时不会报错,也不生效)
  • 修饰类时:对 @Transactional 修饰的类中所有的符合条件的方法都生效

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

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

rollbackFor

@Transactional 默认只在遇到运行时异常和 Error 时才会回滚,非运行时异常不回滚

接下来我们改为如下代码

@RestController
public class UserController {@Autowiredprivate UserService userService;@Transactional@RequestMapping("/registry")public String registry(String name, String password) throws IOException {//用户注册userService.registryUser(name, password);throw new IOException("模拟IO异常");}
}

运行程序,访问接口

发现虽然程序抛出了异常,但是事务依然进行了提交,表中还是加了数据

如果我们需要所有异常都回滚,需要通过 rollbackFor指定出现何种异常类型时事务进行回滚

@RestController
public class UserController {@Autowiredprivate UserService userService;@Transactional(rollbackFor = Exception.class)@RequestMapping("/registry")public String registry(String name, String password) throws IOException {//用户注册userService.registryUser(name, password);throw new IOException("模拟IO异常");}
}

事务隔离级别

MySQL 事务隔离级别 (回顾)

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

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

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

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

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

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

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

  1. 串行化 (SERIALIZABLE): 序列化,事务最高隔离级别。它会强制事务排序,使之不会发生冲突,从而解决了脏读,不可重复读和幻读问题,但因为执行效率低,所以真正使用的场景并不多
事务隔离级别脏读不可重复读幻读
读未提交(READ UNCOMMITTED)
读已提交(READ COMMITTED)×
可重复读(REPEATABLE READ)××
串行化(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

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

Spring 事务传播机制

事务隔离级别解决的是多个事务同时调用一个数据库的问题,而事务传播机制解决的是一个事务在多个节点 (方法) 中传递的问题

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

比如有两个方法 A,B 都被 @Transactional 修饰,A 方法调用 B 方法

A 方法运行时,会开启一个事务。当 A 调用 B 时,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
http://www.xdnf.cn/news/15961.html

相关文章:

  • CSS 单位完全指南:掌握 em、rem、vh、vw 等响应式布局核心单位
  • 仙盟数据库应用-外贸标签打印系统 前端数据库-V8--毕业论文-—-—仙盟创梦IDE
  • 单链表专题
  • docker compose 编排容器 mysql Springboot应用
  • 使用pnpm安装项目的生产依赖dependencies和开发依赖devDependies及pnpm工作空间等简单使用方法说明
  • 全面解析MySQL(2)——CRUD基础
  • SQL 调优第一步:EXPLAIN 关键字全解析
  • HTTP1-HTTP2-HTTP3简要概述
  • day 12 看门狗外设
  • 运行时常量池 和 字符串常量池 区别
  • 【数据集】NOAA 全球监测实验室(GML)海洋边界层(MBL)参考简介
  • 虚拟机VMware安装国产桌面系统统信UOS
  • 传输层协议 TCP
  • 【Python数据采集】Python爬取小红书搜索关键词下面的所有笔记的内容、点赞数量、评论数量等数据,绘制词云图、词频分析、数据分析
  • docker-compose启动前后端分离项目(单机)
  • ARFoundation系列讲解 - 101 VisionPro 真机调试
  • MySQL EXPLAIN 解读
  • DAY 20 奇异值分解(SVD)
  • ant+Jmeter+jenkins接口自动化,如何实现把执行失败的接口信息单独发邮件?
  • leetcode丑数II计算第n个丑数
  • zabbix服务器告警处理
  • 【milvus检索】milvus检索召回率
  • pages.json页面路由中,globalStyle的各个属性
  • 社交圈子系统开源社交源码 / 小程序+H5+APP 多端互通的底层技术分析
  • Ubuntu 24.04 设置静态 IP 的方法
  • 对LLM某一层进行优化:通过眼动数据发现中间层注重语句内在含义,进而对中间层参数优化
  • pthread_detach与pthread_join区别及使用场景
  • 408考研逐题详解:2010年第35题——RIP协议
  • BST(二叉搜索树)的笔试大题(C语言)
  • AG32:解锁MCU+FPGA应用新姿势,功能与实战全解析