源码位置:transaction
1. 事务回顾
在数据库阶段,想必大家都已经学习过事务了。当多个操作要么一起成功,要么一起失败的时候就需要将多个操作放在同一个事务中。
举个例子:比如用户A给用户B转账100元的业务,需要把用户A的余额-100,并且用户B的余额+100,这两个操作对应着数据库的两条SQL语句,两条SQL语句可以放入事务中,要么一起成功(提交)一起失败(回滚)。
MySQL中的事务无非围绕着这三个SQL语句:
-- 开启事务
start transaction;
-- 提交事务
commit;
-- 回滚事务
rollback;
在数据库阶段主要以理解事务的概念为主,然而在实际的开发中,并不是简单的通过事务来处理,因此我们要学习Spring中的事务操作。
2. Spring中事务的实现
Spring中的事务操作分为两类:
- 编程式事务(手动写代码操作事务)
- 声明式事务(利用注解自动开启和提交事务)
2.1 前置操作
在学习事务之前,先准备需要使用的库和表,SQL脚本如下:
drop database if exists trans;
create database trans default character set utf8mb4;
use trans;
drop table if exists userinfo;
create table userinfo (
`id` int not null auto_increment,
`username` varchar(20) not null,
`password` varchar(30) 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 = '用户表';
通过普通的MyBatis插入几条数据:
实体类 UserInfo:
@Data
public class UserInfo {
private Integer id;
private String username;
private String password;
private Timestamp createTime;
private Timestamp updateTime;
}
控制层 UserController:
@RequestMapping("/user")
@RestController
@Slf4j
public class UserController {
@Autowired
UserService userService;
@RequestMapping("/registry")
public String registry(UserInfo userInfo) {
Integer result = userService.insertUser(userInfo);
log.info("插入了" + result + "条数据~");
return "注册成功";
}
}
服务层 UserService:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public Integer insertUser(UserInfo userInfo) {
return userMapper.insertUser(userInfo);
}
}
mapper层 UserMapper:
@Mapper
public interface UserMapper {
Integer insertUser(UserInfo userInfo);
}
UserMapper.xml实现接口
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.chenshu.transaction.mapper.UserMapper">
<insert id="insertUser">
insert into userinfo
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="username != null">
username,
</if>
<if test="password != null">
password,
</if>
<if test="createTime != null">
creat_time,
</if>
<if test="updateTime != null">
update_time,
</if>
</trim>
values
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="username != null">
#{username},
</if>
<if test="password != null">
#{password},
</if>
<if test="createTime != null">
#{createTime},
</if>
<if test="updateTime != null">
#{updateTime},
</if>
</trim>
</insert>
</mapper>
通过url
插入几条数据:
通过几次url访问,成功插入以下数据:
2.1 编程式事务(了解)
Spring手动操作事务和上面MySQL操作事务类似,有3个重要操作步骤:
- 获取事务状态
- 回滚事务
- 提交事务
2.1.1 获取事务状态
编程式事务有两个重要的类:
- 事务管理器:
org.springframework.jdbc.datasource.DataSourceTransactionManager
- 事务属性的接口:
org.springframework.transaction.TransactionDefinition
、
在Controller层依赖注入这两个类:
public class UserController {
@Autowired
private DataSourceTransactionManager dataSourceTransactionManager;
@Autowired
private TransactionDefinition transactionDefinition;
通过事务管理器DataSourceTransactionManager
的getTransaction()
方法,在方法里传入TransactionDefinition
可以拿到一个TransactionStatus
代表当前事务状态的类。
TransactionStatus transaction = dataSourceTransactionManager.getTransaction(transactionDefinition);
通过TransactionStatus
可以对事务进行回滚 or 提交
2.1.2 事务回滚
@RequestMapping("/registry")
public String registry(UserInfo userInfo) {
//获取事务状态
TransactionStatus transaction = dataSourceTransactionManager.getTransaction(transactionDefinition);
Integer result = userService.insertUser(userInfo);
log.info("插入了" + result + "条数据~");
//回滚事务
dataSourceTransactionManager.rollback(transaction);
return "注册成功";
}
再次通过url调用该方法:
由于事务回滚,表里没有插入数据:
2.1.3 事务提交
@RequestMapping("/registry")
public String registry(UserInfo userInfo) {
//获取事务状态
TransactionStatus transaction = dataSourceTransactionManager.getTransaction(transactionDefinition);
Integer result = userService.insertUser(userInfo);
log.info("插入了" + result + "条数据~");
//提交事务
dataSourceTransactionManager.commit(transaction);
return "注册成功";
}
再次输入url来调用注册方法:
由于事务提交,表中成功插入了数据:
我们发现用户wangwu此时的自增 id 为5,证明上次的SQL语句即使回滚了,还是留下了执行SQL的痕迹。
既然执行过SQL又是如何进行回滚的呢?接下来我们就来讲讲事务状态TransactionStatus
这个对象做了什么。
2.1.4 事务状态的解释
观察事务的回滚和提交的日志信息,并对比差别:
回滚事务的日志:
//创建事务会话
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2014a533]
//从连接池获取连接
JDBC Connection [HikariProxyConnection@207488314 wrapping
//执行SQL语句
com.mysql.cj.jdbc.ConnectionImpl@135f132a] will be managed by Spring
==> Preparing: insert into userinfo ( username, password ) values ( ?, ? )
==> Parameters: wangwu(String), 123(String)
<== Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2014a533]
2024-04-26 22:10:19.758 INFO 10561 --- [nio-8080-exec-1] c.c.t.controller.UserController : 插入了1条数据~
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2014a533]
//关闭事务会话
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2014a533]
提交事务的日志:
//创建事务会话
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@599475b7]
//从连接池获取连接
JDBC Connection [HikariProxyConnection@1632144075 wrapping
//执行SQL语句
com.mysql.cj.jdbc.ConnectionImpl@37fb4a40] will be managed by Spring
==> Preparing: insert into userinfo ( username, password ) values ( ?, ? )
==> Parameters: wangwu(String), 123(String)
<== Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@599475b7]
2024-04-26 22:02:44.931 INFO 10517 --- [nio-8080-exec-1] c.c.t.controller.UserController : 插入了1条数据~
//提交事务
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@599475b7]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@599475b7]
//关闭事务会话
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@599475b7]
对比二者发现提交事务的连接多了一个提交事务的操作。
数据库中是类似这样处理的:
- 当获取事务状态时,相当于开启了事务,数据库此时会记录一个时间点
TransactionStatus transaction = dataSourceTransactionManager.getTransaction(transactionDefinition);
- 执行SQL语句
- 通过
dataSourceTransactionManager
事务管理器进行rollback()
orcommit()
rollback()
: 不提交事务,将事务标记为已回滚状态后关闭事务会话,并回滚到开启事务的时间点;commit()
: 提交事务,并将事务标记为已提交状态后关闭事务会话
2.2 声明式事务(推荐)
为了简化编程式事务,Spring新增了声明式事务的方式。
使用一个@Transactional
注解声明方法:
@RequestMapping("/registry")
@Transactional
public String registry(UserInfo userInfo) {
Integer result = userService.insertUser(userInfo);
log.info("插入了" + result + "条数据~");
return "注册成功";
}
2.2.1 @Transactional 作用
@Transactional
可以用来修饰方法或类:
- 修饰方法:只有修饰public方法时才生效(修饰其他方法时不会报错,也不生效)【推荐】
- 修饰类:类中所有的public方法都生效
当方法/类被该注解修饰后,它会做下面三件事:
- 在方法执行前开启事务
- 执行目标方法
- 如果未出现异常或异常被程序捕获就提交事务,如果出现异常并没被捕获就回滚事务(不完全对,马上就讲)
2.2.2 @Transactional
的常用属性
value
和transactionManager
:这两个属性的作用相同,对应事务管理器的名称,分库分表时会用到
@AliasFor("transactionManager")
String value() default "";
@AliasFor("value")
String transactionManager() default "";
propagation
和isolation
:代表传播机制和隔离级别,后面详细介绍
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
【引入】回滚相关属性
与回滚相关的属性有下面四个:
Class<? extends Throwable>[] rollbackFor() default {};
String[] rollbackForClassName() default {};
Class<? extends Throwable>[] noRollbackFor() default {};
String[] noRollbackForClassName() default {};
分别解释四个参数:
rollbackFor
:里面传的是异常的.class对象,抛出符合条件异常就回滚rollbackForClassName
:里面传异常的类名,抛出符合条件异常就回滚noRollbackfor
:里面传的是异常的.class对象,抛出符合条件异常不回滚noRollbackForClassName
:里面传异常的类名,抛出符合条件异常不回滚
注:符合条件指的是抛出该异常或该异常的子类
如果@Transactional
注解没有添加上面任何属性值,那么只有在运行时异常和Error才会进行回滚
举个例子,如果想抛出所有异常都回滚,可以这样写:
@RequestMapping("/registry")
@Transactional(rollbackFor = {Exception.class})
public String registry(UserInfo userInfo) {
Integer result = userService.insertUser(userInfo);
log.info("插入了" + result + "条数据~");
return "注册成功";
}
前面提到如果异常被捕获,就会提交事务,那如果发生异常后被捕获,也希望事务回滚,可以通过下面这两种方式回滚事务:
- 继续把异常抛出去
@RequestMapping("/registry")
@Transactional(rollbackFor = {Exception.class})
public String registry(UserInfo userInfo) {
Integer result = userService.insertUser(userInfo);
log.info("插入了" + result + "条数据~");
try {
int a = 10/0;
} catch (Exception e) {
log.warn("发生了异常..");
throw e;
}
return "注册成功";
}
- 手动设置回滚
@RequestMapping("/registry")
@Transactional(rollbackFor = {Exception.class})
public String registry(UserInfo userInfo) {
Integer result = userService.insertUser(userInfo);
log.info("插入了" + result + "条数据~");
try {
int a = 10/0;
} catch (Exception e) {
log.warn("发生了异常..");
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return "注册成功";
}
2.2.3 Spring 事务隔离级别
理解Spring的隔离级别是以了解SQL标准中的事务隔离级别为基础的,如果对此部分内容有所遗忘可以点击这里:数据库的事务的并发问题和四种隔离级别
MySQL默认的隔离级别是可重复读
通过下面的SQL可以查看MySQL设置的隔离级别
select @@global.tx_isolation,@@tx_isolation;
Spring中事务隔离级别有5种:对比SQL标准中的隔离级别多了一个DEFAULT
Isolation.DEFAULT
:以连接的数据库的事务隔离级别为主Isolation.READ_UNCOMMITTED
:读未提交Isolation.READ_COMMITTED
:读已提交Isolation.REPEATABLE_READ
:可重复读Isolation.SERIALIZABLE
:串行化
Spring的事务隔离级别是通过@Transational
注解的isolation
属性来设置的:
2.2.4 Spring 事务传播机制
事务传播机制是Spring新增加的概念,在数据库中是不存在的。
它描述的内容是:多个事务方法存在调用关系时,事务是如何在这些方法之间进行传播的。
假设方法A调用方法B,在方法B上设置不同的事务传播级别程序会 进行不同的处理。事务传播机制的设置是通过@Transational
注解的propagation
属性来设置的,属性值如下:
Propagation.REQUIRED
: 需要事务 (默认的事务传播级别),如果方法A存在事务,则方法B加⼊该事务;如果方法A没有事务,则方法B创建⼀个新的事务。
2. Propagation.SUPPORTS
: 支持事务 如果方法A存在事务,则方法B加⼊该事务;如果方法A没有事务,则方法B以⾮事务的⽅式运⾏。
-
Propagation.MANDATORY
:强制性事务 如果方法A存在事务,则方法B加⼊该事务;如果方法A没有事务,则抛出异常。 -
Propagation.REQUIRES_NEW
: 需要新事务 如果方法A存在事务,则把当前事务挂起;如果方法A不存在事务,方法B就创建新事务。也就是说不管外部⽅法是否开启事务, 方法B都会新开启⾃⼰的事务,且开启的事务相互独立,互不干扰。 -
Propagation.NOT_SUPPORTED
: 不支持事务 如果方法A存在事务,则把当前事务挂起(不⽤),方法B以非事务的方式运行;如果方法A不存在事务,方法B以非事务的方式运行。也就是说不管外部⽅法是否开启事务,方法B都会以事务的方式运行。
6.Propagation.NEVER
: 非事务 以⾮事务⽅式运⾏,如果当前存在事务,则抛出异常。
7.Propagation.NESTED
: 嵌套事务 如果方法A存在事务,则方法B创建⼀个事务作为当前事务的嵌套事务来运⾏;如果方法A没有事务,则该取值等价于 PROPAGATION_REQUIRED
。
接下来我将通过代码演示加入事务、挂起事务、嵌套事务的区别,前置工作如下:
SQL脚本:
drop table if exists log_info;
create table log_info (
`id` int primary key auto_increment,
`username` varchar(20) not null,
`op` varchar(256) not null,
`create_time` datetime default now(),
`update_time` datetime default now() ON UPDATE now()
) default charset 'utf8mb4'
LogMapper
:映射日志表的新增SQL
@Mapper
public interface LogMapper {
@Insert("insert into log_info(username, op) values (#{username},#{op})")
Integer insertLog(LogInfo logInfo);
}
LogService
:新增日志表记录的业务
@Service
public class LogService {
@Autowired
private LogMapper logMapper;
@Transactional
public Integer insertLog(LogInfo logInfo) {
Integer ret = logMapper.insertLog(logInfo);
return ret;
}
}
PropaController
:分别调用 userService
和 logService
的方法
@RequestMapping("/propa")
@RestController
public class PropaController {
@Autowired
private LogService logService;
@Autowired
private UserService userService;
@Transactional
@RequestMapping("/p1")
public String p1(UserInfo userInfo) {
userService.insertUser(userInfo);
LogInfo logInfo = new LogInfo();
logInfo.setUsername(userInfo.getUsername());
logInfo.setOp("用户主动注册");
logService.insertLog(logInfo);
return "注册成功";
}
}
【解释】加入事务和挂起事务的区别
这里我将通过Propagation.REQUIRED
和Propagation.REQUIRES_NEW
两种隔离级别来演示。
1. 两个被调用的方法都使用Propagation.REQUIRED
隔离级别,并且有一个方法出现异常的情况
我将UserService
和LogService
的事务隔离级别设置为Propagation.REQUIRED
,并在LogService
中构造算术异常:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Transactional(propagation = Propagation.REQUIRED)
public Integer insertUser(UserInfo userInfo) {
return userMapper.insertUser(userInfo);
}
}
@Service
public class LogService {
@Autowired
private LogMapper logMapper;
@Transactional(propagation = Propagation.REQUIRED)
public Integer insertLog(LogInfo logInfo) {
Integer ret = logMapper.insertLog(logInfo);
//构造算术异常
ing a = 10/0;
return ret;
}
}
此时我将userinfo
以及log_info
表中的所有数据清空,并在浏览器的url中输入http://localhost:8080/propa/p1?username=admin&password=123 ,看看会是什么结果:
结论: 由于此时UserService
和LogService
中的两个方法设置的传播机制都是Propagation.REQUIRED
,因此直接加入PropaController
的p1
事务,而由于LogService
的方法出现算术异常,因此整个事务一起回滚,所有两个表中什么数据都没有。
2. 两个被调用的方法都使用Propagation.NESTED
隔离级别,并且有一个方法出现异常的情况
我将UserService
和LogService
的事务隔离级别设置为Propagation.REQUIRES_NEW
,并在浏览器的url中输入http://localhost:8080/propa/p1?username=admin&password=123 ,再次进行测试:
结论: 由于此时UserService
和LogService
中的两个方法设置的传播机制都是Propagation.REQUIRED_NEW
,因此将PropaController
的p1
事务挂起,并开启两个新的事务,所有事务之间相互独立,互不干扰,因此插入userinfo
的事务成功提交,插入log_info
的事务独自回滚。
【解释】加入事务和嵌套事务的区别
这里我将通过Propagation.NESTED
隔离级别来演示。
其实加入事务和嵌套事务差不多,只不过在手动回滚的处理下嵌套事务可以进行单独回滚。
1. 不手动设置回滚的情况
将UserService
和LogService
的事务隔离级别设置为propagation = Propagation.NESTED
,并在LogService
中构造算术异常并且不捕获异常
@Transactional(propagation = Propagation.REQUIRED)
public Integer insertLog(LogInfo logInfo) {
Integer ret = logMapper.insertLog(logInfo);
int a = 10/0;
return ret;
}
此时我将userinfo
以及log_info
表中的所有数据清空并在浏览器的url中输入http://localhost:8080/propa/p1?username=admin&password=123 进行测试:
结论: 在没有自行进行回滚事务的时候,整个事务一起回滚,与Propagation.REQUIRED
是一样的。
2. 手动设置回滚的情况
将UserService
和LogService
的事务隔离级别设置为propagation = Propagation.NESTED
,并在LogService
中构造算术异常并在捕获后手动设置回滚
@Transactional(propagation = Propagation.REQUIRED)
public Integer insertLog(LogInfo logInfo) {
Integer ret = logMapper.insertLog(logInfo);
try {
int a = 10/0;
} catch (Exception e) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return ret;
}
此时我将userinfo
以及log_info
表中的所有数据清空并在浏览器的url中输入http://localhost:8080/propa/p1?username=admin&password=123 进行测试:
结论: 在自行回滚事务的时候,嵌套事务的传播机制使插入log_info
的事务单独回滚,而不影响其他事务。