目录

Spring事务和事务传播机制

Spring事务和事务传播机制


1.事务

1.1 什么是事务?

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

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

1.2 为什么需要事务?

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

比如转账操作:

第一步:A账户-100元.

第二步:B账户+100元.

如果没有事务,第一步执行成功了,第二步执行失败了,那么A账户的100元就平白无故消失了.如果使

用事务就可以解决这个问题,让这一组操作要么一起成功,要么一起失败.

1.3 事物的操作

事务的操作主要有三步:

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

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';

配置文件:

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/trans_test?characterEncoding=utf8&useSSL=false
    username: root
    password: jqka
    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 #配置驼峰自动转换

实体类:

package com.example.trans.model;

import lombok.Data;

import java.time.LocalDateTime;
@Data
public class UserInfo {
    private Integer id;
    private String username;
    private String password;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}
package com.example.trans.model;

import lombok.Data;

import java.time.LocalDateTime;

@Data
public class LogInfo {
    private Integer id;
    private String username;
    private String op;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

2.1 Spring编程式事务

Spring手动操作事务和上面MySQL操作事务类似,有3个重要操作步骤:

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

SpringBoot内置了两个对象:

  1. DataSourceTransactionManager 事务管理器.用来获取事务(开启事务)提交或回滚事务
  2. TransactionDefinition是事务的属性,在获取事务的时候需要将TransactionDefinition传递进去从而获得一个事务TransactionStatus

提交事务和回滚事务只能选择一个

@RestController
@RequestMapping("/user")
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 "注册成功";
    }
}

https://i-blog.csdnimg.cn/direct/d05e7edfa9ba4e19b4a85b99938ce61a.png

2.2 Spring声明式事务@Transactional

  1. 添加依赖

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-tx</artifactId>
    </dependency>
  2. 在需要事务的方法上添加@Transactional注解就可以实现了.无需手动开启事务和提交事务,进入方法时自动开启事务,方法执行完会自动提交事务,如果中途发生了没有处理的异常会自动回滚事务.

    @RestController
    @RequestMapping("/trans")
    public class TransactionalController {
        @Autowired
        private UserService userService;
    
        @Transactional
        @RequestMapping("/registry")
        public String registry(String name,String password) {
            //用户注册
            userService.registryUser(name,password);
            return "注册成功";
    
        }
    }
    @Slf4j
    @RestController
    @RequestMapping("/trans")
    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 "注册成功";
    
        }
    }

    发现虽然日志显示数据插入成功,但数据库却没有新增数据,事务进行了回滚.https://i-blog.csdnimg.cn/direct/1d75e4ccac7d431989fd3d380973c459.png

@Transactional 作用

@Transactional可以用来修饰方法或类:

  • 修饰方法时:只有修饰public方法时才生效(修饰其他方法时不会报错,也不生效)[推荐]
  • 修饰类时:对@Transactional修饰的类中所有的 public方法都生效.

方法/类被@Transactional注解修饰时,在目标方法执行开始之前,会自动开启事务,方法执行结束之后,自动提交事务.

如果在方法执行过程中,出现异常,且异常未被捕获,就进行事务回滚操作。

如果异常被程序捕获,方法就被认为是成功执行,依然会提交事务.

try {
    int a = 10 / 0;
} catch (Exception e) {
    e.printStackTrace();
}

运行程序,发现虽然程序出错了,但是由于异常被捕获了,所以事务依然得到了提交。

如果需要事务进行回滚,有以下两种方式:

  1. 抛出异常

    try {
        int a = 10 / 0;
    } catch (Exception e) {
        throw e;
    }
  2. 手动回滚事务
    使用TransactionAspectSupport.currentTransactionStatus()得到当前的事务,并使用setRollbackOnly设置setRollbackOnly

    try {
        int a = 10/0;
    }catch (Exception e){
        log.error("发生异常");
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }

3.@Transactional详解

@Transactional注解当中的三个常见属性:

rollbackFor:异常回滚属性.指定能够触发事务回滚的异常类型.可以指定多个异常类型

Isolation:事务的隔离级别.默认值为Isolation.DEFAULT

propagation:事务的传播机制. 默认值为Propagation.REQUIRED

