16. Spring 事务和事务传播机制

发布于:2024-04-28 ⋅ 阅读:(20) ⋅ 点赞:(0)

源码位置:transaction

1. 事务回顾

在数据库阶段,想必大家都已经学习过事务了。当多个操作要么一起成功,要么一起失败的时候就需要将多个操作放在同一个事务中。

举个例子:比如用户A给用户B转账100元的业务,需要把用户A的余额-100,并且用户B的余额+100,这两个操作对应着数据库的两条SQL语句,两条SQL语句可以放入事务中,要么一起成功(提交)一起失败(回滚)。

MySQL中的事务无非围绕着这三个SQL语句:

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

在数据库阶段主要以理解事务的概念为主,然而在实际的开发中,并不是简单的通过事务来处理,因此我们要学习Spring中的事务操作。

2. Spring中事务的实现

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

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

2.1 前置操作

在学习事务之前,先准备需要使用的库和表,SQL脚本如下:

drop database if exists trans;
create database trans default character set utf8mb4;
use trans;

drop table if exists userinfo;
create table userinfo (
    `id` int not null auto_increment,
    `username` varchar(20) not null,
    `password` varchar(30) 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 = '用户表';

通过普通的MyBatis插入几条数据:

实体类 UserInfo:

@Data
public class UserInfo {

    private Integer id;
    private String username;
    private String password;
    private Timestamp createTime;
    private Timestamp updateTime;
}

控制层 UserController:

@RequestMapping("/user")
@RestController
@Slf4j
public class UserController {
    @Autowired
    UserService userService;

    @RequestMapping("/registry")
    public String registry(UserInfo userInfo) {
        Integer result = userService.insertUser(userInfo);
        log.info("插入了" + result + "条数据~");
        return "注册成功";
    }
}

服务层 UserService:

@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;

    public Integer insertUser(UserInfo userInfo) {
        return userMapper.insertUser(userInfo);
    }
}

mapper层 UserMapper:

@Mapper
public interface UserMapper {
    Integer insertUser(UserInfo userInfo);
}

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.chenshu.transaction.mapper.UserMapper">

    <insert id="insertUser">
        insert into userinfo
                        <trim prefix="(" suffix=")" suffixOverrides=",">
                            <if test="username != null">
                                username,
                            </if>
                            <if test="password != null">
                                password,
                            </if>
                            <if test="createTime != null">
                                creat_time,
                            </if>
                            <if test="updateTime != null">
                                update_time,
                            </if>
                        </trim>
        values
            <trim prefix="(" suffix=")" suffixOverrides=",">
                <if test="username != null">
                    #{username},
                </if>
                <if test="password != null">
                    #{password},
                </if>
                <if test="createTime != null">
                    #{createTime},
                </if>
                <if test="updateTime != null">
                    #{updateTime},
                </if>
            </trim>
    </insert>
</mapper>

通过url插入几条数据:

image.png

通过几次url访问,成功插入以下数据:

image.png

2.1 编程式事务(了解)

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

  • 获取事务状态
  • 回滚事务
  • 提交事务

2.1.1 获取事务状态

编程式事务有两个重要的类:

  • 事务管理器:org.springframework.jdbc.datasource.DataSourceTransactionManager
  • 事务属性的接口:org.springframework.transaction.TransactionDefinition

在Controller层依赖注入这两个类:

public class UserController {
    @Autowired
    private DataSourceTransactionManager dataSourceTransactionManager;

