事务(Transaction)是一个程序中一系列严密的操作,所有操作执行必须成功完成,否则在每个操作所做的更改将会被撤销,这也是事务的原子性(要么成功,要么失败)。在计算机术语中,事务通常是指访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。事务通常由高级数据库操纵语言或编程语言(如SQL、C++、Java)书写的用户程序的执行所引起,并用形如BeginTransaction和EndTransaction语句(或函数调用)来界定。
前面我们学习 MySQL 的时候,也为大家介绍了关于事务方面的知识,事务具有以下特性:
当我们在进行转账或者购物的时候,往往需要用到事务操作,为什么呢?假设我向别人转账 100 元,我钱已经转过去了,但是在对方接收的时候出现了问题,那么我自己账户上的余额少了 100,但是对方的账户上却没有收到我的 100,这种现象是绝对不可以出现的。有了事务的插足,如果在我们钱已经转出去了情况,但是在对方收的过程中发生问题的话,事务就会进行回滚,发出方的 100 元就不会扣掉。
事务的操作主要为下面三个部分:
在 Spring 中实现事务的方式有两种:
这篇文章为大家介绍事务,主要通过操作数据库的操作来体现,所以我们先准备两个表:
-- 创建数据库 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';
配置 MyBatis:
spring: datasource: url: jdbc:mysql://127.0.0.1:3306/trans_test?characterEncoding=utf8&useSSL=false username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl mapper-locations: classpath:mapper/**Mapper.xml
为对应的表创建 model:
import lombok.Data; import java.util.Date; @Data public class UserInfo { private int id; private String userName; private String password; private Date createTime; private Date updateTime; }
import lombok.Data; import java.util.Date; @Data public class LogInfo { private int id; private String userName; private String op; private Date createTime; private Date updateTime; }
MyBatis 操作数据库:
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 (#{userName},#{password})") Integer insert(String userName, String password); }
import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Mapper; @Mapper public interface LogInfoMapper { @Insert("insert into log_info (`name`, `op`) values (#{name},#{op})") Integer insertLog(String name,String op); }
Service 层:
import com.example.springtransaction.mapper.UserInfoMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class UserInfoService { @Autowired private UserInfoMapper userInfoMapper; public void registryUser(String name, String password) { userInfoMapper.insert(name, password); } }
import com.example.springtransaction.mapper.LogInfoMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class LogInfoService { @Autowired private LogInfoMapper logInfoMapper; public void insertLog(String name, String op) { logInfoMapper.insertLog(name,op); } }
Contoller 层:
import com.example.springtransaction.service.UserInfoService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RequestMapping("/user") @RestController public class UserInfoController { @Autowired private UserInfoService userInfoService; @RequestMapping("/registry") public String registry(String name, String password) { userInfoService.registryUser(name, password); return "注册成功"; } }
import com.example.springtransaction.service.LogInfoService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RequestMapping("/log") @RestController public class LogInfoController { @Autowired private LogInfoService logInfoService; @RequestMapping("/insert") public String insertLog(String name, String op) { logInfoService.insertLog(name, op); return "日志插入成功"; } }
Spring 手动操作事务,有三个重要步骤:
在 Spring 中如何获取到事务呢?
要想获取到事务,我们需要借助 Spring 内置的两个对象:
import com.example.springtransaction.service.UserInfoService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionStatus; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RequestMapping("/user") @RestController public class UserInfoController { @Autowired private UserInfoService userInfoService; @Autowired private DataSourceTransactionManager dataSourceTransactionManager; @Autowired private TransactionDefinition transactionDefinition; @RequestMapping("/registry") public String registry(String name, String password) { //开启事务 TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition); userInfoService.registryUser(name, password); //提交事务 dataSourceTransactionManager.commit(transactionStatus); return "注册成功"; } }
访问 127.0.0.1:8080/user/registry 之后,我们查看日志:
观察表可以发现数据插入成功:
这个是事务提交成功的日志,然后我们再来看看当事务回滚之后会出现什么日志:
//回滚事务 dataSourceTransactionManager.rollback(transactionStatus);
可以看到,当发生事务回滚的时候,就只有打开 sqlSession 和关闭 sqlSession 的操作,没有 commit 提交的操作,并且观察数据库可以发现,并没有插入数据:
通过编程式实现事务操作比较复杂,而使用声明式事务就简单很多。
Spring 声明式实现事务很简单,只需要加上 @Transactional 注解就可以实现了,无需手动开启和提交事务,进入方法的时候会自动开始事务,中途发生了未处理的异常就会自动回滚事务。跟前面的 AOP 统一功能处理是一样的,方法开始前干什么,结束后干什么,抛出异常后干什么。
import com.example.springtransaction.service.UserInfoService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RequestMapping("/trans") @RestController public class TransactionalController { @Autowired private UserInfoService userInfoService; @Transactional @RequestMapping("/registry") public String registry(String name, String password) { userInfoService.registryUser(name,password); return "注册成功"; } }
我们在这个方法中制造出异常,看是否会发生事务的回滚:
System.out.println(10/0);
没有提交事务就说明发生了事务的回滚。
@Transactional 可以修饰方法,也可以修饰类:
当类/方法被 @Transactional 修饰的时候,在目标方法执行之前,就会自动开启事务,方法执行结束之后,会自动结束事务。但是如果在方法执行的过程中出现了异常,并且异常没有被正确捕获的话,就会进行事务的回滚操作;如果这个异常被成功捕获,那么方法就会被认为是正常执行,事务就会被正常提交。
我们对上面制造的异常进行捕获:
try { System.out.println(10/0); } catch (Exception e) { e.printStackTrace(); }
事务被提交,并且数据库的插入操作成功:
如果我们想要自己控制事务的回滚,有两种方式可以达到:
(1)重新抛出异常:
try { System.out.println(10/0); } catch (Exception e) { throw e; }
(2)手动回滚事务:
首先我们需要通过 TransactionAspectSupport.currentTransactionStatus()方法来获取到当前事务,然后再调用 setRollbackOnly 进行事务的回滚操作:
try { System.out.println(10/0); } catch (Exception e) { TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); }
上面我们了解了 @Transactional 的基本使用,那么接下来我们将详细学习一下 @Transactional 注解。
@Transactional 注解中的属性有很多,但是我们主要学习这三个属性:
@Transactional 默认只会在发生运行时异常和 Error 的时候才会发生事务的回滚操作:
假设我们抛出的异常是非运行时异常:
try { System.out.println(10/0); } catch (Exception e) { throw new IOException(); }
可以看到,当抛出的异常类型是非运行异常的时候,不会发生事务的回滚。
如果我们想指定,发生非运行异常的时候也能进行事务回滚的操作的话,我们就需要配置 @Transactional 注解中的 rollbackFor 属性:
@Transactional(rollbackFor = Exception.class)
抛出非运行时异常的时候,也进行了事务的回滚操作。
结论:
事务的隔离级别有下面几种:
给大家举个例子:假设我要考试了,但是考试前两天我去老师办公室叫作业,我看见老师电脑上显示的是 2024年高数期末考试试卷草稿,所以我就将试卷给拍了下来,然后回到了寝室就只琢磨复习试卷上出现的题目就,等到考试那天我信心满满的走进考场,但是当我看到试卷的那一刻,我懵了,很多题目都不一样。这是因为我那天看到的只是草稿,在我走后老师又对其进行了修改,这就是脏读的问题。
假设我工厂生产机器,要生产两批不同的机器,A机器生产100台,B机器生产50台,先生产的是 A 机器,我按照给定的图纸来制造,当制造了 95 台 A 机器的时候,上面就将 B 机器的图纸传过来了,但是我不知道,我知道的就是按照图纸来造,这样就导致了 A 机器制造的数量不够,这就是不可重复读的问题。
假设公司让我们一个团队的人进行数据库的增加操作,我的操作就是先查询一遍数据库,看看还有那些数据需要插入,然后我后面插入的时候也是看第一遍查询的结果吗,这样我一个人做的话,是不会出现什么问题的,但是如果跟我同一个团队的人也在执行同样的操作的话,他插入了我将要插入的数据,但是我还是按照第一编查询的结果来,那么这个数据就不能成功插入,我再查询,还是第一遍的结果,我说这条数据我没插入啊,为什么插入不进去呢?这就是幻读的问题。
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 | √ | √ | √ |
读已提交 | × | √ | √ |
可重复读 | × | × | √ |
串行化 | × | × | × |
随着隔离级别的提高,效率也会降低。
Spring 中事务隔离级别有五种:
Spring 中隔离级别的配置需要配置 @Transactional 注解的 isolation 属性:
@Transactional(rollbackFor = Exception.class, isolation = Isolation.REPEATABLE_READ)
什么是事务传播机制?
事务传播机制是指当一个事务(父事务)调用另一个事务(子事务)的方法时,子事务如何传播的事务处理机制。它主要解决的是在多个事务方法相互调用时,如何决定使用哪个事务上下文以及如何管理这些事务的执行顺序和隔离级别。
事务隔离级别解决的是多个事务同时调用一个数据库的问题
而事务传播机制解决的是一个事务在多个节点(方法)中传递的问题
Spring 中的事务传播机制有7种:
对于上面的事务传播机制,我这里主要为大家说明两种:
这里我们在 controller 层和 service 层都加上这个注解 @Transactional(propagation = Propagation.REQUIRED)
@RequestMapping("/trans") @RestController public class TransactionalController { @Autowired private UserInfoService userInfoService; @Autowired private LogInfoService logInfoService; @Transactional(rollbackFor = Exception.class, isolation = Isolation.REPEATABLE_READ, propagation = Propagation.REQUIRED) @RequestMapping("/registry") public String registry(String name, String password) throws IOException { userInfoService.registryUser(name,password); logInfoService.insertLog(name,"注册"); return "注册成功"; } }
在其中一个事务中制造异常:
@Transactional(propagation = Propagation.REQUIRED) public void registryUser(String name, String password) { userInfoMapper.insert(name, password); System.out.println(10/0); }
可以看到,当事务传播机制为 Propagation.REQUIRED 的时候,当其中任何一个事务出现异常的时候,整个事务都会执行事务回滚的操作。
再来看看 Propagation.REQUIRED_NEW 隔离级别:
@Transactional(propagation = Propagation.REQUIRES_NEW)
还是 userInfoService 中抛出异常:
可以看到 Propagation.REQUIRED_NEW 隔离级别中的事务都是相互独立的,互不影响。
将隔离级别改为 NEVER
当隔离级别为 NEVER 的时候,如果当前存在事务,就会直接报错。
NESTES 隔离级别
使用嵌套 NESTED 隔离级别,当其中一个事务抛出异常之后,所有事务都会回滚。
但是这样不就和 REQUIRED 隔离级别是一样的吗?这样看确实一样,但是还是有区别的:
我们将出现错误的事务单独进行回滚:
@Transactional(propagation = Propagation.NESTED) public void registryUser(String name, String password)throws RuntimeException { userInfoMapper.insert(name, password); try { System.out.println(10/0); }catch (Exception e) { TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); } }
然后将隔离级别改为 REQUIRED:
整个事务都回滚了。
所以 REQUIRED 和 NESTED 隔离界别的区别: