• 【Spring】Spring 事务和事务传播机制


    1、为什么需要事务?(回顾)

    事务定义: 将⼀组操作封装成⼀个执⾏单元(封装到⼀起),要么全部成功,要么全部失败。
    为什么要用事务?
    ⽐如转账分为两个操作:

    第⼀步操作:A 账户 -100 元。
    第⼆步操作:B 账户 +100 元。

    如果没有事务,第⼀步执⾏成功了,第⼆步执⾏失败了,那么 A 账户平⽩⽆故的 100 元就“⼈间蒸
    发”了。⽽如果使⽤事务就可以解决这个问题,让这⼀组操作要么⼀起成功,要么⼀起失败。

    2、Spring 中事务的实现

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

    1. ⼿动操作事务。
    2. 声明式⾃动提交事务。

    在开始讲解它们之前,咱们先来回顾事务在 MySQL 中是如何使⽤的❓🤔

    2.1 MySQL 中的事务使用(回顾)

    事务在 MySQL 有 3 个重要的操作:开启事务、提交事务、回滚事务,它们对应的操作命令如下:

    -- 开启事务
    start transaction;
    -- 业务执⾏
    -- 提交事务
    commit;
    -- 回滚事务
    rollback;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    2.2 Spring 中编程式事务的实现

    此方式包含了 3 个重要的操作:

    1. 获取事务(开启事务)
    2. 提交事务
    3. 回滚事务

    依赖 两个重要的对象:

    • DataSourceTransactionManager:⽤来获取事务(开启事务)、提交或回滚事务的
    • TransactionDefinition :是事务的属性,在获取事务的时候需要将TransactionDefinition 传递进去从而获得⼀个事务 TransactionStatus

    实现代码如下:
    application.yml

    # 配置数据库连接
    spring:
      datasource:
        url: jdbc:mysql://127.0.0.1:3306/mycnblog?characterEncoding=utf8&useSSL=false&serverTimezone=UTC
    
        username: root
        password: 666666
        driver-class-name: com.mysql.cj.jdbc.Driver
    # 配置mybatis xml 保存路径
    mybatis:
      mapper-locations: classpath:mybatis/**Mapper.xml
      configuration: # 配置打印MyBatis执行的SQL
       log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    # 配置打印MyBatis执行的SQL
    logging:
      level:
        com:
          example:
            demo: debug
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    UserMapper

    package com.example.demo.mapper;
    
    import com.example.demo.model.UserInfo;
    import org.apache.ibatis.annotations.Mapper;
    
    /**
     * @date 2022/8/17 7:48
     */
    @Mapper
    public interface UserMapper {
        int  add(UserInfo userInfo);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    UserMapper.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.example.demo.mapper.UserMapper">
    
        <insert id="add">
            insert into userinfo(username,password) values(#{username},#{password})
        </insert>
    </mapper>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    UserInfo

    package com.example.demo.model;
    
    import lombok.Data;
    
    /**
     * @date 2022/8/17 7:48
     */
    @Data
    public class UserInfo {
        private int id;
        private String username;
        private String password;
        private String photo;
        private String createtime;
        private String updatetime;
        private int state;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    UserService

    package com.example.demo.service;
    
    import com.example.demo.mapper.UserMapper;
    import com.example.demo.model.UserInfo;
    import org.springframework.stereotype.Service;
    
    import javax.annotation.Resource;
    
    /**
     * @date 2022/8/17 7:57
     */
    @Service
    public class UserService {
        @Resource
        private UserMapper userMapper;
        public int add(UserInfo userInfo){
            return userMapper.add(userInfo);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    UserController

    package com.example.demo.controller;
    
    import com.example.demo.model.UserInfo;
    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.util.StringUtils;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * @date 2022/8/17 7:56
     */
    @RestController
    public class UserController {
        @Autowired
        private UserService userService;
        // JDBC 事务管理器
        @Autowired
        private DataSourceTransactionManager transactionManager;
         // 定义事务属性
        @Autowired
        private TransactionDefinition transactionDefinition;
        // 在此方法中使用编程式的事务
        @RequestMapping("/add")
        public int add(UserInfo userInfo){
            // 非空校验—验证用户名和密码不能为空
            if(userInfo==null || !StringUtils.hasLength(userInfo.getUsername())||
                    !StringUtils.hasLength(userInfo.getPassword())) {
                return 0;
            }
            // 开启事务(获取事务)
            TransactionStatus transactionStatus=
                    transactionManager.getTransaction(transactionDefinition);
            int result= userService.add(userInfo);
            System.out.println("add 受影响的行数: "+result);
            // 提交事务/回滚事务
            transactionManager.rollback(transactionStatus);// 回滚事务
            return result;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    测试结果如下:
    在这里插入图片描述
    在这里插入图片描述

    而我们数据库中并没有添加进数据(实现了事务的回滚):

    在这里插入图片描述
    要想提交的话,执行下列代码:
    在这里插入图片描述
    测试结果如下:
    在这里插入图片描述
    在这里插入图片描述

    2.3 Spring 声明式事务(自动事务)

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

    具体实现代码如下:

        // 使用声明式事务
        @Transactional // 在进入方法之前,自动开启事务,在方法执行完之后,自动提交事务,如果出现异常,则自动回滚事务
        @RequestMapping("/add1")
        public int add1(UserInfo userInfo){
            // 非空校验—验证用户名和密码不能为空
            if(userInfo==null || !StringUtils.hasLength(userInfo.getUsername())||
                    !StringUtils.hasLength(userInfo.getPassword())) {
                return 0;
            }
            int result= userService.add(userInfo);
            System.out.println("add 受影响的行数: "+result);
            int n=10/0;
            return result;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    测试结果如下:
    在这里插入图片描述
    显示添加成功了:
    在这里插入图片描述
    但是数据库中并没有添加数据,这是因为代码中有报错,执行了自动回滚事务
    在这里插入图片描述
    没有异常后就可以真正的把数据添加进来了:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    2.3.1 @Transactional 作⽤范围

    @Transactional 可以⽤来修饰⽅法

    • 修饰⽅法时:需要注意只能应⽤到 public ⽅法上,否则不⽣效。
    • 修饰类时:表明该注解对该类中所有的 public ⽅法都⽣效(都会自动的开启和提交(回滚)事务)。

    2.3.2 @Transactional 参数说明

    参数作用
    value当配置了多个事务管理器时, 可以使用该属性指定选择哪个事务管理器
    transactionManager当配置了多个事务管理器时, 可以使用该属性指定选择哪个事务管理器
    propagation事务的传播行为. 默认为 Propagation.REQUIRED
    isolation事务的隔离级别. 默认为 Isolation.DAEFAULT
    timeout事务的超时事件. 默认值为-1, 如果超过该时间限制但事务还没有完成, 则自动回滚事务.
    readOnly指定事务是否只读事务. 默认为 false. 为了忽略那些不需要事务的方法, 比如读取数据, 可以设置为 read-only 为 true
    rollbackFor用于指定能够触发事务回滚的异常类型, 可以指定多个异常类型
    rollbackForClassName用于指定能够触发事务回滚的异常类型, 可以指定多个异常类型
    noRollbackFor抛出指定的异常类型, 不回滚事务, 也可以指定多个异常类型
    noRollbackForClassName抛出指定的异常类型, 不回滚事务, 也可以指定多个异常类型

    2.3.3 注意事项

    @Transactional 在异常被捕获的情况下,不会进⾏事务⾃动回滚

    验证以下代码是否会发⽣事务回滚:

     @Transactional() // 在进入方法之前,自动开启事务,在方法执行完之后,自动提交事务,如果出现异常,则自动回滚事务
        @RequestMapping("/add2")
        public int add2(UserInfo userInfo){
            // 非空校验—验证用户名和密码不能为空
            if(userInfo==null || !StringUtils.hasLength(userInfo.getUsername())||
                    !StringUtils.hasLength(userInfo.getPassword())) {
                return 0;
            }
            int result= userService.add(userInfo);
            System.out.println("add 受影响的行数: "+result);
            try {
                int n=10/0;
            }catch (Exception e){
    
            }
            return result;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    在这里插入图片描述
    测试结果如下:
    在这里插入图片描述
    并没有进行事务的回滚⬇️
    在这里插入图片描述
    解决⽅案1 对于捕获的异常,事务是会⾃动回滚的,因此解决⽅案1就是可以将异常重新抛出,具体实现如下:
    在这里插入图片描述
    测试结果如下:
    在这里插入图片描述
    可以发现数据并没有插入到数据库中:
    在这里插入图片描述
    解决⽅案2 ⼿动回滚事务,在⽅法中使⽤ TransactionAspectSupport.currentTransactionStatus() 可以得到当前的事务,然后设置回滚⽅法 setRollbackOnly 就可以实现回滚了,具体实现代码如下:
    在这里插入图片描述
    测试结果如下:
    在这里插入图片描述
    数据也没有插入到数据库中:
    在这里插入图片描述

    2.3.4 @Transactional ⼯作原理

    @Transactional 是基于 AOP 实现的,AOP ⼜是使⽤动态代理实现的。如果⽬标对象实现了接⼝,默认情况下会采⽤ JDK 的动态代理,如果⽬标对象没有实现了接⼝,会使⽤ CGLIB 动态代理。
    @Transactional 在开始执⾏业务之前,通过代理先开启事务,在执⾏成功之后再提交事务。如果中途遇到的异常,则回滚事务。
    @Transactional 实现思路预览:
    在这里插入图片描述
    @Transactional 具体执⾏细节如下图所示:
    在这里插入图片描述

    3、事务隔离级别

    3.1 事务特性回顾

    事务有4 ⼤特性(ACID):原⼦性、持久性、⼀致性和隔离性,具体概念如下:

    • 原⼦性: ⼀个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执⾏过程中发⽣错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执⾏过⼀样。
    • ⼀致性: 在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写⼊的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以⾃发性地完成预定的⼯作。
    • 持久性: 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
    • 隔离性: 数据库允许多个并发事务同时对其数据进⾏读写和修改的能⼒,隔离性可以防⽌多个事务并发执⾏时由于交叉执⾏⽽导致数据的不⼀致。事务隔离分为不同级别,包括读未提交(Readuncommitted)、读提交(read committed)、可重复读(repeatable read)和串⾏化
      (Serializable)。

    上⾯ 4 个属性,可以简称为ACID。

    原⼦性(Atomicity,或称不可分割性)
    ⼀致性(Consistency)
    隔离性(Isolation,⼜称独⽴性)
    持久性(Durability)

    ⽽这 4 种特性中,只有隔离性(隔离级别)是可以设置的。

    3.2 Spring 中设置事务隔离级别

    Spring 中事务隔离级别可以通过 @Transactional 中的 isolation 属性进⾏设置,具体操作如下图所示:
    在这里插入图片描述

    3.2.1 MySQL 事务隔离级别有 4 种

    1. READ UNCOMMITTED读未提交,也叫未提交读,该隔离级别的事务可以看到其他事务中未提交的数据。该隔离级别因为可以读取到其他事务中未提交的数据,⽽未提交的数据可能会发⽣回滚,因此我们把该级别读取到的数据称之为脏数据,把这个问题称之为脏读
    2. READ COMMITTED读已提交,也叫提交读,该隔离级别的事务能读取到已经提交事务的数据,因此它不会有脏读问题。但由于在事务的执⾏中可以读取到其他事务提交的结果,所以在不同时间的相同 SQL 查询中,可能会得到不同的结果,这种现象叫做不可重复读
    3. REPEATABLE READ可重复读,是 MySQL 的默认事务隔离级别,它能确保同⼀事务多次查询的结果⼀致。但也会有新的问题,⽐如此级别的事务正在执⾏时,另⼀个事务成功的插⼊了某条数据,但因为它每次查询的结果都是⼀样的,所以会导致查询不到这条数据,⾃⼰重复插⼊时⼜失败(因为唯⼀约束的原因)。明明在事务中查询不到这条信息,但⾃⼰就是插⼊不进去,这就叫幻读(Phantom Read)
    4. SERIALIZABLE序列化,事务最高隔离级别,它会强制事务排序,使之不会发⽣冲突,从⽽解决了脏读、不可重复读和幻读问题,但因为执⾏效率低,所以真正使⽤的场景并不多

    在这里插入图片描述

    • 脏读:⼀个事务读取到了另⼀个事务修改的数据之后,后⼀个事务⼜进⾏了回滚操作,从⽽导致第⼀个事务读取的数据是错的。
    • 不可重复读:⼀个事务两次查询得到的结果不同,因为在两次查询中间,有另⼀个事务把数据修改了。
    • 幻读:⼀个事务两次查询中得到的结果集不同,因为在两次查询中另⼀个事务有新增了⼀部分数据。

    3.2.2 Spring 事务隔离级别有 5 种

    1. Isolation.DEFAULT:以连接的数据库的事务隔离级别为主。
    2. Isolation.READ_UNCOMMITTED:读未提交,可以读取到未提交的事务,存在脏读。
    3. Isolation.READ_COMMITTED:读已提交,只能读取到已经提交的事务,解决了脏读,存在不可重复读。
    4. Isolation.REPEATABLE_READ:可重复读,解决了不可重复读,但存在幻读(MySQL默认级别)。
    5. Isolation.SERIALIZABLE:串⾏化,可以解决所有并发问题,但性能太低。从上述介绍可以看出,相⽐于 MySQL 的事务隔离级别,Spring 的事务隔离级别只是多了⼀个Isolation.DEFAULT(以数据库的全局事务隔离级别为主)

    在这里插入图片描述
    注意事项:

    1. 当 Spring 中设置了事务隔离级别和连接的数据库 (MySQL) 事务隔离级别发生冲突时,那么以 Spring 的为准
    2. Spring 中的事务隔离级别机制的实现是依靠连接数据库支持事务隔离级别为基础

    4、Spring 事务传播机制

    4.1 事务传播机制是什么?

    Spring 事务传播机制定义了多个包含了事务的⽅法,相互调⽤时,事务是如何在这些⽅法间进⾏传递
    的。

    4.2 为什么需要事务传播机制?

    事务隔离级别是保证多个并发事务执⾏的可控性的(稳定性的),⽽事务传播机制保证⼀个事务在多个调⽤⽅法间的可控性的(稳定性的)。
    事务隔离级别解决的是多个事务同时调⽤⼀个数据库的问题,如下图所示:
    在这里插入图片描述
    ⽽事务传播机制解决的是⼀个事务在多个节点(⽅法)中传递的问题,如下图所示:
    在这里插入图片描述

    4.3 事务传播机制有哪些?

    Spring 事务的传播机制包含以下 7 种 :

    1. Propagation.REQUIRED默认的事务传播级别, 它表示如果当前存在事务, 则加入该事务; 如果当前没有事务, 则创建一个新的事务。
    2. Propagation.SUPPORTS: 如果当前存在事务,则加入该事务; 如果当前没有事务, 则以非事务的方式继续运行。
    3. Propagation.MANDATORY: 如果当前存在事务, 则加入该事务; 如果当前没有事务, 则抛出异常。
    4. Propagation.REQUIRES_NEW: 表示创建一个新的事务, 如果当前存在事务, 则把当前事务挂起。
    5. Propagation.NOT_SUPPORTED: 以非事务方式运行, 如果当前存在事务, 则把当前事务挂起。
    6. Propagation.NEVER:以非事务方式运行, 如果当前存在事务, 则抛出异常。
    7. Propagation.NESTED: 如果当前存在事务, 则创建一个事务作为当前事务的嵌套事务来运行; 如果不存在事务,则创建一个新的事务。

    以上 7 种传播⾏为,可以根据是否⽀持当前事务分为以下 3 类:
    在这里插入图片描述
    以情侣关系为例来理解以上分类:
    在这里插入图片描述

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

    4.4.1 支持当前事务(REQUIRED)

    以下代码实现中,先开启事务先成功插⼊⼀条⽤户数据,然后再执⾏⽇志报错,⽽在⽇志报错时发⽣了异常,观察 propagation = Propagation.REQUIRED 的执⾏结果。
    LogService

    package com.example.demo.service;
    
    import com.example.demo.mapper.LogMapper;
    import com.example.demo.model.LogInfo;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Propagation;
    import org.springframework.transaction.annotation.Transactional;
    
    import javax.annotation.Resource;
    
    /**
     * @date 2022/8/17 23:40
     */
    @Service
    public class LogService {
        @Resource
        private LogMapper logMapper;
        @Transactional(propagation = Propagation.REQUIRED)
        public int add(LogInfo logInfo){
            int n=10/0;  // 异常
            return logMapper.add(logInfo);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    UserController
    在这里插入图片描述
    在这里插入图片描述
    程序运行前的数据库数据如下:
    在这里插入图片描述
    测试结果如下:
    在这里插入图片描述
    在这里插入图片描述
    虽然日志中显示添加用户成功了,但是整体都进行了回滚:
    在这里插入图片描述

    4.4.2 不⽀持当前事务(REQUIRES_NEW)

    UserController 类中的代码不变,将添加⽤户和添加⽇志的⽅法修改为 REQUIRES_NEW 不⽀持当前事务,重新创建事务,观察执⾏结果:
    在这里插入图片描述
    测试:
    在这里插入图片描述
    在这里插入图片描述
    程序执⾏结果:userinfo 表中成功插⼊了数据,loginfo 表执⾏失败,但没影响 UserController 中的事务。
    在这里插入图片描述

    4.4.3 不⽀持当前事务(NOT_SUPPORTED)

    在这里插入图片描述
    测试:
    在这里插入图片描述
    在这里插入图片描述
    程序执⾏结果:全部添加成功
    在这里插入图片描述

    4.4.4 嵌套事务(NESTED)

    方法调用流程:controller/add -> 用户添加方法 -> 日志添加方法
    在这里插入图片描述
    测试:
    在这里插入图片描述
    在这里插入图片描述
    当日志添加方法出现异常之后,嵌套事务的执行结果是:

    1. 用户添加不受影响,添加用户成功
    2. 日志添加失败,因为发生异常回滚了事务
      在这里插入图片描述

    4.2.5 加入事务 (REQUIRED)

    方法流程:controller/add -> 用户添加方法 -> 日志添加方法
    在这里插入图片描述
    测试:
    在这里插入图片描述
    在这里插入图片描述
    当日志添加方法出现异常之后,加入事务的执行结果是:

    1. 用户添加成功的数据也回滚了
    2. 日志添加数据也回滚了
      在这里插入图片描述
  • 相关阅读:
    15Spring Boot整合MyBatis
    【老生谈算法】matlab实现fsk-ask算法源码——fsk-ask算法
    MBSE之简单介绍
    ASP.NET多媒体设备管理系统VS开发sqlserver数据库web结构c#编程计算机网页目
    水环保网关在湿地保护有什么作用?
    深度学习 Transformer架构解析
    Unity在安卓Build时报错解决:CommandInvokationFailure和编译器 (1.8.0-adoptopenjdk) 中出现异常错误
    11.盛最多水的容器 C++
    Flutter: ListView or Column
    Wireshark TS | 消失的 TCP DUP ACK
  • 原文地址:https://blog.csdn.net/m0_46468731/article/details/126376375