    @Autowired
    private TransactionDefinition transactionDefinition;

通过事务管理器DataSourceTransactionManagergetTransaction()方法,在方法里传入TransactionDefinition可以拿到一个TransactionStatus代表当前事务状态的类。

TransactionStatus transaction = dataSourceTransactionManager.getTransaction(transactionDefinition);

通过TransactionStatus可以对事务进行回滚 or 提交

2.1.2 事务回滚

@RequestMapping("/registry")
public String registry(UserInfo userInfo) {
    //获取事务状态
    TransactionStatus transaction = dataSourceTransactionManager.getTransaction(transactionDefinition);

    Integer result = userService.insertUser(userInfo);
    log.info("插入了" + result + "条数据~");
    //回滚事务
    dataSourceTransactionManager.rollback(transaction);

    return "注册成功";
}

再次通过url调用该方法:

image.png

由于事务回滚,表里没有插入数据:

image.png

2.1.3 事务提交

@RequestMapping("/registry")
public String registry(UserInfo userInfo) {
    //获取事务状态
    TransactionStatus transaction = dataSourceTransactionManager.getTransaction(transactionDefinition);

    Integer result = userService.insertUser(userInfo);
    log.info("插入了" + result + "条数据~");
    //提交事务
    dataSourceTransactionManager.commit(transaction);

    return "注册成功";
}

再次输入url来调用注册方法:

image.png

由于事务提交,表中成功插入了数据:

image.png

我们发现用户wangwu此时的自增 id 为5,证明上次的SQL语句即使回滚了,还是留下了执行SQL的痕迹。

既然执行过SQL又是如何进行回滚的呢?接下来我们就来讲讲事务状态TransactionStatus这个对象做了什么。

2.1.4 事务状态的解释

观察事务的回滚和提交的日志信息,并对比差别:

回滚事务的日志:

//创建事务会话
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2014a533]
//从连接池获取连接
JDBC Connection [HikariProxyConnection@207488314 wrapping
//执行SQL语句
com.mysql.cj.jdbc.ConnectionImpl@135f132a] will be managed by Spring
==>  Preparing: insert into userinfo ( username, password ) values ( ?, ? )
==> Parameters: wangwu(String), 123(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2014a533]
2024-04-26 22:10:19.758  INFO 10561 --- [nio-8080-exec-1] c.c.t.controller.UserController          : 插入了1条数据~
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2014a533]
//关闭事务会话
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2014a533]

提交事务的日志:

//创建事务会话
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@599475b7]
//从连接池获取连接
JDBC Connection [HikariProxyConnection@1632144075 wrapping 
//执行SQL语句
com.mysql.cj.jdbc.ConnectionImpl@37fb4a40] will be managed by Spring
==>  Preparing: insert into userinfo ( username, password ) values ( ?, ? )
==> Parameters: wangwu(String), 123(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@599475b7]
2024-04-26 22:02:44.931  INFO 10517 --- [nio-8080-exec-1] c.c.t.controller.UserController          : 插入了1条数据~
//提交事务
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@599475b7]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@599475b7]
//关闭事务会话
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@599475b7]

对比二者发现提交事务的连接多了一个提交事务的操作。

数据库中是类似这样处理的:

  1. 当获取事务状态时,相当于开启了事务,数据库此时会记录一个时间点
TransactionStatus transaction = dataSourceTransactionManager.getTransaction(transactionDefinition);
  1. 执行SQL语句
  2. 通过dataSourceTransactionManager事务管理器进行rollback() or commit()
  3. rollback(): 不提交事务,将事务标记为已回滚状态后关闭事务会话,并回滚到开启事务的时间点;commit(): 提交事务,并将事务标记为已提交状态后关闭事务会话

2.2 声明式事务(推荐)

为了简化编程式事务,Spring新增了声明式事务的方式。

使用一个@Transactional注解声明方法:

@RequestMapping("/registry")
@Transactional
public String registry(UserInfo userInfo) {
    Integer result = userService.insertUser(userInfo);
    log.info("插入了" + result + "条数据~");
    return "注册成功";
}

2.2.1 @Transactional 作用

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

  • 修饰方法:只有修饰public方法时才生效(修饰其他方法时不会报错,也不生效)【推荐】
  • 修饰类:类中所有的public方法都生效

当方法/类被该注解修饰后,它会做下面三件事:

  1. 在方法执行前开启事务
  2. 执行目标方法
  3. 如果未出现异常或异常被程序捕获就提交事务,如果出现异常并没被捕获就回滚事务(不完全对,马上就讲)

2.2.2 @Transactional的常用属性

valuetransactionManager:这两个属性的作用相同,对应事务管理器的名称,分库分表时会用到

@AliasFor("transactionManager")
String value() default "";

@AliasFor("value")
String transactionManager() default "";

propagationisolation:代表传播机制隔离级别,后面详细介绍

Propagation propagation() default Propagation.REQUIRED;

Isolation isolation() default Isolation.DEFAULT;
【引入】回滚相关属性

与回滚相关的属性有下面四个:

Class<? extends Throwable>[] rollbackFor() default {};

String[] rollbackForClassName() default {};

Class<? extends Throwable>[] noRollbackFor() default {};

String[] noRollbackForClassName() default {};

分别解释四个参数:

  • rollbackFor:里面传的是异常的.class对象,抛出符合条件异常就回滚
  • rollbackForClassName:里面传异常的类名,抛出符合条件异常就回滚
  • noRollbackfor:里面传的是异常的.class对象,抛出符合条件异常不回滚
  • noRollbackForClassName:里面传异常的类名,抛出符合条件异常不回滚

注:符合条件指的是抛出该异常或该异常的子类

image.png

如果@Transactional注解没有添加上面任何属性值,那么只有在运行时异常和Error才会进行回滚

