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

[Spring] Spring事务与事务的传播

🌸个人主页:https://blog.csdn.net/2301_80050796?spm=1000.2115.3001.5343
🏵️热门专栏:
🧊 Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm=1001.2014.3001.5482
🍕 Collection与数据结构 (92平均质量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm=1001.2014.3001.5482
🧀线程与网络(96平均质量分) https://blog.csdn.net/2301_80050796/category_12643370.html?spm=1001.2014.3001.5482
🍭MySql数据库(93平均质量分)https://blog.csdn.net/2301_80050796/category_12629890.html?spm=1001.2014.3001.5482
🍬算法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12676091.html?spm=1001.2014.3001.5482
🍃 Spring(97平均质量分)https://blog.csdn.net/2301_80050796/category_12724152.html?spm=1001.2014.3001.5482
感谢点赞与关注~~~
在这里插入图片描述

目录

  • 1. 回忆数据库中的事务
  • 2. Spring中事务的实现
    • 2.1 Spring编程式事务
    • 2.2 Spring声明式事务@Transactional
    • 2.3 @Transactional的作用
  • 3. @Transactional详解
    • 3.1 rollbackFor
    • 3.2 事务隔离级别
      • 3.2.1 回顾MySQL中的事务隔离级别
      • 3.3.2 Spring事务隔离级别
    • 3.3 Spring事务传播机制
      • 3.3.1 概念
      • 3.3.2 事务的传播机制分类
      • 3.3.3 Spring事务传播机制代码演示

1. 回忆数据库中的事务

https://lilesily12385.blog.csdn.net/article/details/137935719

2. 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:datasource:url: jdbc:mysql://127.0.0.1:3306/trans_test?
characterEncoding=utf8&useSSL=falseusername: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Driver
mybatis:configuration: # 配置打印 MyBatis⽇志 log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case: true #配置驼峰⾃动转换 

创建实体类:

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;
}
mport lombok.Data;
import java.util.Date;
@Data
public class LogInfo {private Integer id;private String userName;private String op;private Date createTime;private Date updateTime;
}

Mapper:

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserInfoMapper {@Insert("insert into user_info(`user_name`,`password`)values(#{name},#{password})")Integer insert(String name,String password);
}
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface LogInfoMapper {@Insert("insert into log_info(`user_name`,`op`)values(#{name},#{op})")Integer insertLog(String name,String op);
}

Service:

