Spring事务和事务传播机制
Spring事务和事务传播机制
1.事务
1.1 什么是事务?
事务是一组操作的集合,是一个不可分割的操作.
事务会把所有的操作作为一个整体,一起向数据库提交或者是撤销操作请求.所以这组操作要么同时成功,要么同时失败。
1.2 为什么需要事务?
我们在进行程序开发时,也会有事务的需求
比如转账操作:
第一步:A账户-100元.
第二步:B账户+100元.
如果没有事务,第一步执行成功了,第二步执行失败了,那么A账户的100元就平白无故消失了.如果使
用事务就可以解决这个问题,让这一组操作要么一起成功,要么一起失败.
1.3 事物的操作
事务的操作主要有三步:
- 开事务start transaction/begin(一组操作前开启事务)
- 提交事务:commit(这组操作全部成功,提交事务)
- 回滚事务:rollback(这组操作中间任何一个操作出现异常,回滚事务)
-- 开启事务
start transaction;
-- 提交事务
commit;
-- 回滚事务
rollback;
2.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';
配置文件:
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/trans_test?characterEncoding=utf8&useSSL=false
username: root
password: jqka
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 #配置驼峰自动转换
实体类:
package com.example.trans.model;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class UserInfo {
private Integer id;
private String username;
private String password;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
package com.example.trans.model;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class LogInfo {
private Integer id;
private String username;
private String op;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
2.1 Spring编程式事务
Spring手动操作事务和上面MySQL操作事务类似,有3个重要操作步骤:
- 开启事务(获取事务)
- 提交事务
- 回滚事务
SpringBoot内置了两个对象:
DataSourceTransactionManager
事务管理器.用来获取事务(开启事务)提交或回滚事务TransactionDefinition
是事务的属性,在获取事务的时候需要将TransactionDefinition
传递进去从而获得一个事务TransactionStatus
提交事务和回滚事务只能选择一个
@RestController
@RequestMapping("/user")
public class UserController {
//JDBC事务管理器
@Autowired
private DataSourceTransactionManager dataSourceTransactionManager;
//定义事务属性
@Autowired
private TransactionDefinition transactionDefinition;
@Autowired
private UserService userService;
@RequestMapping("/registry")
public String registry(String name, String password) {
//开启事务
TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
//用户注册
userService.registryUser(name,password);
//提交事务
//dataSourceTransactionManager.commit(transactionStatus);
//回滚事务
dataSourceTransactionManager.rollback(transactionStatus);
return "注册成功";
}
}
2.2 Spring声明式事务@Transactional
添加依赖
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> </dependency>
在需要事务的方法上添加
@Transactional
注解就可以实现了.无需手动开启事务和提交事务,进入方法时自动开启事务,方法执行完会自动提交事务,如果中途发生了没有处理的异常会自动回滚事务.@RestController @RequestMapping("/trans") public class TransactionalController { @Autowired private UserService userService; @Transactional @RequestMapping("/registry") public String registry(String name,String password) { //用户注册 userService.registryUser(name,password); return "注册成功"; } }
@Slf4j @RestController @RequestMapping("/trans") public class TransactionalController { @Autowired private UserService userService; @Transactional @RequestMapping("/registry") public String registry(String name,String password) { //用户注册 userService.registryUser(name,password); log.info("用户数据插入成功"); //强制抛出异常 int a = 10 / 0; return "注册成功"; } }
发现虽然日志显示数据插入成功,但数据库却没有新增数据,事务进行了回滚.
@Transactional 作用
@Transactional可以用来修饰方法或类:
- 修饰方法时:只有修饰public方法时才生效(修饰其他方法时不会报错,也不生效)[推荐]
- 修饰类时:对@Transactional修饰的类中所有的 public方法都生效.
方法/类被@Transactional
注解修饰时,在目标方法执行开始之前,会自动开启事务,方法执行结束之后,自动提交事务.
如果在方法执行过程中,出现异常,且异常未被捕获,就进行事务回滚操作。
如果异常被程序捕获,方法就被认为是成功执行,依然会提交事务.
try {
int a = 10 / 0;
} catch (Exception e) {
e.printStackTrace();
}
运行程序,发现虽然程序出错了,但是由于异常被捕获了,所以事务依然得到了提交。
如果需要事务进行回滚,有以下两种方式:
抛出异常
try { int a = 10 / 0; } catch (Exception e) { throw e; }
手动回滚事务
使用TransactionAspectSupport.currentTransactionStatus()
得到当前的事务,并使用setRollbackOnly
设置setRollbackOnly
try { int a = 10/0; }catch (Exception e){ log.error("发生异常"); TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); }
3.@Transactional详解
@Transactional注解当中的三个常见属性:
rollbackFor:异常回滚属性.指定能够触发事务回滚的异常类型.可以指定多个异常类型
Isolation:事务的隔离级别.默认值为Isolation.DEFAULT
propagation:事务的传播机制. 默认值为Propagation.REQUIRED
3.1 rollbackFor
Spring 的@Transactional
注解,默认仅在抛出 RuntimeException
(运行时异常)及其子类,或 Error
时才会触发事务回滚;普通的Exception
(受检异常)不会触发回滚
try {
int a = 10 / 0;
} catch (Exception e) {
//抛出异常
throw new IOException();
}
此时虽然程序抛出了异常,但是事务依然进行了提交
如果我们需要所有异常都回滚,需要来配置@Transactional
注解当中的rollbackFor
属性,通过rollbackFor
这个属性指定出现何种异常类型时事务进行回滚.
@Transactional(rollbackFor = Exception.class)
@RequestMapping("/r2")
public String r2(String name,String password) throws IOException {
//⽤⼾注册
userService.registryUser(name,password);
log.info("用户数据插入成功");
if (true){
throw new IOException();
}
return "r2";
}
加了rollbackFor
属性设置了任何事务都会回滚,此时事务就不会进行提交
3.2 事务隔离级别
3.2.1 MySQL事务隔离级别
SQL标准定义了四种隔离级别,MySQL全都支持.这四种隔离级别分别是:
- 读未提交(READUNCOMMITTED):读未提交,也叫未提交读.该隔离级别的事务可以看到其他事务中未提交的数据。
因为其他事务未提交的数据可能会发生回滚,但是该隔离级别却可以读到,我们把该级别读到的数据称之为脏数据,这个问题称之为脏读。
- 读提交(READCOMMITTED):读已提交,也叫提交读.该隔离级别的事务能读取到已经提交事务的数据
该隔离级别不会有脏读的问题.但由于在事务的执行中可以读取到其他事务提交的结果,所以在不同时间的相同SQL查询可能会得到不同的结果,这种现象叫做不可重复读
- 可重复读(REPEATABLEREAD):事务不会读到其他事务对已有数据的修改,即使其他事务已提交.也就可以确保同一事务多次查询的结果一致,但是其他事务新插入的数据,是可以感知到的.这也就引发了幻读问题.可重复读,是MySQL的默认事务隔离级别。
比如此级别的事务正在执行时,另一个事务成功的插入了某条数据,但因为它每次查询的结果都是一样的,所以会导致查询不到这条数据,自己重复插入时又失败(因为唯一约束的原因),明明在事务中查询不到这条信息,但自己就是插入不进去,这个现象叫幻读。
- 串行化(SERIALIZABLE):序列化,事务最高隔离级别.它会强制事务排序,使之不会发生冲突,从而解决了脏读,不可重复读和幻读问题,但因为执行效率低,所以真正使用的场景并不多
3.2.2 Spring事务隔离级别
Isolation.DEFAULT
:以连接的数据库的事务隔离级别为主.Isolation.READ_UNCOMMITTED
:读未提交,对应SQL标准中READUNCOMMITTED
Isolation.READ_COMMITTED
:读已提交,对应SQL标准中READCOMMITTED
Isolation.REPEATABLE_READ
:可重复读,对应SQL标准中REPEATABLE READ
Isolation.SERIALIZABLE
:串行化,对应SQL标准中SERIALIZABLE
public enum Isolation {
DEFAULT(-1),
READ_UNCOMMITTED(1),
READ_COMMITTED(2),
REPEATABLE_READ(4),
SERIALIZABLE(8);
private final int value;
private Isolation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
Spring中事务隔离级别可以通过@Transactional中的isolation属性进行设置
@Transactional(isolation = Isolation.READ_COMMITTED)
3.3 Spring事务传播机制
3.3.1 什么是事务传播机制
事务传播机制就是:多个事务方法存在调用关系时,事务是如何在这些方法间进行传播的
比如有两个方法A,B都被@Transactional
修饰,A方法调用B方法
A方法运行时,会开启一个事务.当A调用B时,B方法本身也有事务,此时B方法运行时,是加入A的事务,还是创建一个新的事务呢?
这个就涉及到了事务的传播机制
比如公司流程管理 执行任务之前,需要先写执行文档,任务执行结束,再写总结汇报
此时A部门有一项工作,需要B部门的支援,此时B部门是直接使用A部门的文档,还是新建一个文档呢?
事务隔离级别解决的是多个事务同时调用一个数据库的问题
而事务传播机制解决的是一个事务在多个节点(方法)中传递的问题
3.3.2 事务的传播机制有哪些
@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.
public enum Propagation {
REQUIRED(0),
SUPPORTS(1),
MANDATORY(2),
REQUIRES_NEW(3),
NOT_SUPPORTED(4),
NEVER(5),
NESTED(6);
private final int value;
private Propagation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
比如一对新人要结婚了,关于是否需要房子
- Propagation。REQUIRED:需要有房子.如果你有房,我们就一起住,如果你没房,我们就一起买房.(如果当前存在事务,则加入该事务,如果当前没有事务,则创建一个新的事务)
- Propagation。SUPPORTS:可以有房子.如果你有房,那就一起住.如果没房,那就租房.(如果当前存在事务,则加入该事务,如果当前没有事务,则以非事务的方式继续运行)
- Propagation.MANDAToRY:必须有房子.要求必须有房,如果没房就不结婚.(如果当前存在事务,则加入该事务.如果当前没有事务,则抛出异常)
- Propagation。REQUIRES_NEW:必须买新房.不管你有没有房,必须要两个人一起买房.即使有房也不住.(创建一个新的事务,如果当前存在事务,则把当前事务挂起)
- Propagation。NOT_SUPPORTED:不需要房.不管你有没有房,我都不住,必须租房.(以非事务方式运行,如果当前存在事务,则把当前事务挂起)
- Propagation。NEVER:不能有房子.(以非事务方式运行,如果当前存在事务,则抛出异常)
- Propagation。NESTED:如果你没房,就一起买房.如果你有房,我们就以房子为根据地,做点小生意,(如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行,如果当前没有事务,则该取值等价于PROPAGATION_REQUIRED
3.3.3 Spring事务传播机制使用和各种场景演示
重点学习:
- REQUIRED(默认值)
- REQUIRES_NEW
3.3.3.1 REQUIRED(加入事务)
代码实现:
- 用户注册,插入一条数据
- 记录操作日志,插入一条数据(出现异常)
观察propagation = Propagation.REQUIRED的执行结果
@RestController
@RequestMapping("/propaga")
public class PropagationController {
@Autowired
private UserService userService;
@Autowired
private LogService logService;
@Transactional(propagation = Propagation.REQUIRED)
@RequestMapping("/p1")
public String p1(String name, String password) {
//用户注册
userService.registryUser(name, password);
//记录操作日志
logService.insertLog(name,"用户注册");
return "p1";
}
}
@Service
public class UserService {
@Autowired
private UserInfoMapper userInfoMapper;
@Transactional(propagation = Propagation.REQUIRED)
public void registryUser(String name, String password){
//插入用户信息
userInfoMapper.insert(name, password);
}
}
@Service
public class LogService {
@Autowired
private LogInfoMapper logInfoMapper;
@Transactional(propagation = Propagation.REQUIRED)
public void insertLog(String name,String op){
//记录用户操作
int a = 10 / 0;
logInfoMapper.insertLog(name,"用户注册");
}
}
流程描述:
- p1方法开始事务
- 用户注册,插入一条数据(执行成功)(和p1使用同一个事务)
- 记录操作日志,插入一条数据(出现异常,执行失败)(和p1使用同一个事务)
- 因为步骤3出现异常,事务回滚.步骤2和3使用同一个事务,所以步骤2的数据也回滚了
3.3.3.2 REQUIRES_NEW(新建事务)
将上述UserService和LogService中相关方法事务传播机制改为Propagation.REQUIRES_NEW
@Service
public class UserService {
@Autowired
private UserInfoMapper userInfoMapper;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void registryUser(String name, String password){
//插入用户信息
userInfoMapper.insert(name, password);
}
}
@Service
public class LogService {
@Autowired
private LogInfoMapper logInfoMapper;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void insertLog(String name,String op){
//记录用户操作
int a = 10 / 0;
logInfoMapper.insertLog(name,"用户注册");
}
}
运行程序,发现用户数据插入成功了,日志表数据插入失败。
LogService方法中的事务不影响UserService中的事务.
当我们不希望事务之间相互影响时,可以使用该传播行为。
3.3.3.3 NEVER(不支持当前事务,抛异常)
修改UserService中对应方法的事务传播机制为
@Service
public class UserService {
@Autowired
private UserInfoMapper userInfoMapper;
@Transactional(propagation = Propagation.NEVER)
public void registryUser(String name, String password){
//插入用户信息
userInfoMapper.insert(name, password);
}
}
程序执行报错,没有数据插入.
在标记为“从不”传播的事务中发现了现有事务 。
3.3.3.4 NESTED(嵌套事务)
将上述UserService和LogService中相关方法事务传播机制改为Propagation.NESTED
@Service
public class UserService {
@Autowired
private UserInfoMapper userInfoMapper;
@Transactional(propagation = Propagation.NESTED)
public void registryUser(String name, String password){
//插入用户信息
userInfoMapper.insert(name, password);
}
}
@Service
public class LogService {
@Autowired
private LogInfoMapper logInfoMapper;
@Transactional(propagation = Propagation.NESTED)
public void insertLog(String name,String op){
//记录用户操作
int a = 10 / 0;
logInfoMapper.insertLog(name,"用户注册");
}
}
运行程序,发现没有任何数据插入。
流程描述:
- Controller 中p1方法开始事务
- UserService用户注册,插入一条数据(嵌套p1事务)
- LogService记录操作日志,插入一条数据(出现异常,执行失败)(嵌套p1事务,回滚当前事务,数据添加失败)
- 由于是嵌套事务,LogService出现异常之后,往上找调用它的方法和事务,所以用户注册也失败了.
- 最终结果是两个数据都没有添加
p1事务可以认为是父事务,嵌套事务是子事务.父事务出现异常,子事务也会回滚,子事务出现异常,如果不进行处理,也会导致父事务回滚。