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

Spring-事务和事务传播机制

上篇文章:

Spring-AOPhttps://blog.csdn.net/sniper_fandc/article/details/148960179?fromshare=blogdetail&sharetype=blogdetail&sharerId=148960179&sharerefer=PC&sharesource=sniper_fandc&sharefrom=from_link

目录

1 数据准备

2 编程式事务

3 声明式事务

4 @Transactional注解详解

4.1 作用域

4.2 提交与回滚逻辑

5 事务隔离级别

6 事务传播机制

6.1 Propagation.REQUIRED

6.2 Propagation.SUPPORTS

6.3 Propagation.MANDATORY

6.4 Propagation.REQUIRES_NEW

6.5 Propagation.NOT_SUPPORTED

6.6 Propagation.NEVER

6.7 Propagation.NESTED


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

        在数据库中,事务的操作:

-- 开启事务start transaction;-- 提交事务commit;-- 回滚事务rollback;

1 数据准备

        创建数据库和数据表:

-- 创建数据库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 CHARACTERSET = 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';

        配置Spring配置文件:

spring:datasource:url: jdbc:mysql://127.0.0.1:3306/trans_test?characterEncoding=utf8&useSSL=falseusername: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Drivermybatis:configuration: # 配置打印 MyBatis日志log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case: true #配置驼峰自动转换

        实体类:

@Datapublic class UserInfo {private Integer id;private String userName;private String password;private Date createTime;private Date updateTime;}@Datapublic class LogInfo {private Integer id;private String userName;private String op;private Date createTime;private Date updateTime;}

        Mapper层:

@Mapperpublic interface UserInfoMapper {@Insert("insert into user_info(user_name,password)values(#{name},#{password})")Integer insert(@Param("name") String name, @Param("password")String password);}@Mapperpublic interface LogInfoMapper {@Insert("insert into log_info(user_name,op)values(#{name},#{op})")Integer insertLog(@Param("name") String name, @Param("op") String op);}

        Service层:

@Slf4j@Servicepublic 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 编程式事务

        即手动编写代码开始事务、提交事务或回滚事务。