@Slf4j
@Service
public class UserService {@Autowiredprivate UserInfoMapper userInfoMapper;public void registryUser(String name,String password){//插⼊⽤⼾信息 userInfoMapper.insert(name,password);}
}
 @Slf4j@Servicepublic class LogService {@Autowiredprivate LogInfoMapper logInfoMapper;public void insertLog(String name,String op){//记录⽤⼾操作 logInfoMapper.insertLog(name,"⽤⼾注册");}}

2.1 Spring编程式事务

Spring手动操作事务主要分为三步,与Sql事务的操作类似:
- 开启事务
- 提交事务
- 或者回滚事务

@RestController
@RequestMapping("/user")
public class TransactionController {@Autowiredprivate UserService userService;@Autowiredprivate DataSourceTransactionManager dataSourceTransactionManager;@Autowiredprivate TransactionDefinition transactionDefinition;@RequestMapping("/registry")public String login(String userName,String password){//开启事务TransactionStatus transaction = dataSourceTransactionManager.getTransaction(transactionDefinition);//用户注册userService.registryUser(userName,password);//提交事务dataSourceTransactionManager.commit(transaction);//回滚事务
//        dataSourceTransactionManager.rollback(transaction);return "注册成功";}
}

SpringBoot内置了两个对象:

  1. DataSourceTransactionManager,数据源事务管理器,一般用于事务的开启回滚和提交.
  2. TransactionDefinition是事务的属性,一般用于传给事务管理器的getTransaction方法,用于事务的获取与开启.
  • 观察事务提交
dataSourceTransactionManager.commit(transaction);

在这里插入图片描述
在这里插入图片描述
我们观察到数据库,数据被插入成功.

  • 观察事务回滚
dataSourceTransactionManager.rollback(transaction);

在这里插入图片描述
在这里插入图片描述
我们看到虽然返回的结果是注册成功,但是数据库中并没有多出任何数据.
下面我们来学习一种简单而快捷的方法,使用注解声明.

2.2 Spring声明式事务@Transactional

一共有两部操作:

  1. 添加依赖
<dependency><groupId>org.springframework</groupId><artifactId>spring-tx</artifactId>
</dependency>
  1. 在事务的方法上添加@Transactional注解就可以实现了.无需手动开启和提交事务,在事务全部执行完成之后会自动提交,在中途发生异常的手会自动回滚.
@RestController
@RequestMapping("/user2")
public class TransactionController2 {@Autowiredprivate UserService userService;@RequestMapping("/registry2")@Transactionalpublic String registry2(String userName,String password){userService.registryUser(userName,password);return "注册成功";}
}

运行程序后,数据插入成功.
修改程序,使之出现异常:

@RestController
@RequestMapping("/user2")
public class TransactionController2 {@Autowiredprivate UserService userService;@RequestMapping("/registry2")@Transactionalpublic String registry2(String userName,String password){userService.registryUser(userName,password);int i = 10/0;return "注册成功";}
}

运行之后,虽然返回了注册成功,但是数据库并没有更新结果.

我们一般写事务的时候会在业务逻辑层来控制事务,因为在业务逻层中,一个业务功能可能会包含多个数据库访问操作,这样就可以把多个访问数据库的操作 合并在同一个事务中.

2.3 @Transactional的作用

@Transactional可以用来修饰方法或者是类.
修饰方法的时候,只对public修饰的方法生效,修饰其他方法也不会报错,但是也不会生效.
修饰类的时候,对类中的所有public方法生效.
在程序出现异常的时候,如果异常没有被捕获,这时候事务就会被回滚,但是如果异常被捕获,就会被认为是正常执行,依然会提交事务.
修改上述代码:

@RestController
@RequestMapping("/user2")
public class TransactionController2 {@Autowiredprivate UserService userService;@RequestMapping("/registry2")@Transactionalpublic String registry2(String userName,String password){userService.registryUser(userName,password);try {int i = 10/0;}catch (ArithmeticException e) {e.printStackTrace();}return "注册成功";}
}

运行程序之后,虽然出错了,由于异常得到了捕获,事务便得到了提交.
以下两种情况,事务依然会回滚:

  1. 重新抛出异常
try {int i = 10/0;
}catch (ArithmeticException e) {throw e;
}
  1. 手动回滚事务
    使用TransactionAspectSupport.currentTransactionStatus()得到当前事务,并使用setRollbackOnly回滚.
try {int i = 10/0;
}catch (ArithmeticException e) {TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}

3. @Transactional详解

我们主要学习@Transactional注解的三个属性:
1. rollbackFor:异常回滚属性,指定能够触发事务回滚的异常类型.可以指定多个异常类型.
2. Isolation:事务的隔离级别,默认是Isolation.DEFAULT.
3. propagation:事务的传播机制.默认值为propagation.REQUIRED

3.1 rollbackFor

异常回滚的时候,@Transactional默认在遇到Error或者运行时异常时才会回滚.
在这里插入图片描述
接下来我们来使用代码验证:

@Transactional
@RequestMapping("/registry3")
public String registry3(String userName,String password) throws IOException {userService.registryUser(userName,password);if (true){throw new IOException();}return "注册成功";
}

向服务器提交数据:
在这里插入图片描述
在这里插入图片描述
我们看到,虽然抛出了异常,但是数据库的数据仍然被修改了.
如果我们要想指定回滚异常的类型,我们需要通过@Transactional的rollbackFor属性来完成,给属性传入异常的类对象来实现对回滚异常的指定,

@RequestMapping("/registry3")
@Transactional(rollbackFor = Exception.class)
public String registry3(String userName,String password) throws IOException {userService.registryUser(userName,password);if (true){throw new IOException();}return "注册成功";
}

运行程序:
在这里插入图片描述在这里插入图片描述
我们看到,事务并没有进行提交,被回滚了,数据库的数据并没有更行.

3.2 事务隔离级别

3.2.1 回顾MySQL中的事务隔离级别

https://lilesily12385.blog.csdn.net/article/details/137935719

3.3.2 Spring事务隔离级别

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

  1. Isolation.DEFAULT: 以连接数据库的隔离级别为准.
  2. Isolation.READ_UNCOMMITTED:读未提交
  3. Isolating.READ_COMMITTED:读提交.
  4. Isolation.REPEATABLE_READ:可重复读.
  5. Isolation.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属性进行设置.

@RequestMapping("/registry3")
@Transactional(isolation = Isolation.DEFAULT)
public String registry3(String userName,String password) throws IOException {userService.registryUser(userName,password);return "注册成功";
}

3.3 Spring事务传播机制

3.3.1 概念

事务的传播机制就是:多个事务方法存在调用关系的时候,事务是如何在这些方法之间进行传播的.事务的传播机制就解决的是一个事务在多个节点上(方法)中的传递.
在这里插入图片描述

比如有两个方法A和B,他们都被@Transactional修饰,方法A调用了方法B.A方法运行的时候,会开启一个新的事物,当A调用B的时候,B方法本身也有事务,此时B方法运行的时候,是假如A事务,还是创建一个新的事务呢?下面我们就来介绍一下事务的传播机制.

3.3.2 事务的传播机制分类

@Transactional注解支持事务的传播机制的设置,我们可以通过propagation属性来设置传播行为.

  1. Propagation.REQUIRED:加入事务,默认的事务传播级别.如果当前存在事务,则加入该事务,如果当前没有事务,则创建一个新的事务.(加入事务,就是共用一个事务,一个事务发生异常,全部回滚.)
  2. Propagation.SUPPORTS:如果当前存在事务,则加入该事务.如果当前没有事务,则以非事务的方式继续运行.
  3. Propagation.MANDATORY:强制性,如果当前存在事务,则加入该事务,如果当前没有事务,则抛出异常.
  4. Propagation.REQUIERS_NEW:新建事务,创建一个新事务如果当前存在事务,则把当前事务挂起.(发生异常,不影响其他事务)也就是不管外部方法是否开启事务,Propagation.REQUIERS_NEW修饰的内部方法都会新开启自己的事务,且开启的事务相互独立,互不干涉.
  5. Propagation.NOT_SUPPORTED: 以非事务的方式运行,如果当前存在事务则把当前事务挂起.
  6. Propagation.NEVER: 不支持当前事务,以非事务的方式运行,如果存在事务,则抛出异常.
  7. Propagation.NESTED: 嵌套事务,如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行,如果当前没有事务,则该取值等价于Propagation.REQUIRED.
    1,4对应,2,5对应,3,6对应.
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.REQUIRES: 如果你有房子,就住你的房子(加入事务),如果你没有房子,我们就一起买房子(创建一个新事务).
  2. Propagation.SUPPORTS: 如果你有房子,我们就住你的房子(加入事务),如果没有房子,我们就租房子(以非事务的方式运行).
  3. Propagation.MANDATORY: 要求必须有房子(加入事务),如果没有房子,就不结婚(抛出异常)
  4. Propagation.REQUIERS_NEW: 必须买新房,不管你有没有房子,必须两个人一起买房(创建一个新事务),即使有房也不住(当前事务挂起).
  5. Propagation.NOT_SUPPORTED: 不管你有没有房子,我都不住(挂起当前事务),必须租房(以非事务的方式运行).
  6. Propagation.NEVER:不能有房子(当前存在事务),有房子就不结婚(抛出异常).
  7. Propagation.NESTED :如果你没房,就⼀起买房.如果你有房,我们就以房子为根据地,做点下生意.(如果如果当前存在事务,则创建⼀个事务作为当前事务的嵌套事务来运行.如果当前没有事务,则该取值等价于PROPAGATION_REQUIRED )

3.3.3 Spring事务传播机制代码演示

重点关注两个: REQUIRED,REQUIERS_NEW.

  • REQUIRED(加入事务)
    用户注册,插入一条数据,并记录操作日志.
@RequestMapping("/r4")
@Transactional
public String registry4(String userName,String password){userService.registryUser(userName,password);logService.insertLog(userName,"用户注册");return "注册成功";
}
@Service
public class UserService {@Autowiredpublic UserInfoMapper userInfoMapper;@Transactional(propagation = Propagation.REQUIRED)public void registryUser(String name,String password) {userInfoMapper.insert(name, password);}
}
@Service
public class LogService {@Autowiredprivate LogInfoMapper logInfoMapper;@Transactional(propagation = Propagation.REQUIRED)public void insertLog(String name,String op){int i = 10/0;//记录⽤⼾操作logInfoMapper.insertLog(name,"⽤户注册");}
}

我们在执行之后,发现数据库中并没有插入任何数据,这就是因为insertLog方法发生了异常,事务发生回滚,当事务回滚之后,registry4方法也发生了回滚,导致了registryUser也发生了回滚,导致数据库中没有插入数据.

  • REQUIRES_NEW(新建事务)
    将上面的UserService和LogService的事务传播机制改为Propagation.REQUIRES_NEW.
@RequestMapping("/r4")
@Transactional
public String registry4(String userName,String password){userService.registryUser(userName,password);logService.insertLog(userName,"用户注册");return "注册成功";
}
@Service
public class LogService {@Autowiredprivate LogInfoMapper logInfoMapper;@Transactional(propagation = Propagation.REQUIRES_NEW)public void insertLog(String name,String op){int i = 10/0;//记录⽤⼾操作logInfoMapper.insertLog(name,"⽤户注册");}
}
@Service
public class UserService {@Autowiredpublic UserInfoMapper userInfoMapper;@Transactional(propagation = Propagation.REQUIRES_NEW)public void registryUser(String name,String password) {userInfoMapper.insert(name, password);}
}

在执行之后,我们会发现,日志表中并没有插入新的数据,但是用户表中插入了新的数据,这是由于UserService,LogServiceregistry4属于不同的事务,LogService出现异常回滚之后不会影响registry4UserService的执行.

  • NEVER(不支持当前事务)
    REQUIRED代码的UserService中的对应方法的事务传播机制修改为Propagation.NEVER.并去掉制造的异常.
@Transactional(propagation = Propagation.NEVER)
public void insertLog(String name,String op){//记录⽤⼾操作logInfoMapper.insertLog(name,"用户注册");
}

运行之后,程序抛出异常,日志表和用户表均没有数据插入.

  • NESTED(嵌套事务)
    将上述REQUIREDUserService中的对应方法的事务传播机制修改为Propagation.NESTED.
@Transactional(propagation = Propagation.NESTED)
public void insertLog(String name,String op){int i = 10/0;//记录用户操作logInfoMapper.insertLog(name,"用户注册");
}
@Transactional(propagation = Propagation.NESTED)`在这里插入代码片`
public void registryUser(String name,String password) {userInfoMapper.insert(name, password);
}

运行程序之后,两张表中都没有插入任何数据,如果我们对出现异常的方法进行手动回滚:

@Transactional(propagation = Propagation.NESTED)
public void insertLog(String name,String op){try {int i = 10/0;}catch (Exception e){TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();}//记录用户操作logInfoMapper.insertLog(name,"用户注册");
}

在这里插入图片描述
我们看到,用户表插入成功了,但是日志表的数据被回滚了.
在这里插入图片描述
在这里插入图片描述

区分REQUIRED和NESTED
REQUIRED:当其中一个事务出现异常的时候,所有事务都会回滚,如果try-catch语句中对事物进行手动回滚,则子事务和父事务全部会被回滚.
NESTED : 当子事务出现异常的时候,子事务对应的父事务也会回滚,但是如果在有异常的子事务中进行try-catch,catch中对事务进行手动回滚,则只有出现异常的事务被回滚,但是另一个没有出现异常的子事务没有被回滚,与REQUIRED最大的区别就是,NESTED可以做到部分回滚,但是REQUIRED只能做到全部回滚.

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 以下关于revision历史版本说法正确的是:
  • C语言-使用指针数组作为函数参数,实现对10个字符串进行排序
  • 海南云亿商务咨询有限公司引领抖音电商新潮流
  • 如何高效记录并整理编程学习笔记
  • rsync远程同步服务
  • SpringBoot解决创建项目无法选择JDK8和JDK11
  • 互斥锁以及进程间通信
  • 无人机之飞控系统基本功能
  • 遗传算法与深度学习实战(4)——遗传算法详解与实现
  • 视觉SLAM第六讲
  • vue3项目中使用 vue-i18n国际化插件,实现多语言效果
  • 响应式Web设计:纯HTML和CSS的实现技巧
  • Dapp链游如何应对DDoS攻击的全方位策略
  • PHP概述、环境搭建与基本语法讲解
  • Eureka 原理与实践详解:深入理解与代码分析
  • 【剑指offer】让抽象问题具体化
  • ES6核心特性
  • GraphQL学习过程应该是这样的
  • javascript 哈希表
  • jquery ajax学习笔记
  • Netty 4.1 源代码学习:线程模型
  • REST架构的思考
  • SpiderData 2019年2月13日 DApp数据排行榜
  • 动手做个聊天室,前端工程师百无聊赖的人生
  • 二维平面内的碰撞检测【一】
  • 更好理解的面向对象的Javascript 1 —— 动态类型和多态
  • 诡异!React stopPropagation失灵
  • 简单基于spring的redis配置(单机和集群模式)
  • 聊聊redis的数据结构的应用
  • 入门到放弃node系列之Hello Word篇
  • 再谈express与koa的对比
  • k8s使用glusterfs实现动态持久化存储
  • 选择阿里云数据库HBase版十大理由
  • 资深实践篇 | 基于Kubernetes 1.61的Kubernetes Scheduler 调度详解 ...
  • ​LeetCode解法汇总2808. 使循环数组所有元素相等的最少秒数
  • ​软考-高级-系统架构设计师教程(清华第2版)【第15章 面向服务架构设计理论与实践(P527~554)-思维导图】​
  • #《AI中文版》V3 第 1 章 概述
  • #LLM入门|Prompt#3.3_存储_Memory
  • $ is not function   和JQUERY 命名 冲突的解说 Jquer问题 (
  • (2024,Flag-DiT,文本引导的多模态生成,SR,统一的标记化,RoPE、RMSNorm 和流匹配)Lumina-T2X
  • (MTK)java文件添加简单接口并配置相应的SELinux avc 权限笔记2
  • (二)原生js案例之数码时钟计时
  • (附源码)apringboot计算机专业大学生就业指南 毕业设计061355
  • (附源码)springboot 校园学生兼职系统 毕业设计 742122
  • (九)One-Wire总线-DS18B20
  • (十)Flink Table API 和 SQL 基本概念
  • (学习日记)2024.01.19
  • (循环依赖问题)学习spring的第九天
  • (转)C#开发微信门户及应用(1)--开始使用微信接口
  • (转)详解PHP处理密码的几种方式
  • (转)重识new
  • ..thread“main“ com.fasterxml.jackson.databind.JsonMappingException: Jackson version is too old 2.3.1
  • .NET C# 使用 SetWindowsHookEx 监听鼠标或键盘消息以及此方法的坑
  • .net php 通信,flash与asp/php/asp.net通信的方法
  • .NET 指南:抽象化实现的基类