文章目录
- 什么是事务
- 事务的操作
- Spring 中事务的实现
- Spring编程式事务
- Spring 声明式事务 @Transactional
- @Transactional作用
- @Transactional 详解
- rollbackFor
- 事务隔离级别
- Spring 事务隔离级别
- Spring 事务传播机制
什么是事务
事务(Transaction)是一个程序中一系列严密的操作,所有操作执行必须成功完成,否则在每个操作所做的更改将会被撤销,这也是事务的原子性(要么成功,要么失败)。在计算机术语中,事务通常是指访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。事务通常由高级数据库操纵语言或编程语言(如SQL、C++、Java)书写的用户程序的执行所引起,并用形如BeginTransaction和EndTransaction语句(或函数调用)来界定。
前面我们学习 MySQL 的时候,也为大家介绍了关于事务方面的知识,事务具有以下特性:
- 原子性(Atomicity):事务必须是原子工作单元;对于其数据修改,要么全都执行,要么全都不执行。原子性可以消除系统处理操作子集的可能性。
- 一致性(Consistency):事务在完成时,必须使所有的数据都保持一致状态。事务可以保证数据库的完整性,避免因各种原因而导致数据库的内容不一致,产生错误的数据。
- 隔离性(Isolation):事务处理过程中的中间状态对其他事务是透明的。事务隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。
- 持续性(Durability):持续性是指事务一旦提交,它对数据库中数据的改变是永久性的。接下来的操作或故障不应对其有任何影响。
当我们在进行转账或者购物的时候,往往需要用到事务操作,为什么呢?假设我向别人转账 100 元,我钱已经转过去了,但是在对方接收的时候出现了问题,那么我自己账户上的余额少了 100,但是对方的账户上却没有收到我的 100,这种现象是绝对不可以出现的。有了事务的插足,如果在我们钱已经转出去了情况,但是在对方收的过程中发生问题的话,事务就会进行回滚,发出方的 100 元就不会扣掉。
事务的操作
事务的操作主要为下面三个部分:
- 开启事务:start transaction/begin(一组操作前开启事务)
- 提交事务:commit(这组操作全部成功,提交事务)
- 回滚事务:rollback(这组操作中间任何一个操作出现异常,并且这个异常没有被处理,就是回滚事务)
Spring 中事务的实现
在 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 中如何获取到事务呢?
要想获取到事务,我们需要借助 Spring 内置的两个对象:
- DataSourceTransactionManager:事务管理器,用来获取事务(开启事务),提交或回滚事务
- TransactionDefinition:事务的属性,在获取事务的时候需要将 TransactionDefinition 传递进去从而获得一个事务 TransactionStatus
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
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 可以修饰方法,也可以修饰类:
- 修饰方法时:只有修饰 public 方法的时候才会生效(修饰其他权限的方法的时候不会报错,但是也不会生效)【推荐】
- 修饰类时:对
@Transactional
修饰的类中所有的 public 方法都生效
当类/方法被 @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 注解中的属性有很多,但是我们主要学习这三个属性:
- rollbackFor:异常回滚属性。指定能够触发事务回滚的异常类型,默认可以发生事务回滚的异常类型是
RuntimeException
及其子类以及Error
,可以指定多个异常类型 - Isolation:事务的隔离级别。默认值为
Isolation.DEFAULT
- propagation:事务的传播机制。默认值为
Propagation.REQUIRED
rollbackFor
@Transactional
默认只会在发生运行时异常和 Error 的时候才会发生事务的回滚操作:
假设我们抛出的异常是非运行时异常:
try {
System.out.println(10/0);
} catch (Exception e) {
throw new IOException();
}
可以看到,当抛出的异常类型是非运行异常的时候,不会发生事务的回滚。
如果我们想指定,发生非运行异常的时候也能进行事务回滚的操作的话,我们就需要配置 @Transactional
注解中的 rollbackFor
属性:
@Transactional(rollbackFor = Exception.class)
抛出非运行时异常的时候,也进行了事务的回滚操作。
结论:
- 在 Spring 的事务管理中,默认只在遇到运行时异常 RuntimeException 和 Error 的时候才会回滚
- 如果需要回滚指定类型的异常,可以通过 rollbackFor 属性来指定
事务隔离级别
事务的隔离级别有下面几种:
- 未提交读(Read Uncommitted):最低的事务隔离级别,允许读取尚未提交的事务数据。这意味着可能会出现脏读、不可重复读和幻读的情况。
- 该隔离级别可以读到其他事务未提交的数据,但是如果其他事务发生了事务回滚的话,那么我们读到的数据就是“脏数据”,这个问题被称为脏读
- 提交读(Read Committed):在事务执行过程中,只允许读取已经提交的数据。这样可以避免脏读问题,但仍然可能出现不可重复读和幻读。
- 该隔离级别可以避免出现脏读的情况,但是由于该隔离级别可以读取到其他事务已提交的数据,所以在不同时间段读取到的数据可能是不同的,这种现象叫做不可重复读
- 可重复读(Repeatable Read):在这个级别中,事务在其生命周期内可以多次读取同一个数据,而不会看到其他事务对该数据的修改。这可以避免脏读和不可重复读问题,但仍然可能出现幻读。
- 假设该事务级别的事务正在读取数据,并且在此期间,其他事务又插入了新的数据,因为该隔离级别下读取到的数据都是一样的,所以就无法读取到新插入的这条数据,当前事务再插入这条数据的时候,因为唯一主键的约束,就无法成功插入,,但是在当前事务中又查询不到这条数据,又插入不成功,所以就出现了幻读的情况
- 可串行化(Serializable):最高的事务隔离级别,通过强制事务串行执行,避免了脏读、不可重复读和幻读问题。但这种级别的性能开销较大,因为事务必须串行执行。
给大家举个例子:假设我要考试了,但是考试前两天我去老师办公室叫作业,我看见老师电脑上显示的是 2024年高数期末考试试卷草稿,所以我就将试卷给拍了下来,然后回到了寝室就只琢磨复习试卷上出现的题目就,等到考试那天我信心满满的走进考场,但是当我看到试卷的那一刻,我懵了,很多题目都不一样。这是因为我那天看到的只是草稿,在我走后老师又对其进行了修改,这就是脏读的问题。
假设我工厂生产机器,要生产两批不同的机器,A机器生产100台,B机器生产50台,先生产的是 A 机器,我按照给定的图纸来制造,当制造了 95 台 A 机器的时候,上面就将 B 机器的图纸传过来了,但是我不知道,我知道的就是按照图纸来造,这样就导致了 A 机器制造的数量不够,这就是不可重复读的问题。
假设公司让我们一个团队的人进行数据库的增加操作,我的操作就是先查询一遍数据库,看看还有那些数据需要插入,然后我后面插入的时候也是看第一遍查询的结果吗,这样我一个人做的话,是不会出现什么问题的,但是如果跟我同一个团队的人也在执行同样的操作的话,他插入了我将要插入的数据,但是我还是按照第一编查询的结果来,那么这个数据就不能成功插入,我再查询,还是第一遍的结果,我说这条数据我没插入啊,为什么插入不进去呢?这就是幻读的问题。
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 | √ | √ | √ |
读已提交 | × | √ | √ |
可重复读 | × | × | √ |
串行化 | × | × | × |
随着隔离级别的提高,效率也会降低。
Spring 事务隔离级别
Spring 中事务隔离级别有五种:
Isolation.DEFAULT
:以连接的数据库事务隔离级别为主Isolation.READ_UNCOMMITTED
:读未提交,对应SQL标准的READ UNCOMMIT
Isolation.READ_COMMITTED
:读已提交,对应SQL标准的READ COMMIT
Isolation.REPEATABLE_READ
:可重复读,对应SQL标准的REPEATABLE READ
Isolation.SERIALIZABLE
:串行化,对应SQL标准的SERIALIZABLE
Spring 中隔离级别的配置需要配置 @Transactional
注解的 isolation 属性:
@Transactional(rollbackFor = Exception.class, isolation = Isolation.REPEATABLE_READ)
Spring 事务传播机制
什么是事务传播机制?
事务传播机制是指当一个事务(父事务)调用另一个事务(子事务)的方法时,子事务如何传播的事务处理机制。它主要解决的是在多个事务方法相互调用时,如何决定使用哪个事务上下文以及如何管理这些事务的执行顺序和隔离级别。
事务隔离级别解决的是多个事务同时调用一个数据库的问题
而事务传播机制解决的是一个事务在多个节点(方法)中传递的问题
Spring 中的事务传播机制有7种:
Propagation.REQUIRED
:默认的事务传播机制。如果当前存在事务,则加入该事务;如果没有事务,则创建一个新事务Propagation.SUPPORTS
:如果当前存在事务,则加入该事务;如果不存在事务,则以非事务的方式继续运行Propagation.MANDATORY
:强制性。如果当前存在事务,则加入该事务,如果不存在,则抛出异常Propagation.REQUIRES_NEW
:创建一个新的事务。如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW
修饰的内部方法都是新开始自己的事务,且开启的事务相互独立,互不干扰Propagation.NOT_SUPPORTED
:以非事务方式运行,如果当前存在事务,则把当前事务挂起(不用)Propagation.NEVER
:以非事务方式运行,如果当前存在事务,则抛出异常Propagation.NESTED
:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于Propagation.REQUIRED
对于上面的事务传播机制,我这里主要为大家说明两种:
- REQUIRED(默认值)
- REQUIRES_NEW
这里我们在 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 隔离界别的区别:
- 整个事务如果都执行成功,二者的结果是一样的
- 如果事务一部分执行成功,REQUIRED 加入事务会导致整个事务回滚,NESTED 嵌套事务可以实现局部回滚,不会影响上一个方法的执行结果