@RequestMapping("/user")@RestController@Slf4jpublic class UserController {// JDBC 事务管理器@Autowiredprivate DataSourceTransactionManager dataSourceTransactionManager;// 定义事务属性@Autowiredprivate TransactionDefinition transactionDefinition;@Autowiredprivate UserService userService;@RequestMapping("/registry")public String registry(String name, String password) {//开启事务TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);//用户注册userService.registryUser(name, password);log.info("用户插入成功");//提交事务dataSourceTransactionManager.commit(transactionStatus);//回滚事务//dataSourceTransactionManager.rollback(transactionStatus);return "注册成功";}}

        DataSourceTransactionManager是Spring提供给我们的事务管理器的Bean,而TransactionDefinition是Spring提供给我们定义事务属性(状态)的Bean,通过事务管理器的commit()来提交事务。

        如果回滚事务,就需要通过事务管理器的rollback()来回滚。

        观察日志发现,回滚事务的日志比提交事务的日志少了一条日志信息:committing。并且虽然显示用户插入成功,但是数据库没有lisi的信息:

        再次尝试提交lisi用户插入的事务:

        注意到,lisi用户成功插入,但是id却不连续,缺少的id正是上次回滚lisi的事务的id。这说明,事务的机制类似于在开启另一条时间线,在相同的时间节点上,开启的事务会在另一条时间线操作,而提交事务时,就会将另一条时间线操作结果合并到主时间线上。回滚事务时,就直接删除另一条时间线,但是却会留下一些操作数据库的痕迹,比如上述id的不连续。

3 声明式事务

        编程式事务操作比较麻烦,而声明式事务是利用@Transactional注解,操作简单:

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

        @Transactional注解在方法执行前会自动开启事务,然后执行目标方法,如果方法执行成功(不发生异常)就提交事务;否则回滚事务。

        强制方法出现异常,事务没有提交,即进行了回滚:

4 @Transactional注解详解

4.1 作用域

        @Transactional注解可以用来修饰类或方法,如果修饰方法,只有public方法执行时才会自动开启事务,其他方法不生效;如果修饰类,表示类下的所有public方法执行时都自动开启事务。

4.2 提交与回滚逻辑

        如果方法正常执行,则执行后提交事务;如果方法发生异常,则回滚事务。对于发生异常,具体流程与细节见下图:

        上述案例如果把抛出的异常捕获后,也不会回滚事务,而是提交事务:

    @Transactional@RequestMapping("/registry1")public String registry1(String name,String password){//用户注册userService.registryUser(name,password);log.info("用户插入成功");//对异常进行捕获try {//强制程序抛出异常int a = 10/0;}catch (Exception e){e.printStackTrace();}return "注册成功";}

        注意:但是如果既希望捕获异常(不会回滚事务),又希望回滚事务,有两种做法:1.catch捕获后重新抛出throw异常。2.catch捕获后手动回滚事务:TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。

        当抛出的异常不是运行时异常(RuntimeException或Error,上述的算数异常是RuntimeException的子类)时,默认是不会回滚的:   

    @Transactional@RequestMapping("/registry2")public String registry2(String name,String password) throws IOException {//用户注册userService.registryUser(name,password);log.info("用户插入成功");//抛出IO异常(不属于运行时异常),不会回滚if(true){throw new IOException();}return "注册成功";}

        抛出的异常不是运行时异常(RuntimeException)时,如果想要回滚,设置@Transactional(rollbackFor = Exception.class)注解的rollbackFor属性为所有的Exception类:

    @Transactional(rollbackFor = Exception.class)@RequestMapping("/registry3")public String registry3(String name,String password) throws IOException {//用户注册userService.registryUser(name,password);log.info("用户插入成功");//抛出IO异常(不属于运行时异常),但是rollbackFor属性指定了所有的Exception都会回滚if(true){throw new IOException();}return "注册成功";}

5 事务隔离级别

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

        (1)读未提交:一个事务能读到另一个事务已经执行但未提交的数据。级别最低。

        注意:读未提交会引起脏读问题。脏读是一个事务读取的数据并不是最新的真实数据(把这种不是新的真实的数据称为脏数据)。原因是因为一个事务可能读到另一个事务未提交的数据,但是另一个事务可能执行回滚操作,导致读到的数据成为脏数据。

        (2)读已提交:一个事务只能读到另一个事务已经提交的数据。级别较低。

        注意:读已提交不会引起脏读问题,会引起不可重复读问题。不可重复读是指一个事务读取另一个事务操作的数据,在该事务执行期间多次读取到的数据不一样。

        (3)可重复读:一个事务多次读取另一个事务修改的数据,多次读取结果一样(对修改不敏感,对插入敏感)。级别较高,是MySQL的默认事务隔离级别。

        注意:可重复读不会引起脏读、不可重复读问题,会引起幻读问题。幻读是指可重复读对插入敏感,因此事务1如果读取事务2插入的数据,会感知到已经插入的新数据,但是由于可重复读每次查询结果一样(即查询结果没有新插入的数据),事务1又要插入重复的新数据(由于唯一约束),插入失败。出现了明明数据库查不到该数据,自己要插入该数据又插不进去的幻觉。

        (4)串行化:序列化,强制对事务执行顺序排序,使之不发生冲突,从而解决脏读(读未提交引起)、不可重复读(读已提交引起)和幻读(可重复读引起)的问题。级别最高,效率低下(可并行执行的事务被串行执行)。

        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。

        可以通过@Transactional(isolation = Isolation.READ_COMMITTED)注解的isolation属性设置事务隔离级别。

6 事务传播机制

        事务传播机制不是MySQL的,而是存在Spring开发中。它是指方法之间存在调用链,如果方法都各自开启了事务,那么调用时事务是如何传播的(是再开启一个事务还是共用一个)。

        @Transactional注解的propagation属性,指明了Spring有7种事务传播机制:

6.1 Propagation.REQUIRED

        默认的事务传播机制。如果当前调用方法开启事务,被调用方法就加入该事务。如果调用方法没有事务,被调用方法就开启一个新事务。

@Slf4j@Servicepublic class UserService {@Autowiredprivate UserInfoMapper userInfoMapper;@Transactionalpublic void registryUser(String name, String password) {//插入用户信息userInfoMapper.insert(name, password);}}@Slf4j@Servicepublic class LogService {@Autowiredprivate LogInfoMapper logInfoMapper;@Transactionalpublic void insertLog(String name, String op) {int a = 10/0;//记录用户操作logInfoMapper.insertLog(name, "用户注册");}}@RequestMapping("/propagation")@RestControllerpublic class PropagationController {@Autowiredprivate UserService userService;@Autowiredprivate LogService logService;@Transactional@RequestMapping("/p1")public String r1(String name, String password) {//用户注册userService.registryUser(name, password);//记录操作日志logService.insertLog(name, "用户主动注册");return "r1";}}

        这里主动在insertLog()方法的事务中抛出算数异常(运行时异常),观察一个事务执行失败、另一个事务执行成功的最终的结果:

        可以发现,当一个事务抛出异常,最终事务是没有提交的,数据库也没有数据插入。因为@Transactional默认是@Transactional(propagation = Propagation.REQUIRED),该级别下被调用方法和调用方法使用的是同一个事务,因此一个事务中任何一个操作失败都会回滚。

6.2 Propagation.SUPPORTS

        如果当前调用方法开启事务,被调用方法就加入该事务。如果调用方法没有事务,被调用方法就以不开启事务的方式执行。

@RequestMapping("/propagation")@RestControllerpublic class PropagationController {@Autowiredprivate UserService userService;@Autowiredprivate LogService logService;@RequestMapping("/p1")public String r1(String name, String password) {//用户注册userService.registryUser(name, password);//记录操作日志logService.insertLog(name, "用户主动注册");return "r1";}}@Slf4j@Servicepublic class UserService {@Autowiredprivate UserInfoMapper userInfoMapper;@Transactional(propagation = Propagation.SUPPORTS)public void registryUser(String name, String password) {//插入用户信息userInfoMapper.insert(name, password);}}@Slf4j@Servicepublic class LogService {@Autowiredprivate LogInfoMapper logInfoMapper;@Transactional(propagation = Propagation.SUPPORTS)public void insertLog(String name, String op) {//记录用户操作logInfoMapper.insertLog(name, "用户注册");int a = 10/0;}}

        实际上,数据库是成功在两个表中插入了数据。这是因为Propagation.SUPPORTS事务传播机制找不到调用方法的异常,因此两个插入实际上以非事务方式执行,SQL语句没有错误就可以插入成功,而抛出的异常在SQL执行之后,不涉及事务的情形下并不会回滚。

6.3 Propagation.MANDATORY

        强制性的事务传播机制。如果当前调用方法开启事务,被调用方法就加入该事务。如果调用方法没有事务,被调用方法就抛出异常。

@RequestMapping("/propagation")@RestControllerpublic class PropagationController {@Autowiredprivate UserService userService;@Autowiredprivate LogService logService;@RequestMapping("/p1")public String r1(String name, String password) {//用户注册userService.registryUser(name, password);//记录操作日志logService.insertLog(name, "用户主动注册");return "r1";}}@Slf4j@Servicepublic class UserService {@Autowiredprivate UserInfoMapper userInfoMapper;@Transactional(propagation = Propagation.MANDATORY)public void registryUser(String name, String password) {//插入用户信息userInfoMapper.insert(name, password);}}@Slf4j@Servicepublic class LogService {@Autowiredprivate LogInfoMapper logInfoMapper;@Transactional(propagation = Propagation.MANDATORY)public void insertLog(String name, String op) {//记录用户操作logInfoMapper.insertLog(name, "用户注册");}}

        运行时直接抛出异常IllegalTransactionStateException。

6.4 Propagation.REQUIRES_NEW

        创建一个新事务。无论调用方法是否开启事务(如果开启了就挂起该事务),被调用方法都会创建一个新事务,并且多个被调用方法开启的事务之间相互独立不干扰。

@RequestMapping("/propagation")@RestControllerpublic class PropagationController {@Autowiredprivate UserService userService;@Autowiredprivate LogService logService;@Transactional@RequestMapping("/p1")public String r1(String name, String password) {//用户注册userService.registryUser(name, password);//记录操作日志logService.insertLog(name, "用户主动注册");return "r1";}}@Slf4j@Servicepublic class UserService {@Autowiredprivate UserInfoMapper userInfoMapper;@Transactional(propagation = Propagation.REQUIRES_NEW)public void registryUser(String name, String password) {//插入用户信息userInfoMapper.insert(name, password);}}@Slf4j@Servicepublic class LogService {@Autowiredprivate LogInfoMapper logInfoMapper;@Transactional(propagation = Propagation.REQUIRES_NEW)public void insertLog(String name, String op) {//记录用户操作logInfoMapper.insertLog(name, "用户注册");int a = 10/0;}}

        根据结果发现,由于调用方法开启了事务,因此被挂起。两个插入操作分别开启各自的新事务,两个事务之间独立,没有互相影响。插入log_info的方法抛出异常,因此回滚。而插入user_info的方法没有抛出异常,因此提交。

6.5 Propagation.NOT_SUPPORTED

        以非事务的方式执行。就算开启了事务,也会挂起该事务。这个简单,不再演示。

6.6 Propagation.NEVER

        以非事务的方式执行。如果开启了事务,就会抛出异常。这个简单,不再演示。

6.7 Propagation.NESTED

        如果当前调用方法开启事务,则被调用方法创建一个事务作为当前事务的嵌套事务(理解为子事务)来运行。如果当前调用方法没有开启事务,则该取值等价于PROPAGATION_REQUIRED,被调用方法开启一个新事务。

@RequestMapping("/propagation")@RestControllerpublic class PropagationController {@Autowiredprivate UserService userService;@Autowiredprivate LogService logService;@Transactional@RequestMapping("/p1")public String r1(String name, String password) {//用户注册userService.registryUser(name, password);//记录操作日志logService.insertLog(name, "用户主动注册");return "r1";}}@Slf4j@Servicepublic class UserService {@Autowiredprivate UserInfoMapper userInfoMapper;@Transactional(propagation = Propagation.NESTED)public void registryUser(String name, String password) {//插入用户信息userInfoMapper.insert(name, password);}}@Slf4j@Servicepublic class LogService {@Autowiredprivate LogInfoMapper logInfoMapper;@Transactional(propagation = Propagation.NESTED)public void insertLog(String name, String op) {//记录用户操作logInfoMapper.insertLog(name, "用户注册");int a = 10/0;}}

        当调用方法开启事务时,被调用方法就创建嵌套事务(子事务),因此子事务抛出异常就会导致整个事务都进行回滚(子事务是父事务的一部分),没有一条数据插入。

        这么看来Propagation.NESTED不就等于Propagation.REQUIRED了吗?实际上两种事务传播机制还是有区别的:

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

        把抛出的异常进行捕获,此时该子事务会提交,因此整个事务也会提交,这和Propagation.REQUIRED也一样。但是如果在子事务捕获异常后强制回滚,观察结果:

        最终数据库中只有user_info的数据插入成功,而log_info的数据进行了回滚。即实现了部分嵌套事务的回滚。而Propagation.REQUIRED事务传播机制是无法进行部分回滚的:

@Slf4j@Servicepublic class UserService {@Autowiredprivate UserInfoMapper userInfoMapper;@Transactional(propagation = Propagation.REQUIRED)public void registryUser(String name, String password) {//插入用户信息userInfoMapper.insert(name, password);}}@Slf4j@Servicepublic class LogService {@Autowiredprivate LogInfoMapper logInfoMapper;@Transactional(propagation = Propagation.REQUIRED)public void insertLog(String name, String op) {//记录用户操作logInfoMapper.insertLog(name, "用户注册");try {int a = 10/0;} catch (Exception e) {e.printStackTrace();TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();}}}

        因为Propagation.REQUIRED事务传播机制本质是所有方法都使用同一个事务,因此只要有一个被调用方法进行回滚,都会进行回滚。而Propagation.NESTED事务传播机制是在父事务这个主线下所有子事务的执行,因此子事务如果捕获异常后强制回滚,父事务认为子事务已经处理了异常,整个父事务也可以进行提交。就出现了部分嵌套事务回滚的现象。

        总结:当事务执行成功,Propagation.REQUIRED和Propagation.NESTED一样。当事务部分执行成功,Propagation.REQUIRED会导致整个事务回滚;Propagation.NESTED处理妥当只会让执行失败(抛出异常)的事务回滚,其他执行成功的嵌套事务成功提交。

        注意:嵌套事务之所以能够实现部分事务的回滚,是因为事务中有一个保存点(savepoint)的概念,嵌套事务进⼊之后相当于新建了一个保存点,而回滚时只回滚到当前保存点。

下篇文章:

Spring原理—Bean作用域https://blog.csdn.net/sniper_fandc/article/details/148975847?fromshare=blogdetail&sharetype=blogdetail&sharerId=148975847&sharerefer=PC&sharesource=sniper_fandc&sharefrom=from_link

http://www.dtcms.com/a/263573.html

相关文章:

  • DFMEA检查表模板下载
  • 简单的 PyTorch 示例,可视化和解释 weight decay 的作用
  • 云上攻防—Docker安全容器逃逸特权模式危险挂载
  • 【C++】简单学——模板初阶
  • tauri v2 开源项目学习(一)
  • PSQL 处理 BLOB 类型数据问题
  • 华为云Flexus+DeepSeek征文 | ​​华为云ModelArts Studio大模型与企业AI会议纪要场景的对接方案
  • 数据库事务全面指南:概念、语法、机制与最佳实践
  • C++ 快速回顾(五)
  • 【冷知识】Spring Boot 配置文件外置
  • SpringBoot -- 自动配置原理
  • Bessel位势方程求解步骤
  • STL简介+string模拟实现
  • 「Java案例」计算矩形面积
  • 大数据(3)-Hive
  • 【算法】动态规划:1137. 第 N 个泰波那契数
  • 初等变换 线性代数
  • C++ STL之string类
  • Windows11系统中安装docker并配置docker镜像到pycharm中
  • EA自动交易完全指南:从策略设计到实盘部署
  • SpringBoot 启动入口深度解析:main方法执行全流程
  • Android Telephony 网络状态中的 NAS 信息
  • 反射,枚举和lambda表达式
  • 《垒球百科》老年俱乐部有哪些项目·垒球1号位
  • 从零到一通过Web技术开发一个五子棋
  • 【MySQL基础】MySQL索引全面解析:从原理到实践
  • 人形机器人_双足行走动力学:MIT机器人跌落自恢复算法及应用
  • 使用Verilog设计模块输出中位数,尽可能较少资源使用
  • 本周股指想法
  • 产品背景知识——API、SDK、Library、Framework、Protocol