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
相关推荐
自燃人~6 小时前
怎么优化慢SQL
数据库·sql
IT_陈寒6 小时前
React 18实战:这5个新特性让我的开发效率提升了40%
前端·人工智能·后端
爱学java的ptt6 小时前
mysql的存储引擎
数据库·mysql
小宇的天下6 小时前
innovus Flip chip 产品设计方法(3)
数据库·windows·microsoft
a努力。6 小时前
京东Java面试被问:双亲委派模型被破坏的场景和原理
java·开发语言·后端·python·面试·linq
不如打代码KK6 小时前
Springboot如何解决跨域问题?
java·spring boot·后端
蓝程序6 小时前
Spring AI学习 程序接入大模型
java·人工智能·spring
GalenZhang8886 小时前
使用 Python SDK 将数据写入飞书多维表格
数据库·python·飞书·多维表格
云和数据.ChenGuang6 小时前
GaussDB 期末考试题与面试题
数据库·opengauss·gaussdb·数据库期末试题
不屈的铝合金6 小时前
SQL 语言概述与数据库核心前置配置了解
数据库·sql·mysql·约束·sql 语句分类·字符集配置·校对规则