SpringBoot 事务

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


为什么需要事务?

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

比如转账操作:

第一步:A 账户 -100 元.

第二步:B 账户 +100 元.

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

比如秒杀系统,

第一步: 下单成功

第二步: 扣减库存

下单成功后, 库存也需要同步减少. 如果下单成功, 库存扣减失败, 那么就会造成下单超出的情况. 所以就需要把这两步操作放在同一个事务中. 要么一起成功, 要么一起失败.

理解事务概念为主, 实际企业开发时, 并不是简单的通过事务来处理.

事务的操作

事务的操作主要有三步:

  1. 开启事start transaction/ begin (一组操作前开启事务)

  2. 提交事务: commit (这组操作全部成功, 提交事务)

  3. 回滚事务: rollback (这组操作中间任何一个操作出现异常, 回滚事务)

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

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://localhost:3306/trans_test?characterEncoding=utf8&useSSL=false
    username: root
    password: 123
    driver-class-name: com.mysql.cj.jdbc.Driver
  # 设置动态代理的方式 true jdk代理, false cglib代理
  aop:
    proxy-target-class: true
mybatis:
  configuration:
    map-underscore-to-camel-case: true #配置驼峰自动转换
#    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #打印sql语句
  mapper-locations: classpath:mapper/**Mapper.xml
# 设置日志文件的文件名
logging:
  file:
    name: logger/spring-book.log

实体类

复制代码
@Data
public class LogInfo {
    private Integer id;
    private String userName;
    private String op;
    private Date createTime;
    private Date updateTime;
}

@Data
public class UserInfo {
    private Integer id;
    private String userName;
    private String password;
    private Date createTime;
    private Date updateTime;
}

mapper

复制代码
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserInfoMapper {
    @Insert("insert into user_info(`user_name`,`password`)values(#{name},#
    {password})")
    Integer insert(String name,String password);
}

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface LogInfoMapper {
    @Insert("insert into log_info(`user_name`,`op`)values(#{name},#{op})")
    Integer insertLog(String name,String op);
}

service

复制代码
@Slf4j
@Service
public class UserService {
    @Autowired
    private UserInfoMapper userInfoMapper;
    public void registryUser(String name,String password){
//插入用户信息
        userInfoMapper.insert(name,password);
    }
}

@Slf4j
@Service
public class LogService {
    @Autowired
    private LogInfoMapper logInfoMapper;
    public void insertLog(String name,String op){
        //记录用户操作
        logInfoMapper.insertLog(name,"用户注册");
    }
}

controller

复制代码
@RequestMapping("/user")
@RestController
public class UserController {
    @Autowired
    private UserService userService;
    @RequestMapping("/registry")
    public String registry(String name,String password){
        //用户注册
        userService.registryUser(name,password);
        return "注册成功";
    }
}

Spring编程式事务

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

• 开启事务(获取事务)

• 提交事务

• 回滚事务

SpringBoot 内置了两个对象:

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

事务交提

复制代码
@RequestMapping("/user")
@RestController
public class UserController {

    @Autowired
    private DataSourceTransactionManager dataSourceTransactionManager;

    @Autowired
    private TransactionDefinition transactionDefinition;

    @Autowired
    private UserService userService;
    @RequestMapping("/registry")
    public String registry(String name,String password){

        TransactionStatus transaction = dataSourceTransactionManager.getTransaction(transactionDefinition);
        //用户注册
        userService.registryUser(name,password);
        // 提交事务
        dataSourceTransactionManager.commit(transaction);

        //回滚事务
        //dataSourceTransactionManager.rollback(transactionStatus);
        return "注册成功";
    }
}

http://localhost:8080/user/registry?name=user1&password=123123


事务回滚

复制代码
@RequestMapping("/user")
@RestController
public class UserController {

    @Autowired
    private DataSourceTransactionManager dataSourceTransactionManager;

    @Autowired
    private TransactionDefinition transactionDefinition;

    @Autowired
    private UserService userService;
    @RequestMapping("/registry")
    public String registry(String name,String password){

        TransactionStatus transaction = dataSourceTransactionManager.getTransaction(transactionDefinition);
        //用户注册
        userService.registryUser(name,password);
        //回滚事务
        dataSourceTransactionManager.rollback(transactionStatus);
        return "注册成功";
    }
}

http://localhost:8080/user/registry?name=user2&password=123123

虽然返回结果时注册成功,但是数据库中并没有插入数据。

Spring声明式事务@Transactional

声明式事务的实现很简单, 只需要在需要事务的方法上添加 @Transactional 注解就可以实现了.无需手动开启事务和提交事务, 进入方法时自动开启事务, 方法执行完会自动提交事务, 如果中途发生了没有处理的异常会自动回滚事务.

复制代码
@RequestMapping("/user")
@RestController
public class UserController {

    @Autowired
    private UserService userService;
    @Transactional
    @RequestMapping("/registry")
    public String registry(String name,String password){
        userService.registryUser(name,password);
        return "注册成功";
    }
}

http://localhost:8080/user/registry?name=user2&password=123123

数据库中插入了数据。


使程序出现异常

复制代码
@RequestMapping("/user")
@RestController
public class UserController {
    @Autowired
    private UserService userService;
    @Transactional
    @RequestMapping("/registry")
    public String registry(String name,String password){
        userService.registryUser(name,password);
        int a = 10 / 0;
        return "注册成功";
    }
}

事务会进行回滚,所以数据库中没有新插入的数据。

我们一般会在业务逻辑层当中来控制事务, 因为在业务逻辑层当中, 一个业务功能可能会包含多个数据访问的操作. 在业务逻辑层来控制事务, 我们就可以将多个数据访问操作控制在一个事务范围内.

Transactional作用

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

• 修饰方法时: 只有修饰public 方法时才生效(修饰其他方法时不会报错, 也不生效)[推荐]

• 修饰类时: 对@Transactional 修饰的类中所有的 public 方法都生效.

方法/类被 @Transactional 注解修饰时, 在目标方法执行开始之前, 会自动开启事务, 方法执行结之后, 自动提交事务.如果在方法执行过程中, 出现异常, 且异常未被捕获, 就进行事务回滚操作.如果异常被程序捕获, 方法就被认为是成功执行, 依然会提交事务.

复制代码
@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. 重新抛出异常

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

  2. 手动回滚事务

    // 使用 TransactionAspectSupport.currentTransactionStatus() 得到当前的事务, 并
    // 使用 setRollbackOnly 设置setRollbackOnly
    @Transactional
    @RequestMapping("/registry")
    public String registry(String name,String password){
    //用户注册
    userService.registryUser(name,password);
    log.info("用户数据插入成功");
    //对异常进行捕获
    try {
    //强制程序抛出异常
    int a = 10/0;
    }catch (Exception e){
    // 手动回滚事务
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
    return "注册成功";
    }

Transactional详解

通过上面的代码, 我们学习了@Transactional 的基本使用. 接下来我们学习@Transactional注解的使用细节.

我们主要学习@Transactional 注解当中的三个常见属性:

  1. rollbackFor: 异常回滚属性. 指定能够触发事务回滚的异常类型. 可以指定多个异常类型
  2. Isolation: 事务的隔离级别. 默认值为Isolation.DEFAULT
  3. propagation: 事务的传播机制. 默认值为 Propagation.REQUIRED

rollbackFor

@Transactional 默认只在遇到运行时异常和Error时才会回滚, 非运行时异常不回滚. 即Exception的子类中, 除了RuntimeException及其子类.

修改一下代码

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

复制代码
@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";
}

数据回滚了。

在Spring的事务管理中,默认只在遇到运行时异常RuntimeException和Error时才会回滚.

如果需要回滚指定类型的异常, 可以通过rollbackFor属性来指定.

事务隔离级别

MySQL事务隔离级别

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

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

    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)
@RequestMapping("/r3")
public String r3(String name,String password) throws IOException {
    //... 代码省略
    return "r3";
}

Spring事务传播机制

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

比如有两个方法A, B都被@Transactional 修饰, A方法调用B方法,A方法运行时, 会开启一个事务. 当A调用B时, B方法本身也有事务, 此时B方法运行时, 是加入A的事务, 还是创建一个新的事务呢?

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

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

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

事务传播机制有哪些

@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. Spring中使用事务, 有两种方式: 编程式事务(手动操作)和声明式事务. 其中声明式事务使用较多,在方法上添加 @Transactional 就可以实现了
  2. 通过 @Transactional(isolation = Isolation.SERIALIZABLE) 设置事务的隔离级别. Spring 中的事务隔离级别有 5 种
  3. 通过 @Transactional(propagation = Propagation.REQUIRED) 设置事务的传播机制, Spring 中的事务传播级别有 7 种, 重点关注 REQUIRED (默认值) 和 REQUIRES_NEW
相关推荐
a未来永远是个未知数2 分钟前
redis数据迁移之通过redis-dump镜像
数据库·redis·缓存
努力搬砖的咸鱼6 分钟前
QTSql全解析:从连接到查询的数据库集成指南
数据库·qt
创码小奇客8 分钟前
Spring Boot 中分布式事务的奇幻漂流
java·spring boot·trae
我是大头鸟16 分钟前
ecplise 工具 没有Java EE Tools 选项
java·java-ee
神洛华41 分钟前
PowerBI 之DAX 1:DAX概述、逻辑、筛选函数
数据库·redis·powerbi
IDRSolutions_CN42 分钟前
开发PDF时,如何比较 PDF 文件
java·经验分享·pdf·软件工程·团队开发
程序员JerrySUN1 小时前
驱动开发硬核特训 · Day 1
java·linux·运维·开发语言·c++·驱动开发
郑州吴彦祖7721 小时前
UTF-8和GBK编码的区别和详细解释
java·utf-8
梦三辰1 小时前
超详细解读:数据库MVCC机制
数据库·mysql·mvcc·快照
盖世英雄酱581361 小时前
为什么类型转换,为导致索引失效
java·后端