举个例子,如果想抛出所有异常都回滚,可以这样写:

@RequestMapping("/registry")
@Transactional(rollbackFor = {Exception.class})
public String registry(UserInfo userInfo) {
    Integer result = userService.insertUser(userInfo);
    log.info("插入了" + result + "条数据~");
    return "注册成功";
}

前面提到如果异常被捕获,就会提交事务,那如果发生异常后被捕获,也希望事务回滚,可以通过下面这两种方式回滚事务:

  1. 继续把异常抛出去
@RequestMapping("/registry")
@Transactional(rollbackFor = {Exception.class})
public String registry(UserInfo userInfo) {
    Integer result = userService.insertUser(userInfo);
    log.info("插入了" + result + "条数据~");
    try {
        int a = 10/0;
    } catch (Exception e) {
        log.warn("发生了异常..");
        throw e;
    }
    return "注册成功";
}
  1. 手动设置回滚
@RequestMapping("/registry")
@Transactional(rollbackFor = {Exception.class})
public String registry(UserInfo userInfo) {
    Integer result = userService.insertUser(userInfo);
    log.info("插入了" + result + "条数据~");
    try {
        int a = 10/0;
    } catch (Exception e) {
        log.warn("发生了异常..");
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
    return "注册成功";
}

2.2.3 Spring 事务隔离级别

理解Spring的隔离级别是以了解SQL标准中的事务隔离级别为基础的,如果对此部分内容有所遗忘可以点击这里:数据库的事务的并发问题和四种隔离级别

MySQL默认的隔离级别是可重复读

通过下面的SQL可以查看MySQL设置的隔离级别

select @@global.tx_isolation,@@tx_isolation;

image.png


Spring中事务隔离级别有5种:对比SQL标准中的隔离级别多了一个DEFAULT

  1. Isolation.DEFAULT:以连接的数据库的事务隔离级别为主
  2. Isolation.READ_UNCOMMITTED:读未提交
  3. Isolation.READ_COMMITTED:读已提交
  4. Isolation.REPEATABLE_READ:可重复读
  5. Isolation.SERIALIZABLE:串行化

Spring的事务隔离级别是通过@Transational注解的isolation属性来设置的:

image.png

2.2.4 Spring 事务传播机制

事务传播机制是Spring新增加的概念,在数据库中是不存在的。

它描述的内容是:多个事务方法存在调用关系时,事务是如何在这些方法之间进行传播的。

假设方法A调用方法B,在方法B上设置不同的事务传播级别程序会 进行不同的处理。事务传播机制的设置是通过@Transational注解的propagation属性来设置的,属性值如下:

  1. Propagation.REQUIRED : 需要事务 (默认的事务传播级别),如果方法A存在事务,则方法B加⼊该事务;如果方法A没有事务,则方法B创建⼀个新的事务。

2. Propagation.SUPPORTS : 支持事务 如果方法A存在事务,则方法B加⼊该事务;如果方法A没有事务,则方法B以⾮事务的⽅式运⾏。

  1. Propagation.MANDATORY :强制性事务 如果方法A存在事务,则方法B加⼊该事务;如果方法A没有事务,则抛出异常。

  2. Propagation.REQUIRES_NEW : 需要新事务 如果方法A存在事务,则把当前事务挂起;如果方法A不存在事务,方法B就创建新事务。也就是说不管外部⽅法是否开启事务, 方法B都会新开启⾃⼰的事务,且开启的事务相互独立,互不干扰。

  3. Propagation.NOT_SUPPORTED : 不支持事务 如果方法A存在事务,则把当前事务挂起(不⽤),方法B以非事务的方式运行;如果方法A不存在事务,方法B以非事务的方式运行。也就是说不管外部⽅法是否开启事务,方法B都会以事务的方式运行。

6.Propagation.NEVER : 非事务 以⾮事务⽅式运⾏,如果当前存在事务,则抛出异常。

7.Propagation.NESTED : 嵌套事务 如果方法A存在事务,则方法B创建⼀个事务作为当前事务的嵌套事务来运⾏;如果方法A没有事务,则该取值等价于 PROPAGATION_REQUIRED


接下来我将通过代码演示加入事务、挂起事务、嵌套事务的区别,前置工作如下:

SQL脚本:

drop table if exists log_info;
create table log_info (
    `id` int primary key auto_increment,
    `username` varchar(20) not null,
    `op` varchar(256) not null,
    `create_time` datetime default now(),
    `update_time` datetime default now() ON UPDATE now()
) default charset 'utf8mb4'

LogMapper:映射日志表的新增SQL

@Mapper
public interface LogMapper {
    @Insert("insert into log_info(username, op) values (#{username},#{op})")
    Integer insertLog(LogInfo logInfo);
}

LogService:新增日志表记录的业务

@Service
public class LogService {
    @Autowired
    private LogMapper logMapper;
    @Transactional
    public Integer insertLog(LogInfo logInfo) {
        Integer ret = logMapper.insertLog(logInfo);
        return  ret;
    }
}

PropaController:分别调用 userServicelogService的方法

@RequestMapping("/propa")
@RestController
public class PropaController {
    @Autowired
    private LogService logService;

    @Autowired
    private UserService userService;

    @Transactional
    @RequestMapping("/p1")
    public String p1(UserInfo userInfo) {
        userService.insertUser(userInfo);
        LogInfo logInfo = new LogInfo();
        logInfo.setUsername(userInfo.getUsername());
        logInfo.setOp("用户主动注册");
        logService.insertLog(logInfo);
        return "注册成功";
    }
}
【解释】加入事务和挂起事务的区别

这里我将通过Propagation.REQUIREDPropagation.REQUIRES_NEW两种隔离级别来演示。

1. 两个被调用的方法都使用Propagation.REQUIRED隔离级别,并且有一个方法出现异常的情况

我将UserServiceLogService的事务隔离级别设置为Propagation.REQUIRED,并在LogService中构造算术异常:

@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;

    @Transactional(propagation = Propagation.REQUIRED)
    public Integer insertUser(UserInfo userInfo) {
        return userMapper.insertUser(userInfo);
    }
}
@Service
public class LogService {
    @Autowired
    private LogMapper logMapper;
    @Transactional(propagation = Propagation.REQUIRED)
    public Integer insertLog(LogInfo logInfo) {
        Integer ret = logMapper.insertLog(logInfo);
        //构造算术异常
        ing a = 10/0;
        return  ret;
    }
}

