Spring 事务

使用事务的原因

事务是把一组操作封装成一个执行单元(封装到一起),要么全部成功,要么全部失败。

比如: 用户A 要给 用户B 转账 100块,第一步是 用户A 扣 100 快,第二步是 用户B 加 100块,但是如果第一步执行成功了,但是第二步突然失败了,那么 A的 100块,就消失了。这搁谁都受不了,所以事务就可以来解决这样的问题,让这组操作要么一起成功,要么一起失败。

实现事务的原理 :是通过 日志 来实现的。会记录一个日志,只有事务的开始和事务的执行,但是没有事务的结束。等下一次恢复的时候,会进行日志的自检,如果发现日志只执行了一半,没执行完,就会执行补偿机制。之前扣掉的钱,现在再加回来。就是表示执行失败了。

Spring 中事务的实现

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

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

编程式事务

编程式事务,也就是手动操作事务,和 MySQL 操作事务类似,也是有三个重要操作步骤:

  1. 开启事务(获取事务)
  2. 提交事务
  3. 回滚事务

Spring Boot 内置了两个对象,也就是涵盖了上面的这些功能:

  1. DataSourceTransactionManager:⽤来获取事务(开启事务)、提交或回滚事务的。
  2. TransactionDefinition:是事务的属性,在获取事务的时候需要将TransactionDefinition 传递进去从⽽获得⼀个事务 TransactionStatus。

我们需要创建一个 SSM 项目,然后配置一下配置文件的信息。yml 代码如下:

# 配置数据库的连接字符串
spring:
  datasource:
    url: jdbc:mysql://127.0.0.1/mycnblog?characterEncoding=utf8
    username: root
    password: sjp151
    driver-class-name: com.mysql.cj.jdbc.Driver
