Spring 事务和事务传播机制
什么是事务
事务是一组操作的集合,是一个不可分割的操作
事务会把所有的操作作为一个整体,一起向数据库提交或者是撤销操作请求。所以这组操作要么同时成功,要么同时失败
事务操作
事务的操作主要有三步:
- 开启事务:
start transaction
/begin
(一组操作前开启事务)。 - 提交事务:
commit
(这组操作全部成功,提交事务)。 - 回滚事务:
rollback
(这组操作中间任何一个操作出现异常,回滚事务)。
-- 开启事务
start transaction;
-- 提交事务
commit;
-- 回滚事务
rollback;
Spring 事务的实现
前面课程我们讲了MySQL的事务操作,Spring对事务也进行了实现
Spring中的事务操作分为两类:
- 编程式事务(手动写代码操作事务)
- 声明式事务(利用注解自动开启和提交事务)
Spring编程式事务
Spring编程式事务:
- 开启事务
- 提交事务
- 回滚食物
SpringBoot内置了两个对象:
DataSourceTransactionManager
:事务管理器,用来获取事务(开启事务)、提交或回滚事务TransactionDefinition
:是事务的属性,在获取事务的时候需要将TransactionDefinition
传递进去从而获得一个事务TransactionStatus
import com.example.demo.service.UserService;
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 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 "注册成功";
}
}
以上的代码,虽然可以实现事务,但是操作也很繁琐。接下来我们学习声明式事务
Spring 声明式事务 @Transactional
声明式事务很简单
- 首先先添加依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
- 在需要事务的方法上添加
@Transactional
注解就可以实现了。无需手动开启事务和提交事务,进入方法时自动开启事务,方法执行完会自动提交事务,如果中途发生了没有处理的异常会自动回滚事务。
我们来看代码实现:
@RequestMapping("/trans")
@RestController
public class TransactionalController {
@Autowired
private UserService userService;
@Transactional
@RequestMapping("/registry")
public String registry(String name, String password) {
// 用户注册
userService.registryUser(name, password);
return "注册成功";
}
}
注意⚠️:
当出现异常的时候,日志显示数据会插入成功,但是数据库没有新增数据,事务进行了回滚
@Transactional作用
@Transactional
可以用来修饰方法或类:
- 修饰方法时:只有修饰
public
方法时才生效(修饰其他方法时不会报错,也不生效)[推荐]。 - 修饰类时:对
@Transactional
修饰的类中所有的public
方法都生效。
方法/类被 @Transactional
注解修饰时,在目标方法执行开始之前,会自动开启事务,方法执行结束之后,自动提交事务
如果在方法执行过程中,出现异常,且异常未被捕获,就进行事务回滚操作
如果异常被程序捕获,方法就被认为是成功执行,依然会提交事务
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Transactional
public void addUser(String username, String password) {
try {
String sql = "INSERT INTO users (username, password) VALUES (?,?)";
jdbcTemplate.update(sql, username, password);
throw new RuntimeException("模拟异常");
} catch (Exception e) {
// 异常被捕获,事务依然会提交
System.out.println("捕获到异常,但事务会提交");
}
}
}
在addUser方法中,虽然抛出了运行时异常,但是异常被try-catch块捕获了。这种情况下,Spring 会认为方法成功执行,依然会提交事务,插入数据库的操作不会回滚
事务回滚有以下两种方法:
- 重新抛出异常
@Transactional
@RequestMapping("/registry")
public String registry(String name, String password) {
//用户注册
userService.registryUser(name, password);
log.info("用户数据插入成功");
//对异常进行捕获
try {
//强制程序抛出异常
int a = 10 / 0;
} catch (Exception e) {
//将异常重新抛出去
throw e;
}
return "注册成功";
}
- 手动回滚事务
使用TransactionAspectSupport.currentTransactionStatus()
得到当前的事务,并使用setRollbackOnly
设置回滚。
@Transactional
@RequestMapping("/registry")
public String registry(String name, String password) {
//用户注册
userService.registryUser(name, password);
log.info("用户数据插入成功");
//对异常进行捕获
try {
//强制程序抛出异常
int a = 10 / 0;
} catch (Exception e) {
// 手动回滚事务
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return "注册成功";
}
@Transactional详解
我们主要学习@Transactional
注解当中的三个常见属性:
rollbackFor
:异常回滚属性,指定能够触发事务回滚的异常类型。可以指定多个异常类型Isolation
:事务的隔离级别。默认值为Isolation.DEFAULT
propagation
:事务的传播机制。默认值为Propagation.REQUIRED
rollbackFor
@Transactional 默认只有在遇到异常和报错的时候才会回滚,不运行的情况下异常不回滚,即Exception的子类中,除了RuntimeException及其子类
如果我们需要所有异常都回滚,需要来配置 @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";
}
结论:
- 默认运行遇到异常RuntimeException和报错才会回滚
- 如果需要指定异常类型回滚,需要rollbackFor属性指定
事务隔离级别
MySQL事务隔离级别
- 读未提交(READ UNCOMMITTED):读未提交,也叫未提交读。该隔离级别的事务可以看到其他事务中未提交的数据
因为其他事务未提交的数据可能会发生回滚,但是该隔离级别却可以读到,我们把该级别读到的数据称之为脏数据,这个问题称之为脏读
- 读提交(READ COMMITTED):读已提交,也叫提交读。该隔离级别的事务能读取到已经提交事务的数据
该隔离级别不会有脏读的问题。但由于在事务的执行中可以读取到其他事务提交的结果,所以在不同时间的相同 SQL 查询可能会得到不同的结果,这种现象叫做不可重复读
- 可重复读(REPEATABLE READ):事务不会读到其他事务对已有数据的修改,即使其他事务已提交。也就可以确保同一事务多次查询的结果一致,但是其他事务新插入的数据,是可以感知到的。这也就引发了幻读问题。可重复读,是 MySQL 的默认事务隔离级别
比如此级别的事务正在执行时,另一个事务成功的插入了某条数据,但因为它每次查询的结果都是一样的,所以会导致查询不到这条数据,自己重复插入时又失败(因为唯一约束的原因)。明明在事务中查询不到这条信息,但自己就是插入不进去,这个现象叫幻读
- 串行化(SERIALIZABLE):序列化,事务最高隔离级别。它会强制事务排序,使之不会发生冲突,从而解决了脏读、不可重复读和幻读问题,但因为执行效率低,所以真正使用的场景并不多
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交(READ UNCOMMITTED) | √ | √ | √ |
读已提交(READ COMMITTED) | × | √ | √ |
可重复读(REPEATABLE READ) | × | × | √ |
串行化(SERIALIZABLE) | × | × | × |
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
事务隔离级别枚举类
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)
@RequestMapping("/r3")
public String r3(String name, String password) throws IOException {
//... 代码省略
return "r3";
}
Spring 事务传播机制
什么是事务传播机制
事务传播机制就是:多个事务方法存在调用关系时,事务是如何在这些方法间进行传播的
事务隔离级别解决的是多个事务同时调用一个数据库的问题
事务传播机制解决的是一个事务在多个节点(多个方法)之间的传递问题
事务传播机制有哪些
@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
概念解释:
比如一对新人要结婚了,关于是否需要房子:
Propagation.REQUIRED
:需要有房子。如果你有房,我们就一起住,如果你没房,我们就一起买房。(如果当前存在事务,则加入该事务。如果当前没有事务,则创建一个新的事务)Propagation.SUPPORTS
:可以有房子。如果你有房,那就一起住。如果没房,那就租房。(如果当前存在事务,则加入该事务。如果当前没有事务,则以非事务的方式继续运行)Propagation.MANDATORY
:必须有房子。要求必须有房,如果没房就不结婚。(如果当前存在事务,则加入该事务。如果当前没有事务,则抛出异常)Propagation.REQUIRES_NEW
:必须买新房。不管你有没有房,必须要两个人一起买房。即使有房也不住。(创建一个新的事务。如果当前存在事务,则把当前事务挂起)Propagation.NOT_SUPPORTED
:不需要房。不管你有没有房,我都不住,必须租房。(以非事务方式运行,如果当前存在事务,则把当前事务挂起)Propagation.NEVER
:不能有房子。(以非事务方式运行,如果当前存在事务,则抛出异常)Propagation.NESTED
:如果你没房,就一起买房。如果你有房,我们就以房子为根据地,做点小生意。(如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行。如果当前没有事务,则该取值等价于PROPAGATION_REQUIRED
)
Spring事务传播机制使用场景
REQUIRED(加入事务)
看下面代码实现:
- 用户注册,插入一条数据
- 记录操作日志,插入一条数据(出现异常)
观察propagation = Propagation.REQUIRED
的执行结果
@RequestMapping("/propagar")
@RestController
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";
}
}
对应的 UserService
和 LogService
都添加上 @Transactional(propagation = Propagation.REQUIRED)
@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的数据也回滚了。
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
中的事务。
当我们不希望事务之间相互影响时,可以使用该传播行为。
NEVER(不支持当前事务,抛异常)
修改 UserService
中对应方法的事务传播机制为 Propagation.NEVER
@Service
public class UserService {
@Autowired
private UserInfoMapper userInfoMapper;
@Transactional(propagation = Propagation.NEVER)
public void registryUser(String name, String password) {
// 插入用户信息
userInfoMapper.insert(name, password);
}
}
程序执行报错,没有数据插入。
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
出现异常之后,往上找调用它的方法和事务,所以用户注册也失败了 - 最终结果是两个数据都没有添加
NESTED 和 REQUIRED 有什么区别?
我们在 LogService
进行当前事务回滚,修改 LogService
代码如下:
@Service
public class LogService {
@Autowired
private LogInfoMapper logInfoMapper;
@Transactional(propagation = Propagation.NESTED)
public void insertLog(String name, String op) {
try {
int a = 10 / 0;
} catch (Exception e) {
// 回滚当前事务
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
// 记录用户操作
logInfoMapper.insertLog(name, "用户注册");
}
}
重新运行程序,发现用户表数据添加成功,日志表添加失败。
LogService
中的事务已经回滚,但是嵌套事务不会回滚嵌套之前的事务,也就是说嵌套事务可以实现部分事务回滚。
对比 REQUIRED
,把 NESTED
传播机制改为 REQUIRED
,修改代码如下:
@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) {
try {
int a = 10 / 0;
} catch (Exception e) {
// 回滚当前事务
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
// 记录用户操作
logInfoMapper.insertLog(name, "用户注册");
}
}
重新运行程序,发现用户表和日志表的数据都添加失败了。
REQUIRED
如果回滚就是回滚所有事务,不能实现部分事务的回滚(因为属于同一个事务)。
NESTED 和 REQUIRED 区别:
- 整个事务如果全部执行成功,二者的结果是一样的。
- 如果事务一部分执行成功,
REQUIRED
加入事务会导致整个事务全部回滚,NESTED
嵌套事务可以实现局部回滚,不影响上一个方法中执行的结果。
嵌套事务之所以能够实现部分事务的回滚,是因为事务中有一个保存点(savepoint
)的概念,嵌套事务进入之后相当于新建了一个保存点,而滚回时只回滚到当前保存点。
REQUIRED
是加入到当前事务中,并没有创建事务的保存点,因此出现了回滚就是整个事务回滚,这就是嵌套事务和加入事务的区别。