3.1 rollbackFor

Spring 的@Transactional 注解,默认仅在抛出 RuntimeException(运行时异常)及其子类,或 Error 时才会触发事务回滚;普通的Exception(受检异常)不会触发回滚

https://i-blog.csdnimg.cn/direct/d67c940338d74d81b8406fd5a89576e5.png

try {
    int a = 10 / 0;
} catch (Exception e) {
    //抛出异常
    throw new IOException();
}

此时虽然程序抛出了异常,但是事务依然进行了提交

如果我们需要所有异常都回滚,需要来配置@Transactional注解当中的rollbackFor属性,通过rollbackFor这个属性指定出现何种异常类型时事务进行回滚.

@Transactional(rollbackFor = Exception.class)
@RequestMapping("/r2")
public String r2(String name,String password) throws IOException {
    //⽤⼾注册 
    userService.registryUser(name,password);
    log.info("用户数据插入成功");
    if (true){
        throw new IOException();
    }
    return "r2";
}

加了rollbackFor属性设置了任何事务都会回滚,此时事务就不会进行提交

3.2 事务隔离级别

3.2.1 MySQL事务隔离级别

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

  • 读未提交(READUNCOMMITTED):读未提交,也叫未提交读.该隔离级别的事务可以看到其他事务中未提交的数据。

因为其他事务未提交的数据可能会发生回滚,但是该隔离级别却可以读到,我们把该级别读到的数据称之为脏数据,这个问题称之为脏读。

  • 读提交(READCOMMITTED):读已提交,也叫提交读.该隔离级别的事务能读取到已经提交事务的数据

该隔离级别不会有脏读的问题.但由于在事务的执行中可以读取到其他事务提交的结果,所以在不同时间的相同SQL查询可能会得到不同的结果,这种现象叫做不可重复读

  • 可重复读(REPEATABLEREAD):事务不会读到其他事务对已有数据的修改,即使其他事务已提交.也就可以确保同一事务多次查询的结果一致,但是其他事务新插入的数据,是可以感知到的.这也就引发了幻读问题.可重复读,是MySQL的默认事务隔离级别。

比如此级别的事务正在执行时,另一个事务成功的插入了某条数据,但因为它每次查询的结果都是一样的,所以会导致查询不到这条数据,自己重复插入时又失败(因为唯一约束的原因),明明在事务中查询不到这条信息,但自己就是插入不进去,这个现象叫幻读。

  • 串行化(SERIALIZABLE):序列化,事务最高隔离级别.它会强制事务排序,使之不会发生冲突,从而解决了脏读,不可重复读和幻读问题,但因为执行效率低,所以真正使用的场景并不多

https://i-blog.csdnimg.cn/direct/1250dcbb5a194707a82c089ba8cb9297.png

3.2.2 Spring事务隔离级别

  1. Isolation.DEFAULT:以连接的数据库的事务隔离级别为主.
  2. Isolation.READ_UNCOMMITTED:读未提交,对应SQL标准中READUNCOMMITTED
  3. Isolation.READ_COMMITTED:读已提交,对应SQL标准中READCOMMITTED
  4. Isolation.REPEATABLE_READ:可重复读,对应SQL标准中REPEATABLE READ
  5. Isolation.SERIALIZABLE:串行化,对应SQL标准中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属性进行设置

@Transactional(isolation = Isolation.READ_COMMITTED)

3.3 Spring事务传播机制

3.3.1 什么是事务传播机制

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

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

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

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

比如公司流程管理 执行任务之前,需要先写执行文档,任务执行结束,再写总结汇报

https://i-blog.csdnimg.cn/direct/9abc92c28add44e08ba84726c7907bd6.png

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

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

https://i-blog.csdnimg.cn/direct/a93b1ff0add642f195945c5718aa5642.png

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

https://i-blog.csdnimg.cn/direct/5f4626d99d8d4749b41403c43b2c2221.png

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

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

重点学习:

  1. REQUIRED(默认值)
  2. REQUIRES_NEW
3.3.3.1 REQUIRED(加入事务)

代码实现:

  1. 用户注册,插入一条数据
  2. 记录操作日志,插入一条数据(出现异常)

