本节目标
-
掌握Spring事务的实现方式
-
掌握事务的传播机制
1. 事务回顾
在数据库阶段, 我们已经学习过事务了.
1.1 什么是事务?
事务是一组操作的集合, 是一个不可分割的操作
事务会把所有的操作作为一个整体, 一起向数据库提交或者是撤销操作请求. 所以这组操作要么同时成功, 要么同时失败.
1.2 为什么需要事务?
我们在进行程序开发时, 也会有事务的需求
比如转账操作:
第一步:A 账户 -100 元. 第二步:B 账户 +100 元
如果没有事务,第一步执行成功了, 第二步执行失败了, 那么A 账户的100 元就平白无故消失了. 如果使 用事务就可以解决这个问题, 让这一组操作要么一起成功, 要么一起失败
1.3 事务的操作
事务的操作主要有三步:
-
开启事start transaction/ begin (一组操作前开启事务)
-
提交事务: commit (这组操作全部成功, 提交事务)
-
回滚事务: rollback (这组操作中间任何一个操作出现异常, 回滚事务)
2. Spring 中事务的实现
前面课程我们讲了MySQL的事务操作, 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-trans, 引入Spring Web, Mybatis, mysql等依赖
-
配置文件
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/trans_test?
characterEncoding=utf8&useSSL=false
username: root
password: root
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 #配
2.1 Spring 编程式事务(了解)
Spring 手动操作事务和上面 MySQL 操作事务类似, 有 3 个重要操作步骤
java
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 "注册成功";
}
}
2.2 Spring 声明式事务 @Transactional
1. 添加依赖
java
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
2. 在需要事务的方法上添加 @Transactional 注解就可以实现了. 无需手动开启事务和提交事 务, 进入方法时自动开启事务, 方法执行完会自动提交事务, 如果中途发生了没有处理的异常会自动 回滚事务
java
@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 "注册成功";
}
}
运行程序, 发现数据插入成功.
修改程序, 使之出现异常
java
@Slf4j
@RequestMapping("/trans")
@RestController
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 注解修饰时, 在目标方法执行开始之前, 会自动开启事务, 方法执行结束 之后, 自动提交事务.
如果在方法执行过程中, 出现异常, 且异常未被捕获, 就进行事务回滚操作
如果异常被程序捕获, 方法就被认为是成功执行, 依然会提交事务
修改上述代码, 对异常进行捕获
java
@Transactional
@RequestMapping("/registry")
public String registry(String name,String password){
//用户注册
userService.registryUser(name,password);
log.info("用户数据插入成功");
//对异常进行捕获
try {
//强制程序抛出异常
int a = 10/0;
}catch (Exception e){
e.printStackTrace();
//throw e;
// 手动回滚事务 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return "注册成功";
}
3. @Transactional 详解
通过上面的代码, 我们学习了 @Transactional 的基本使用. 接下来我们学习 @Transactional 注解的使用细节
我们主要学习 @Transactional 注解当中的三个常见属性:
-
rollbackFor: 异常回滚属性. 指定能够触发事务回滚的异常类型. 可以指定多个异常类型
-
Isolation: 事务的隔离级别. 默认值为 Isolation.DEFAULT
-
propagation: 事务的传播机制. 默认值为 Propagation.REQUIRED
3.1 rollbackFor
@Transactional 默认只在遇到运行时异常和Error时才会回滚, 非运行时异常不回滚. 即 Exception的子类中, 除了RuntimeException及其子类

我们上面为了演示事务回滚, 手动设置了程序异常
java
int a = 10/0;
接下来我们把异常改为如下代码
java
@Transactional
@RequestMapping("/r2")
public String r2(String name,String password) throws IOException {
//用户注册
userService.registryUser(name,password);
log.info("用户数据插入成功");
if (true){
throw new IOException();
}
return "r2";
}

发现虽然程序抛出了异常, 但是事务依然进行了提交

如果我们需要所有异常都回滚, 需要来配置 @Transactional 注解当中的 rollbackFor 属性, 通 过 rollbackFor 这个属性指定出现何种异常类型时事务进行回滚
java
@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";
}
• 在Spring的事务管理中,默认只在遇到运行时异常RuntimeException和Error时才会回滚.
• 如果需要回滚指定类型的异常, 可以通过rollbackFor属性来指定
3.2 事务隔离级别
3.2.1 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
java
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 属性进行设置
java
@Transactional(isolation = Isolation.READ_COMMITTED)
@RequestMapping("/r3")
public String r3(String name,String password) throws IOException {
//... 代码省略
return "r3";
}
3.3 Spring 事务传播机制
3.3.1 什么是事务传播机制
事务传播机制就是: 多个事务方法存在调用关系时, 事务是如何在这些方法间进行传播的
-
Propagation.REQUIRED : 默认的事务传播级别. 如果当前存在事务, 则加入该事务. 如果当前没 有事务, 则创建一个新的事务.
-
Propagation.SUPPORTS : 如果当前存在事务, 则加入该事务. 如果当前没有事务, 则以非事务的 方式继续运行.
-
Propagation.MANDATORY :强制性. 如果当前存在事务, 则加入该事务. 如果当前没有事务, 则 抛出异常.
-
Propagation.REQUIRES_NEW : 创建一个新的事务. 如果当前存在事务, 则把当前事务挂起. 也 就是说不管外部方法是否开启事务, Propagation.REQUIRES_NEW 修饰的内部方法都会新开 启自己的事务, 且开启的事务相互独立, 互不干扰.
-
Propagation.NOT_SUPPORTED : 以非事务方式运行, 如果当前存在事务, 则把当前事务挂起(不 用).
-
Propagation.NEVER: 以非事务方式运行, 如果当前存在事务, 则抛出异常
-
Propagation.NESTED : 如果当前存在事务, 则创建一个事务作为当前事务的嵌套事务来运行. 如果当前没有事务, 则该取值等价于 PROPAGATION_REQUIRED
java
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;
}
}
3.3.2 Spring 事务传播机制使用和各种场景演示
对于以上事务传播机制,我们重点关注以下两个就可以了:
-
REQUIRED(默认值)
-
REQUIRES_NEW
3.3.2.1 REQUIRED(加入事务)
看下面代码实现:
-
用户注册, 插入一条数据
-
记录操作日志, 插入一条数据(出现异常)
观察 propagation = Propagation.REQUIRED 的执行结果
java
@RequestMapping("/propaga")
@RestController
public class PropagationController {
@Autowired
private UserService userService;
@Autowired
private LogService logService;
@Transactional(propagation = Propagation.REQUIRED)
@RequestMapping("/p1")
public String r3(String name,String password){
//用户注册
userService.registryUser(name,password);
//记录操作日志
logService.insertLog(name,"用户注册");
return "r3";
}
}
应的UserService和LogService都添加上 @Transactional(propagation = Propagation.REQUIRED)
java
@Slf4j
@Service
public class UserService {
@Autowired
private UserInfoMapper userInfoMapper;
@Transactional(propagation = Propagation.REQUIRED)
public void registryUser(String name,String password){
//插入用户信息
userInfoMapper.insert(name,password);
}
}
java
@Slf4j
@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,"用户注册");
}
}
运行程序, 发现数据库没有插入任何数据
3.3.2.2 REQUIRES_NEW(新建事务)
将上述UserService 和LogService 中相关方法事务传播机制改为Propagation.REQUIRES_NEW
java
@Service
public class UserService {
@Autowired
private UserInfoMapper userInfoMapper;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void registryUser(String name,String password){
//插入用户信息
userInfoMapper.insert(name,password);
}
}
java
@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,"用户注册");
}
}
运行程序, 发现用户数据插入成功了, 日志表数据插入失败
当我们不希望事务之间相互影响时, 可以使用该传播行为
3.3.2.3 NEVER (不支持当前事务, 抛异常)
修改UserService 中对应方法的事务传播机制为 Propagation.NEVER
java
@Slf4j
@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.2.4 NESTED(嵌套事务)
将上述UserService 和LogService 中相关方法事务传播机制改为 Propagation.NESTED
java
@Slf4j
@Service
public class UserService {
@Autowired
private UserInfoMapper userInfoMapper;
@Transactional(propagation = Propagation.NESTED)
public void registryUser(String name,String password){
//插入用户信息
userInfoMapper.insert(name,password);
}
}
java
@Slf4j
@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事务可以认为是父事务, 嵌套事务是子事务. 父事务出现异常, 子事务也会回滚, 子事务出现异常, 如 果不进行处理, 也会导致父事务回滚.
3.3.2.5 NESTED和REQUIRED 有什么区别
我们在 LogService 进行当前事务回滚, 修改 LogService 代码如下:
java
@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, 修改代码如下:
java
@Service
public class UserService {
@Autowired
private UserInfoMapper userInfoMapper;
@Transactional(propagation = Propagation.REQUIRED)
public void registryUser(String name,String password){
//插入用户信息
userInfoMapper.insert(name,password);
}
}
java
@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嵌套事务可以实 现局部回滚, 不会影响上一个方法中执行的结果
总结
-
Spring中使用事务, 有两种方式: 编程式事务(手动操作)和声明式事务. 其中声明式事务使用较多,在 方法上添加 @Transactional 就可以实现了
-
通过 @Transactional(isolation = Isolation.SERIALIZABLE) 设置事务的隔离级 别. Spring 中的事务隔离级别有 5 种
-
通过 @Transactional(propagation = Propagation.REQUIRED) 设置事务的传播机制, Spring 中的 事务传播级别有 7 种, 重点关注 REQUIRED (默认值) 和 REQUIRES_NEW