Spring事务和事务传播机制

文章目录

  • [1. 事务回顾](#1. 事务回顾)
    • [1.1 什么是事务?](#1.1 什么是事务?)
    • [1.2 为什么需要事务?](#1.2 为什么需要事务?)
    • [1.3 事务的操作](#1.3 事务的操作)
  • [2. Spring 中事务的实现](#2. Spring 中事务的实现)
    • [2.1 Spring 编程式事务(了解)](#2.1 Spring 编程式事务(了解))
    • [2.2 Spring 声明式事务 @Transactional](#2.2 Spring 声明式事务 @Transactional)
      • [@Transactional 作用](#@Transactional 作用)
  • [3. @Transactional 详解](#3. @Transactional 详解)
    • [3.1 rollbackFor](#3.1 rollbackFor)
    • [3.2 事务隔离级别](#3.2 事务隔离级别)
      • [3.2.1 MySQL 事务隔离级别(回顾)](#3.2.1 MySQL 事务隔离级别(回顾))
      • [3.2.2 Spring 事务隔离级别](#3.2.2 Spring 事务隔离级别)
    • [3.3 Spring 事务传播机制](#3.3 Spring 事务传播机制)
      • [3.3.1 什么是事务传播机制](#3.3.1 什么是事务传播机制)
      • [3.3.2 事务的传播机制有哪些](#3.3.2 事务的传播机制有哪些)

1. 事务回顾

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

1.1 什么是事务?

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

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

1.2 为什么需要事务?

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

比如转账操作:

第一步: A 账户 -100 元.

第二步: B 账户 +100 元.

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

1.3 事务的操作

事务的操作主要有三步:

  1. 开启事start transaction/ begin (一组操作前开启事务)
  2. 提交事务: commit (这组操作全部成功,提交事务)
  3. 回滚事务: rollback (这组操作中间任何一个操作出现异常,回滚事务)
sql 复制代码
-- 开启事务
start transaction;

-- 提交事务
commit;

-- 回滚事务
rollback;

2. Spring 中事务的实现

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

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

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

需求: 用户注册,注册时在日志表中插入一条操作记录.

数据准备:

java 复制代码
-- 创建数据库
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';

配置文件

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 个重要操作步骤:

  • 开启事务(获取事务)
  • 提交事务
  • 回滚事务

SpringBoot 内置了两个对象:

  1. DataSourceTransactionManager 事务管理器.用来获取事务(开启事务),提交或回滚事务的
  2. TransactionDefinition 是事务的属性,在获取事务的时候需要将 TransactionDefinition 传递进去从而获得一个事务 TransactionStatus
java 复制代码
@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 "注册成功";
    }
}
  • 观察事务提交

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

观察事务回滚

复制代码
//回滚事务
dataSourceTransactionManager.rollback(transactionStatus);

运行程序:

观察数据库,虽然程序返回"注册成功",但数据库并没有新增数据.

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

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

2.2 Spring 声明式事务 @Transactional

声明式事务的实现很简单

两步操作:

  1. 添加依赖

    <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();
        }
        return "注册成功";
    }

运行程序,发现虽然程序出错了,但是由于异常被捕获了,所以事务依然得到了提交,如果需要事务进行回滚,有以下两种方式:

    1. 重新抛异常
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();
        }
        return "注册成功";
    }
    1. 手动回滚事务

使用 TransactionAspectSupport.currentTransactionStatus() 得到当前的事务,并使用 setRollbackOnly 设置 setRollbackOnly

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();
            //手动回滚事务
            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 复制代码
@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 这个属性指定出现何种异常类型时事务进行回滚.

运行程序报错


事务回滚了

结论:

  • 在Spring的事务管理中,默认只在遇到运行时异常RuntimeException和Error时才会回滚.
  • 如果需要回滚指定类型的异常,可以通过rollbackFor属性来指定.

3.2 事务隔离级别

3.2.1 MySQL 事务隔离级别(回顾)

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

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

3.2.2 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 属性进行设置

3.3 Spring 事务传播机制

3.3.1 什么是事务传播机制

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

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

A方法运行时,会开启一个事务.当A调用B时, B方法本身也有事务,此时B方法运行时,是加入A的事务,还是创建一个新的事务呢?

这个就涉及到了事务的传播机制.

比如公司流程管理

执行任务之前,需要先写执行文档,任务执行结束,再写总结汇报

此时A部门有一项工作,需要B部门的支援,此时B部门是直接使用A部门的文档,还是新建一个文档呢?

事务隔离级别解决的是多个事务同时调用一个数据库的问题

而事务传播机制解决的是一个事务在多个节点(方法)中传递的问题

3.3.2 事务的传播机制有哪些

@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.

比如一对新人要结婚了,关于是否需要房子

  1. Propagation.REQUIRED : 需要有房子.如果你有房,我们就一起住,如果你没房,我们就一起买房.(如果当前存在事务,则加入该事务.如果当前没有事务,则创建一个新的事务)
  2. Propagation.SUPPORTS :可以有房子.如果你有房,那就一起住.如果没房,那就租房.(如果当前存在事务,则加入该事务.如果当前没有事务,则以非事务的方式继续运行)
  3. Propagation.MANDATORY :必须有房子.要求必须有房,如果没房就不结婚.(如果当前存在事务,则加入该事务.如果当前没有事务,则抛出异常)
  4. Propagation.REQUIRES_NEW :必须买新房.不管你有没有房,必须要两个人一起买房.即使有房也不住.(创建一个新的事务.如果当前存在事务,则把当前事务挂起)
  5. Propagation.NOT_SUPPORTED :不需要房.不管你有没有房,我都不住,必须租房.(以非事务方式运行,如果当前存在事务,则把当前事务挂起)
  6. Propagation.NEVER :不能有房子.(以非事务方式运行,如果当前存在事务,则抛出异常)
  7. Propagation.NESTED :如果你没房,就一起买房.如果你有房,我们就以房子为根据地,做点下生意.(如果如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行.如果当前没有事务,则该取值等价于 PROPAGATION_REQUIRED)

总结

  1. Spring中使用事务,有两种方式:编程式事务(手动操作)和声明式事务.其中声明式事务使用较多,在方法上添加 @Transactional 就可以实现了
  2. 通过 @Transactional(isolation = Isolation.SERIALIZABLE) 设置事务的隔离级别. Spring 中的事务隔离级别有 5 种
  3. 通过 @Transactional(propagation = Propagation.REQUIRED) 设置事务的传播机制, Spring 中的事务传播级别有 7 种, 重点关注 REQUIRED (默认值) 和 REQUIRES_NEW
相关推荐
论迹2 小时前
【JavaEE】-- Spring Web MVC入门
前端·spring·java-ee
A***27952 小时前
后端服务限流配置,Spring Cloud Gateway
java·运维·数据库
222you2 小时前
SpringBoot对SpringMVC的整合
java·spring boot·后端
Dxy12393102162 小时前
MySQL如何修改最大连接数
数据库·mysql
UCoding2 小时前
我们来学mysql -- delete undo log的形成
数据库·mysql·delete的undo log
dblens 数据库管理和开发工具2 小时前
PostgreSQL物化视图详解:用空间换时间的性能优化利器
数据库·postgresql·性能优化
TDengine (老段)2 小时前
TDengine 字符串函数 REGEXP_IN_SET 用户手册
数据库·物联网·mysql·时序数据库·tdengine·涛思数据
珹洺2 小时前
Java-Spring入门指南(三十二)Android SQLite数据库实战
java·数据库·spring
刘一说2 小时前
深入理解 Spring Boot 高级特性:条件化 Bean 注册机制
java·spring boot·后端