观察propagation = Propagation.REQUIRED的执行结果

@RestController
@RequestMapping("/propaga")
public class PropagationController {
    @Autowired
    private UserService userService;
    @Autowired
    private LogService logService;

    @Transactional(propagation = Propagation.REQUIRED)
    @RequestMapping("/p1")
    public String p1(String name, String password) {
        //用户注册
        userService.registryUser(name, password);
        //记录操作日志
        logService.insertLog(name,"用户注册");
        return "p1";
    }
}
@Service
public class UserService {
    @Autowired
    private UserInfoMapper userInfoMapper;
    @Transactional(propagation = Propagation.REQUIRED)
    public void registryUser(String name, String password){
        //插入用户信息
        userInfoMapper.insert(name, password);
    }
}



@Service
public class LogService {
    @Autowired
    private LogInfoMapper logInfoMapper;
    @Transactional(propagation = Propagation.REQUIRED)
    public void insertLog(String name,String op){
        //记录用户操作
        int a = 10 / 0;
        logInfoMapper.insertLog(name,"用户注册");
    }

}

流程描述:

  1. p1方法开始事务
  2. 用户注册,插入一条数据(执行成功)(和p1使用同一个事务)
  3. 记录操作日志,插入一条数据(出现异常,执行失败)(和p1使用同一个事务)
  4. 因为步骤3出现异常,事务回滚.步骤2和3使用同一个事务,所以步骤2的数据也回滚了
3.3.3.2 REQUIRES_NEW(新建事务)

将上述UserService和LogService中相关方法事务传播机制改为Propagation.REQUIRES_NEW

@Service
public class UserService {
    @Autowired
    private UserInfoMapper userInfoMapper;
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void registryUser(String name, String password){
        //插入用户信息
        userInfoMapper.insert(name, password);
    }
}

@Service
public class LogService {
    @Autowired
    private LogInfoMapper logInfoMapper;
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void insertLog(String name,String op){
        //记录用户操作
        int a = 10 / 0;
        logInfoMapper.insertLog(name,"用户注册");
    }

}

运行程序,发现用户数据插入成功了,日志表数据插入失败。

LogService方法中的事务不影响UserService中的事务.

当我们不希望事务之间相互影响时,可以使用该传播行为。

3.3.3.3 NEVER(不支持当前事务,抛异常)

修改UserService中对应方法的事务传播机制为

@Service
public class UserService {
    @Autowired
    private UserInfoMapper userInfoMapper;
    @Transactional(propagation = Propagation.NEVER)
    public void registryUser(String name, String password){
        //插入用户信息
        userInfoMapper.insert(name, password);
    }
}

程序执行报错,没有数据插入.

在标记为“从不”传播的事务中发现了现有事务 。https://i-blog.csdnimg.cn/direct/5e69e78d21884c96b3059f691334b48f.png

3.3.3.4 NESTED(嵌套事务)

将上述UserService和LogService中相关方法事务传播机制改为Propagation.NESTED

@Service
public class UserService {
    @Autowired
    private UserInfoMapper userInfoMapper;
    @Transactional(propagation = Propagation.NESTED)
    public void registryUser(String name, String password){
        //插入用户信息
        userInfoMapper.insert(name, password);
    }
}

@Service
public class LogService {
    @Autowired
    private LogInfoMapper logInfoMapper;
    @Transactional(propagation = Propagation.NESTED)
    public void insertLog(String name,String op){
        //记录用户操作
        int a = 10 / 0;
        logInfoMapper.insertLog(name,"用户注册");
    }

}

运行程序,发现没有任何数据插入。

流程描述:

  1. Controller 中p1方法开始事务
  2. UserService用户注册,插入一条数据(嵌套p1事务)
  3. LogService记录操作日志,插入一条数据(出现异常,执行失败)(嵌套p1事务,回滚当前事务,数据添加失败)
  4. 由于是嵌套事务,LogService出现异常之后,往上找调用它的方法和事务,所以用户注册也失败了.
  5. 最终结果是两个数据都没有添加

p1事务可以认为是父事务,嵌套事务是子事务.父事务出现异常,子事务也会回滚,子事务出现异常,如果不进行处理,也会导致父事务回滚。