此时我将userinfo以及log_info表中的所有数据清空,并在浏览器的url中输入http://localhost:8080/propa/p1?username=admin&password=123 ,看看会是什么结果:

image.png

结论: 由于此时UserServiceLogService中的两个方法设置的传播机制都是Propagation.REQUIRED,因此直接加入PropaControllerp1事务,而由于LogService的方法出现算术异常,因此整个事务一起回滚,所有两个表中什么数据都没有。


2. 两个被调用的方法都使用Propagation.NESTED隔离级别,并且有一个方法出现异常的情况

我将UserServiceLogService的事务隔离级别设置为Propagation.REQUIRES_NEW,并在浏览器的url中输入http://localhost:8080/propa/p1?username=admin&password=123 ,再次进行测试:

image.png

结论: 由于此时UserServiceLogService中的两个方法设置的传播机制都是Propagation.REQUIRED_NEW,因此将PropaControllerp1事务挂起,并开启两个新的事务,所有事务之间相互独立,互不干扰,因此插入userinfo的事务成功提交,插入log_info的事务独自回滚。

【解释】加入事务和嵌套事务的区别

这里我将通过Propagation.NESTED隔离级别来演示。

其实加入事务和嵌套事务差不多,只不过在手动回滚的处理下嵌套事务可以进行单独回滚。

1. 不手动设置回滚的情况
UserServiceLogService的事务隔离级别设置为propagation = Propagation.NESTED,并在LogService中构造算术异常并且不捕获异常

@Transactional(propagation = Propagation.REQUIRED)
public Integer insertLog(LogInfo logInfo) {
    Integer ret = logMapper.insertLog(logInfo);
    int a = 10/0;
    return  ret;
}

此时我将userinfo以及log_info表中的所有数据清空并在浏览器的url中输入http://localhost:8080/propa/p1?username=admin&password=123 进行测试:

image.png

结论: 在没有自行进行回滚事务的时候,整个事务一起回滚,与Propagation.REQUIRED是一样的。

2. 手动设置回滚的情况

UserServiceLogService的事务隔离级别设置为propagation = Propagation.NESTED,并在LogService中构造算术异常并在捕获后手动设置回滚

@Transactional(propagation = Propagation.REQUIRED)
public Integer insertLog(LogInfo logInfo) {
    Integer ret = logMapper.insertLog(logInfo);
    try {
        int a = 10/0;
    } catch (Exception e) {
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
    return  ret;
}

此时我将userinfo以及log_info表中的所有数据清空并在浏览器的url中输入http://localhost:8080/propa/p1?username=admin&password=123 进行测试:

image.png

结论: 在自行回滚事务的时候,嵌套事务的传播机制使插入log_info的事务单独回滚,而不影响其他事务。


网站公告

今日签到

点亮在社区的每一天
去签到