Spring 事务和事务传播机制
文章目录
- 事务回顾
- Spring 中事务的实现
- Spring 编程式事务 (了解)
- Spring 声明式事务 (@Transactional)
- @Transactional注解在什么情况下事务不生效?
- @Transactional 属性
- rollbackFor
- 事务隔离级别
- Spring 事务传播机制
事务回顾
事务将一组相关操作封装为一个不可分割的整体。这组操作在执行过程中,要么全部成功提交并持久化到数据库,要么在任一环节失败时整体撤销(回滚),以此确保数据一致性和完整性
为什么需要事务?
我们在进行程序开发时,也会有事务的需求
比如转账操作:
第一步:A 账户 - 100 元
第二步:B 账户 +100 元
如果没有事务,第一步执行成功了,第二步执行失败了,那么 A 账户的 100 元就平白无故消失了。如果使用事务就可以避免这个问题,让这一组操作要么一起成功,要么一起失败
MySQL事务的操作
在 MySQL 中,事务的操作可概括为三个关键环节:
- 开启事务:通过 START TRANSACTION 显式启动事务,后续操作纳入同一事务上下文
- 执行业务操作:在事务内执行增删改查等操作,所有变更会先写入内存,此时数据变更对其他事务的可见性由 隔离级别 决定
- 确认结果并收尾:
- 若所有操作均成功完成,执行 COMMIT 提交事务,将内存中所有变更一次性持久化到数据库,事务结束
- 若过程中出现异常或不符合预期,执行 ROLLBACK 回滚事务,撤销所有已执行的操作,数据库状态恢复到事务开启前
-- 1. 开启事务(后续操作纳入事务管理)
START TRANSACTION; -- 2. 执行一组操作(DML语句:INSERT/UPDATE/DELETE)
UPDATE account SET balance = balance - 100 WHERE id = 1; -- A账户扣100元
UPDATE account SET balance = balance + 100 WHERE id = 2; -- B账户加100元-- 3. 判断是否提交或回滚
-- 若所有操作成功,提交事务(修改永久生效)
COMMIT;-- 若中间有任何操作失败(如SQL错误、业务逻辑异常),回滚事务(所有修改撤销)
-- ROLLBACK;
Spring 中事务的实现
Spring 对事务也进行了实现,事务操作分为两类:
- 编程式事务 (手动写代码操作事务)
- 声明式事务 (利用注解自动开启和提交事务)
在学习事务之前,我们先准备数据和数据的访问代码
数据准备:
-- 创建数据库
DROP DATABASE IF EXISTS trans_test;
CREATE DATABASE trans_test DEFAULT CHARACTER SET utf8mb4;use trans_test;-- 用户表
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 = '用户表';
代码准备:
- 创建 Spring Boot 项目并引入 Spring Web、Lombok、MyBatis、MySQL 依赖
- 配置文件
# 数据库连接配置
spring:datasource:url: jdbc:mysql://127.0.0.1:3306/trans_test?characterEncoding=utf8&useSSL=falseusername: root # 连接数据库的用户名password: '123456' # 连接数据库的密码driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:configuration:# 开启 MyBatis 的日志打印功能并输出到控制台log-impl: org.apache.ibatis.logging.stdout.StdOutImpl# 配置驼峰自动转换map-underscore-to-camel-case: true
- 实体类
import lombok.Data;
import java.util.Date;@Data
public class UserInfo {private Integer id;private String userName;private String password;private Date createTime;private Date updateTime;
}
- Mapper
@Mapper
public interface UserInfoMapper {@Insert("insert into user_info(`user_name`,`password`) values(#{name},#{password})")Integer insert(String name, String password);
}
- Service
@Service
public class UserService {@Autowiredprivate UserInfoMapper userInfoMapper;public void registryUser(String name, String password) {// 插入用户信息userInfoMapper.insert(name, password);}
}
- Controller
@RestController
public class UserController {@Autowiredprivate UserService userService;@RequestMapping("/registry")public String registry(String name, String password) {//用户注册userService.registryUser(name, password);return "注册成功";}
}
Spring 编程式事务 (了解)
Spring 的编程式事务通过手动编码方式实现事务控制,其核心操作逻辑与 MySQL 原生事务类似,主要包含三个关键步骤:
- 开启事务
- 执行业务操作
- 事务收尾(提交或回滚):
- 若所有业务操作均正常完成,提交事务,使所有变更永久生效
- 若执行过程中出现异常(如业务校验失败、数据库错误等),回滚事务,撤销所有已执行的操作,恢复到事务开启前的状态
代码示例如下:
@RestController
public class UserController {// SpringBoot 内置了两个对象:// JDBC 事务管理器(用于手动管理事务(开启事务、提交、回滚))@Autowiredprivate DataSourceTransactionManager dataSourceTransactionManager;// 定义事务属性@Autowiredprivate TransactionDefinition transactionDefinition;@Autowiredprivate UserService userService;@RequestMapping("/registry")public String registry(String name, String password) {// 开启事务TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);try {// 用户注册userService.registryUser(name, password);// 提交事务(没有异常时提交)dataSourceTransactionManager.commit(transactionStatus);return "注册成功";} catch (Exception e) {// 回滚事务(发生异常时回滚)dataSourceTransactionManager.rollback(transactionStatus);return "注册失败: " + e.getMessage();}}
}
运行程序,访问 http://127.0.0.1:8080/registry?name=admin&password=admin
观察数据库的结果,数据插入成功
模拟异常场景:
try {// 用户注册userService.registryUser(name, password);int a = 10/0;// 提交事务(没有异常时提交)dataSourceTransactionManager.commit(transactionStatus);return "注册成功";} catch (Exception e) {// 回滚事务(发生异常时回滚)dataSourceTransactionManager.rollback(transactionStatus);return "注册失败: " + e.getMessage();}
数据库并没有新增数据
以上代码虽然可以实现事务,但操作繁琐,有没有更简单的实现方法呢?
接下来我们学习声明式事务
Spring 声明式事务 (@Transactional)
声明式事务的实现很简单,只要在需要事务的方法上添加 @Transactional
注解就可以了。无需手动开启事务和提交/回滚事务,进入方法时自动开启事务,方法正常执行完会自动提交事务,如果发生 ERROR 或者 有未捕获的 RuntimeException 及其子类(默认行为)会自动回滚事务
我们来看代码实现:
@RestController
public class UserController {@Autowiredprivate UserService userService;@Transactional@RequestMapping("/registry")public String registry(String name, String password) {//用户注册userService.registryUser(name, password);return "注册成功";}
}
运行程序,访问接口发现数据插入成功
修改程序, 使之出现异常
@RestController
public class UserController {@Autowiredprivate UserService userService;@Transactional@RequestMapping("/registry")public String registry(String name, String password) {//用户注册userService.registryUser(name, password);//强制程序抛出异常int a = 10/0;return "注册成功";}
}
运行程序,访问接口发现事务进行了回滚
我们一般在业务逻辑层(Service层)控制事务,因为在业务逻辑层,一个业务功能可能会包含多个数据操作。在业务逻辑层来控制事务,我们就可以将多个数据操作控制在一个事务范围内。上述代码在 Controller 中书写,只是为了方便学习
@Transactional注解在什么情况下事务不生效?
- 标注了@Transactional的方法里面的异常被捕获了
- 标注了@Transactional的方法发生了非 Error 或者 RuntimeException
- 若是错误的配置以下三种 Propagation,事务将不会发生回滚:
- Propagation.SUPPORTS
- Propagation.NOT_SUPPORTED
- Propagation.NEVER
- 应用在非 public、static、final 方法上时
- 数据库引擎不支持事务
- 调用方法A,A内部调用方法B,A没有@Transaction注解而B有@Transactional注解
- 标注了@Transactional的方法发生的异常不是rollbackFor指定的类型或子类
- 若注解同时指定了 rollbackFor 和 noRollbackFor,且两个属性包含相同或有继承关系的异常类型,noRollbackFor 会覆盖 rollbackFor,导致对应异常不回滚
- 若 @Transactional 所在的类未被 Spring 容器扫描并实例化为 Bean,则 AOP 无法生成代理对象,事务自然不生效
如果异常被程序捕获,方法就被认为是成功执行,依然会提交事务
修改上述代码,对异常进行捕获
@RestController
public class UserController {@Autowiredprivate UserService userService;@Transactional@RequestMapping("/registry")public String registry(String name, String password) {//用户注册userService.registryUser(name, password);//对异常进行捕获try {int a = 10/0;} catch (ArithmeticException e) {e.printStackTrace();}return "注册成功";}
}
运行程序,访问接口发现事务依然得到了提交
如果需要事务进行回滚,有以下两种方式:
- 重新抛出异常
@RestController
public class UserController {@Autowiredprivate UserService userService;@Transactional@RequestMapping("/registry")public String registry(String name, String password) {//用户注册userService.registryUser(name, password);try {int a = 10/0;} catch (ArithmeticException e) {//将异常重新抛出去throw e;}return "注册成功";}
}
- 手动回滚事务
使用 TransactionAspectSupport.currentTransactionStatus() 获取当前事务的状态对象,并调用其 setRollbackOnly() 方法手动标记事务需要回滚
@RestController
public class UserController {@Autowiredprivate UserService userService;@Transactional@RequestMapping("/registry")public String registry(String name, String password) {//用户注册userService.registryUser(name, password);try {int a = 10/0;} catch (ArithmeticException e) {// 手动回滚事务TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();}return "注册成功";}
}
@Transactional 属性
@Transactional 可以用来修饰方法或类:
- 修饰方法时:只有修饰 public 方法时才生效 (修饰其他方法时不会报错,也不生效)
- 修饰类时:对 @Transactional 修饰的类中所有的符合条件的方法都生效
我们主要学习 @Transactional 注解当中的三个常见属性:
- rollbackFor:异常回滚属性。指定能够触发事务回滚的异常类型。可以指定多个异常类型
- Isolation:事务的隔离级别。默认值为 Isolation.DEFAULT
- propagation:事务的传播机制。默认值为 Propagation.REQUIRED
rollbackFor
@Transactional 默认只在遇到运行时异常和 Error 时才会回滚,非运行时异常不回滚
接下来我们改为如下代码
@RestController
public class UserController {@Autowiredprivate UserService userService;@Transactional@RequestMapping("/registry")public String registry(String name, String password) throws IOException {//用户注册userService.registryUser(name, password);throw new IOException("模拟IO异常");}
}
运行程序,访问接口
发现虽然程序抛出了异常,但是事务依然进行了提交,表中还是加了数据
如果我们需要所有异常都回滚,需要通过 rollbackFor指定出现何种异常类型时事务进行回滚
@RestController
public class UserController {@Autowiredprivate UserService userService;@Transactional(rollbackFor = Exception.class)@RequestMapping("/registry")public String registry(String name, String password) throws IOException {//用户注册userService.registryUser(name, password);throw new IOException("模拟IO异常");}
}
事务隔离级别
MySQL 事务隔离级别 (回顾)
SQL 标准定义了四种隔离级别,MySQL 全都支持。这四种隔离级别分别是:
- 读未提交 (READ UNCOMMITTED): 读未提交,也叫未提交读。该隔离级别的事务可以看到其他事务中未提交的数据
因为其他事务未提交的数据可能会发生回滚,但是该隔离级别却可以读到,我们把该级别读到的数据称之为脏数据,这个问题称之为脏读
- 读提交 (READ COMMITTED): 读已提交,也叫提交读。该隔离级别的事务能读取到已经提交事务的数据
该隔离级别不会有脏读的问题。但由于在事务的执行中可以读取到其他事务提交的结果,所以在不同时间的相同 SQL 查询可能会得到不同的结果,这种现象叫做不可重复读
- 可重复读 (REPEATABLE READ): 事务不会读到其他事务对已有数据的修改,即使其他事务已提交。也就可以确保同一事务多次查询的结果一致,但是其他事务新插入的数据,是可以感知到的。这也就引发了幻读问题。可重复读,是 MySQL 的默认事务隔离级别
比如此级别的事务正在执行时,另一个事务成功的插入了某条数据,但因为它每次查询的结果都是一样的,所以会导致查询不到这条数据,自己重复插入时又失败 (因为唯一约束的原因). 明明在事务中查询不到这条信息,但自己就是插入不进去,这个现象叫幻读
- 串行化 (SERIALIZABLE): 序列化,事务最高隔离级别。它会强制事务排序,使之不会发生冲突,从而解决了脏读,不可重复读和幻读问题,但因为执行效率低,所以真正使用的场景并不多
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交(READ UNCOMMITTED) | √ | √ | √ |
读已提交(READ COMMITTED) | × | √ | √ |
可重复读(REPEATABLE READ) | × | × | √ |
串行化(SERIALIZABLE) | × | × | × |
在数据库中通过以下 SQL 查询全局事务隔离级别和当前连接的事务隔离级别:
select @@global.tx_isolation,@tx_isolation;
Spring 事务隔离级别
Spring 中事务隔离级别有 5 种:
- Isolation.DEFAULT:以连接的数据库的事务隔离级别为主.
- Isolation.READ_UNCOMMITTED:读未提交,对应 SQL 标准中 READ UNCOMMITTED
- Isolation.READ_COMMITTED:读已提交,对应 SQL 标准中 READ COMMITTED
- Isolation.REPEATABLE_READ:可重复读,对应 SQL 标准中 REPEATABLE READ
- Isolation.SERIALIZABLE:串行化,对应 SQL 标准中 SERIALIZABLE
Spring 中事务隔离级别可以通过 @Transactional 中的 isolation 属性进行设置
Spring 事务传播机制
事务隔离级别解决的是多个事务同时调用一个数据库的问题,而事务传播机制解决的是一个事务在多个节点 (方法) 中传递的问题
事务传播机制就是:多个事务方法存在调用关系时,事务是如何在这些方法间进行传播的.
比如有两个方法 A,B 都被 @Transactional 修饰,A 方法调用 B 方法
A 方法运行时,会开启一个事务。当 A 调用 B 时,B 方法本身也有事务,此时 B 方法运行时,是加入 A 的事务,还是创建一个新的事务呢?
这个就涉及到了事务的传播机制
@Transactional 注解支持事务传播机制的设置,通过 propagation 属性来指定传播行为
Spring 事务传播机制有以下 7 种:
- Propagation.REQUIRED:默认的事务传播级别。如果当前存在事务,则加入该事务。如果当前没有事务,则创建一个新的事务
- Propagation.SUPPORTS:如果当前存在事务,则加入该事务。如果当前没有事务,则以非事务的方式继续运行
- Propagation.MANDATORY:强制性。如果当前存在事务,则加入该事务。如果当前没有事务,则抛出异常
- Propagation.REQUIRES_NEW:创建一个新的事务。如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部方法都会新开启自己的事务,且开启的事务相互独立,互不干扰。
- Propagation.NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起(不用)
- Propagation.NEVER:以非事务方式运行,如果当前存在事务,则抛出异常
- Propagation.NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行。如果当前没有事务,则该取值等价于 PROPAGATION_REQUIRED