# 设置 Mybatis 的 xml 保存路径
mybatis:
  mapper-locations: classpath:mybatis/**Mapper.xml
  configuration: # 配置打印 MyBatis 执行的 SQL
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 配置打印 MyBatis 执行的 SQL
logging:
  level:
    com:
      example:
        springtransaction: debug

设置 User 的信息:

@Data
public class UserInfo {
    private int id;
    private String username;
    private String password;
    private String photo;
    private String createtime;
    private String updatetime;
    private int state;
}

然后在 Mapper 当中添加方法:

@Mapper
public interface UserMapper {
    int add(UserInfo userInfo);
}

xml 当中的 SQL 代码如下:

<insert id="add">
    insert into userinfo(username,password) values (#{username}, #{password});
</insert>

回滚事务

设置 Controller 的方法:

@RequestMapping("/add")
public int add(UserInfo userInfo) throws InterruptedException {
    //非空校验
    if (userInfo == null || !StringUtils.hasLength(userInfo.getUsername())
        || !StringUtils.hasLength(userInfo.getPassword())) {
        return 0;
    }
    //开启事务
    TransactionStatus transactionStatus = transactionManager.getTransaction(transactionDefinition);
    int result = userService.add(userInfo);
    System.out.println("add 受影响的行数:" + result);
    transactionManager.rollback(transactionStatus);//回滚事务
    return result;
}

先看数据库的内容:
在这里插入图片描述

运行结果如下:
在这里插入图片描述
也就是受影响的行数是 1,然后我们看数据库的内容:
在这里插入图片描述
并没有发生改变,也就是我们的事务回滚起效果了。

提交事务

上面的代码是回滚事务,下面我们来试试提交事务:

@RequestMapping("/add")
public int add(UserInfo userInfo) throws InterruptedException {
    //非空校验
    if (userInfo == null || !StringUtils.hasLength(userInfo.getUsername())
        || !StringUtils.hasLength(userInfo.getPassword())) {
        return 0;
    }
    //开启事务
    TransactionStatus transactionStatus = transactionManager.getTransaction(transactionDefinition);
    int result = userService.add(userInfo);
    System.out.println("add 受影响的行数:" + result);
    
    //提交事务
 	transactionManager.commit(transactionStatus);    return result;
}

运行结果如下:
在这里插入图片描述
然后返回数据库查看信息:
在这里插入图片描述
成功添加数据信息。

声明式事务

声明式事务,也就是添加 @Transactional 注解,但是只有方法执行没有问题的时候,才会进行事务的提交操作,如果方法出现了异常,才会进行回滚操作。代码如下:

@RequestMapping("/add2")
public int add2(UserInfo userInfo) {
    //非空校验
    if (userInfo == null || !StringUtils.hasLength(userInfo.getUsername())
            || !StringUtils.hasLength(userInfo.getPassword())) {
        return 0;
    }
    int result = 0;
    try {
        result = userService.add(userInfo);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    System.out.println("add 受影响的行数:" + result);
    //程序出现异常之后,事务就会回滚
    int num = 10/0;
    return result;
}

访问结果如下:
在这里插入图片描述
直接报错,报错信息是 除0,然后来看 SQL:
在这里插入图片描述
说明程序是先插入,然后才报错的。查看数据库信息:
在这里插入图片描述
数据库并没有添加新的数据,也就是数据进行回滚了。去掉 除0 的那行,然后再运行,结果如下:
在这里插入图片描述

@Transactional 作用范围

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

  1. 修饰方法时,需要注意的是,只能应用到 public 方法上,否则不生效。
  2. 修饰类时,表示这个注解对类中所有的 public 方法都生效。都会自动开启和提交 / 回滚事务。

@Transactional 的参数

@Transactional 有很多参数,如下图:
在这里插入图片描述

  1. value 和 transactionManager,就是如果有多个事务管理器时,可以使用 该属性指定选择哪个事务管理器。
  2. propagation,就是指事务的传播级别,就是有多个事务存在嵌套的情况,那么,我多个事务之间的行为模式,就叫做事务的传播机制。
  3. isolation 和 事务的隔离级别,isolation 参数 是用来设置 事务的隔离级别。

MySQL 事务的隔离级别

MySQL 的事务隔离级别有四种:

  1. READ UNCOMMITTED:读未提交,也叫未提交读。
    a)这个隔离级别的事务可以看到其他事务中未提交的数据。
    b)它的隔离级别因为可以读取到其他事务中未提交的数据,⽽未提交的数据可能会发⽣回滚。
    c)因此我们把该级别读取到的数据称之为脏数据,把这个问题称之为脏读。

  2. READ COMMITTED:读已提交,也叫提交读。
    a)这个隔离级别的事务能读取到已经提交事务的数据,因此它不会有脏读问题。
    b)但由于在事务的执⾏中可以读取到其他事务提交的结果,所以在不同时间的相同 SQL 查询中,可能会得到不同的结果(可能有老六修改了提交的数据),这种现象叫做不可重复读。

  3. REPEATABLE READ:可重复读。
    a)是 MySQL 的默认事务隔离级别,它能确保同⼀事务多次查询的结果是⼀致的。但也会有新的问题。
    b)此级别的事务正在执⾏时,另⼀个事务成功的插⼊了某条数据,在下一次 以同样的 SQL 查询的时候,突然发现多出一条记录。无缘无故 多出一个条记录,好神奇,这就叫幻读(Phantom Read)。

  4. SERIALIZABLE:序列化。
    a)它事务最⾼隔离级别,它会强制事务排序,使之不会发⽣冲突,从⽽解决了脏读、不可重复读和幻读问题,但因为执⾏效率低,所以真正使⽤的场景并不多。

用图片更直观的看:
在这里插入图片描述

使用事务

通过 isolation 就可以实现事务的隔离级别了:
在这里插入图片描述
第一个是默认事务隔离级别,也就是可重复读。

要注意的是:

  1. 当 Spring 中设置了事务隔离级别 和 连接的数据库(MySQL)事务隔离级别 发生冲突的时候,那么,会以 Spring 的为主。
  2. Spring 中的 事务隔离级别机制的实现,是依靠 连接的数据库中 支持的事务隔离级别 为基础。

简单来说:虽然我们在项目中设置了隔离级别,但是!项目中的隔离级别是否生效,还是要看 连接的数据库 是否支持 项目中设置的隔离级别。

timeout :事务的超时时间。

  1. 默认值是 -1。表示的是,没有时间限制。没有规定超过多少时间算超时。

  2. 如果设置了超时时间,并超过了该时间。也就是说: 事务 到了规定的时间,还没有执行完。此时,就会认为这个事务无法完成,自动回滚事务。数据库现有数据如下:
    在这里插入图片描述

    在 userService 当中休眠三秒钟。代码如下:

    public int add(UserInfo userInfo) {
        try {
            Thread.sleep(3*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return userMapper.add(userInfo);
    }
    

    然后在方法里面设置 timeout:

    @Transactional(timeout = 1)
    @RequestMapping("/add2")
    public int add2(UserInfo userInfo) {
        //非空校验
        if (userInfo == null || !StringUtils.hasLength(userInfo.getUsername())
                || !StringUtils.hasLength(userInfo.getPassword())) {
            return 0;
        }
        int result = 0;
        result = userService.add(userInfo);
        System.out.println("add 受影响的行数:" + result);
        //程序出现异常之后,事务就会回滚
        return result;
    }
    

    运行结果如下,报了 Transaction time out 错误:
    在这里插入图片描述

readOnly :就是指定事务为只读事务,默认是 false,默认指定的事务不是 只读事务。表示该事务只能被读取。不能进行其它操作。确认只是读数据的话,就设置 read-only 为 true。

rollbackFor (类型)/ rollbackForClassName(String类型) :就是指定能够触发事务回滚的异常类型。可以指定多个异常类型。当发生某些异常的时候,能够 执行 事务回滚操作。

要注意的是 :由于我们都是使用注解来解决问题的,内部的运行对于我们程序员来说是不可见的(黑盒)。所以,有些时候,是会出现一些意外错误的。

@Transactional 在异常被捕获的情况下,就不会进⾏事务⾃动回滚。

  1. 当然也可以进行抛出异常,然后事务自动进行回滚。
  2. 当然也可以通过代码手动回滚 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); 来实现事务的回滚。

@Transactional 工作原理

  1. @Transactional 是基于 AOP 实现的,AOP 优势使用动态代理实现的。
  2. 如果⽬标对象实现了接⼝,默认情况下会采⽤ JDK 的动态代理,如果⽬标对象没有实现了接⼝,会使⽤ CGLIB 动态代理。
  3. @Transactional 在开始执⾏业务之前,通过代理先开启事务,在执⾏成功之后再提交事务。如果中途遇到的异常,则回滚事务。

@Transactional 实现思路大致如下:
在这里插入图片描述

@Transactional 具体执行细节如下图:
在这里插入图片描述

Spring 事务传播机制

什么是事务传播机制

Spring 事务传播机制定义了多个包含了事务的方法,相互调用时,事务时如何在这些方面进行传递的。

但是我们到了具体的业务方法里面,到框架里面,业务场景就会变得很复杂很复杂。就像。传播机制如下,在一条调用链之下:
在这里插入图片描述
并发事务如下:
在这里插入图片描述

传播机制有哪些

在方法说明添加 @Transactional 之后,然后添加 propagation 属性就可以了:
在这里插入图片描述
一共有上面这七种:

  1. Propagation.REQUIRED:默认的事务传播级别,它表示如果当前存在事务,则加⼊该事务;如果当前没有事务,则创建⼀个新的事务。就像结婚买房,房子(事务)是必须项。女方就是:男方买房子,就住进去,如果男方没房子,也没事,一起努力买房,但是房子必须有。
  2. Propagation.SUPPORTS:如果当前存在事务,则加⼊该事务;如果当前没有事务,则以⾮事务的⽅式继续运⾏。继续使用上面的例子,也就是说,男方有没有房子都无所谓,就想和他结婚。有房子,就住。没有,就租房。
  3. Propagation.MANDATORY:(mandatory:强制性)如果当前存在事务,则加⼊该事务;如果当前没有事务,则抛出异常。就相当于,如果男方没钱,就不结婚,甚至直接分手。有的话,女方就直接住进去。
  4. Propagation.REQUIRES_NEW:表示创建⼀个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部⽅法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部⽅法会新开启⾃⼰的事务,且开启的事务相互独⽴,互不⼲扰。就相当于男方已经买了房,但是女生表示不是新房,我不干了。
  5. Propagation.NOT_SUPPORTED:以⾮事务⽅式运⾏,如果当前存在事务,则把当前事务挂起。就相当于,男方有房子,女方也不住,就是要租房,
  6. Propagation.NEVER:以⾮事务⽅式运⾏,如果当前存在事务,则抛出异常。就相当于,男方有房子,就不结婚了,直接分手。如果没有房子,那我就和你结婚。
  7. Propagation.NESTED:如果当前存在事务,则创建⼀个事务作为当前事务的嵌套事务来运⾏。如果当前没有事务,则该取值等价于 PROPAGATION_REQUIRED(创建一个新事物)。就相当于是:男方有房,女方家里又给了一套房,如果男生没有房,女方表示也可以直接送一套。

上面的几种,可以划分为以下三类:
在这里插入图片描述
如果划分为情侣关系的话,就是这样:
在这里插入图片描述

支持当前事务(REQUIRED)

我们需要创建两张表,用户表 和 日志表,这样搭配起来,才能突出事务嵌套的情况。方便我们去模拟事务嵌套的情况。我们来使用 userinfo 表和 loginfo 表:
在这里插入图片描述
创建 LogInfo 的 Mapper 方法:

@Mapper
public interface LogMapper {
    int add(LogInfo logInfo);
}

XML 当中的 SQL 如下:

<insert id="add">
    insert into loginfo(name,`desc`) values (#{name},#{desc});
</insert>

Controller 当中进行添加方法:

@Transactional(propagation = Propagation.REQUIRED)
@RequestMapping("/add4")
public int add4(UserInfo userInfo) throws InterruptedException {
    if (userInfo == null || !StringUtils.hasLength(userInfo.getUsername())
            || !StringUtils.hasLength(userInfo.getPassword())) {
        return 0;
    }
    int userResult = userService.add(userInfo);
    System.out.println("添加用户:" + userResult);
    LogInfo logInfo = new LogInfo();
    logInfo.setName("添加用户");
    logInfo.setDesc("添加用户结果:" + userResult);
    int logResult = logService.add(logInfo);
    return userResult;
}

然后 Service 层进行 add 方法:

@Transactional(propagation = Propagation.REQUIRED)
public int add(LogInfo logInfo) {
    int num = 10/0;
    return logMapper.add(logInfo);
}

这样的话,日志添加失败,事务是会全部回滚,还是只有日志回滚。两张表的数据如下:
在这里插入图片描述
访问结果下:
在这里插入图片描述
报了我们之前设置好的算术异常。日志信息如下:
在这里插入图片描述
说明添加用户的时候没问题,添加日志的时候出了异常。然后再次查看那两张表的数据:
在这里插入图片描述
数据库当中的数据没有发生改变,也就是说一个方法里面,如果发生了异常,那么所有事务都会回滚。也就是 REQUIRED 的结果。

Controller,UserService,LogService 的执行逻辑如下
在这里插入图片描述

不支持当前事务(REQUIRES_NEW)

对 Service 层都加上 REQUIRES_NEW 事务,logservice:

@Transactional(propagation = Propagation.REQUIRES_NEW)
public int add(LogInfo logInfo) {
    int result = logMapper.add(logInfo);;
    System.out.println("添加日志结果:" + result);
    int num = 10/0;
    return logMapper.add(logInfo);
}

userservice:

@Transactional(propagation = Propagation.REQUIRES_NEW)
public int add(UserInfo userInfo) throws InterruptedException {
    int result = userMapper.add(userInfo);
    return result;
}

运行结果如下:
在这里插入图片描述
还是算术异常,然后看日志:
在这里插入图片描述
日志当中的用户和日志都添加成功了,然后我们来看数据库数据:
在这里插入图片描述
发现 userinfo 数据表里面多了休息,而 loginfo 却没有,这就是我们设置了 REQUIRES_NEW 的原因。然后 loginfo 报错,数据回滚。

非事务(NOT_SUPPORTED)

也是不支持当前事务的一种,但这个是直接以非事务运行的,然后我们把 service 当中的 add 改为 NOT_SUPPORTED。

userservice:

@Transactional(propagation = Propagation.NOT_SUPPORTED)
public int add(UserInfo userInfo) throws InterruptedException {
    int result = userMapper.add(userInfo);
    return result;
}

logservice:

@Transactional(propagation = Propagation.NOT_SUPPORTED)
public int add(LogInfo logInfo) {
    int result = logMapper.add(logInfo);;
    System.out.println("添加日志结果:" + result);
    int num = 10/0;
    return logMapper.add(logInfo);
}

然后继续运行:
在这里插入图片描述
仍然是算术异常的500错误,然后查看日志:
在这里插入图片描述
也是添加完日志之后才报的错,我们查看数据库信息:
在这里插入图片描述
发现两者都添加了数据,也就是说在 NOT_SUPPORTED 事务下,是不会发生事务回滚的。

事务嵌套 NESTED

NESTED 就是支持事务的嵌套,就是把 Controller 和 Service 当中的事务都设置为 NESTED,那么此时就算是把 Service 当中的事务嵌套在 主事务中了。

controller 代码:

@Transactional(propagation = Propagation.NESTED)
@RequestMapping("/add4")
public int add4(UserInfo userInfo) throws InterruptedException {
    if (userInfo == null || !StringUtils.hasLength(userInfo.getUsername())
            || !StringUtils.hasLength(userInfo.getPassword())) {
        return 0;
    }
    int userResult = userService.add(userInfo);
    System.out.println("添加用户:" + userResult);
    LogInfo logInfo = new LogInfo();
    logInfo.setName("添加用户");
    logInfo.setDesc("添加用户结果:" + userResult);
    int logResult = logService.add(logInfo);
    return userResult;
}

userservice:

@Transactional(propagation = Propagation.NESTED)
public int add(UserInfo userInfo) throws InterruptedException {
    int result = userMapper.add(userInfo);
    return result;
}

logservice:

@Transactional(propagation = Propagation.NESTED)
public int add(LogInfo logInfo) {
    int result = logMapper.add(logInfo);;
    System.out.println("添加日志结果:" + result);
    try {
        int num = 10/0;
    } catch (NumberFormatException e) {
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
    return logMapper.add(logInfo);
}

运行结果如下:
在这里插入图片描述
仍然是报算术异常,看日志:
在这里插入图片描述
显示都添加成功了,然后看数据库:
在这里插入图片描述
发现 log 的添加方法出现异常之后,数据回滚了。调用流程如下:

  1. controller/add -> 用户添加方法 -> 日志添加方法。
  2. 当日志添加方法出现异常之后,嵌套事务的执行结果是:
    a)用户添加不受影响,添加用户成功了
    日志添加失败,因为发生异常,回滚了事务。
  • 6
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lockey-s

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值