[Spring] Spring事务与事务的传播
🌸个人主页:https://blog.csdn.net/2301_80050796?spm=1000.2115.3001.5343
🏵️热门专栏:
🧊 Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm=1001.2014.3001.5482
🍕 Collection与数据结构 (92平均质量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm=1001.2014.3001.5482
🧀线程与网络(96平均质量分) https://blog.csdn.net/2301_80050796/category_12643370.html?spm=1001.2014.3001.5482
🍭MySql数据库(93平均质量分)https://blog.csdn.net/2301_80050796/category_12629890.html?spm=1001.2014.3001.5482
🍬算法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12676091.html?spm=1001.2014.3001.5482
🍃 Spring(97平均质量分)https://blog.csdn.net/2301_80050796/category_12724152.html?spm=1001.2014.3001.5482
感谢点赞与关注~~~
1. 回忆数据库中的事务
https://lilesily12385.blog.csdn.net/article/details/137935719
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';
代码准备:
1. 创建项目,配置文件引入日志打印,驼峰转换,数据库配置等.
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 #配置驼峰⾃动转换
创建实体类:
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;
}
mport lombok.Data;
import java.util.Date;
@Data
public class LogInfo {
private Integer id;
private String userName;
private String op;
private Date createTime;
private Date updateTime;
}
Mapper:
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserInfoMapper {
@Insert("insert into user_info(`user_name`,`password`)values(#{name},#{password})")
Integer insert(String name,String password);
}
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface LogInfoMapper {
@Insert("insert into log_info(`user_name`,`op`)values(#{name},#{op})")
Integer insertLog(String name,String op);
}
Service:
@Slf4j
@Service
public class UserService {
@Autowired
private UserInfoMapper userInfoMapper;
public void registryUser(String name,String password){
//插⼊⽤⼾信息
userInfoMapper.insert(name,password);
}
}
@Slf4j
@Service
public class LogService {
@Autowired
private LogInfoMapper logInfoMapper;
public void insertLog(String name,String op){
//记录⽤⼾操作
logInfoMapper.insertLog(name,"⽤⼾注册");
}
}
2.1 Spring编程式事务
Spring手动操作事务主要分为三步,与Sql事务的操作类似:
- 开启事务
- 提交事务
- 或者回滚事务
@RestController
@RequestMapping("/user")
public class TransactionController {
@Autowired
private UserService userService;
@Autowired
private DataSourceTransactionManager dataSourceTransactionManager;
@Autowired
private TransactionDefinition transactionDefinition;
@RequestMapping("/registry")
public String login(String userName,String password){
//开启事务
TransactionStatus transaction = dataSourceTransactionManager.getTransaction(transactionDefinition);
//用户注册
userService.registryUser(userName,password);
//提交事务
dataSourceTransactionManager.commit(transaction);
//回滚事务
// dataSourceTransactionManager.rollback(transaction);
return "注册成功";
}
}
SpringBoot内置了两个对象:
DataSourceTransactionManager
,数据源事务管理器,一般用于事务的开启回滚和提交.TransactionDefinition
是事务的属性,一般用于传给事务管理器的getTransaction
方法,用于事务的获取与开启.
- 观察事务提交
dataSourceTransactionManager.commit(transaction);
我们观察到数据库,数据被插入成功.
- 观察事务回滚
dataSourceTransactionManager.rollback(transaction);
我们看到虽然返回的结果是注册成功,但是数据库中并没有多出任何数据.
下面我们来学习一种简单而快捷的方法,使用注解声明.
2.2 Spring声明式事务@Transactional
一共有两部操作:
- 添加依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
- 在事务的方法上添加
@Transactional
注解就可以实现了.无需手动开启和提交事务,在事务全部执行完成之后会自动提交,在中途发生异常的手会自动回滚.
@RestController
@RequestMapping("/user2")
public class TransactionController2 {
@Autowired
private UserService userService;
@RequestMapping("/registry2")
@Transactional
public String registry2(String userName,String password){
userService.registryUser(userName,password);
return "注册成功";
}
}
运行程序后,数据插入成功.
修改程序,使之出现异常:
@RestController
@RequestMapping("/user2")
public class TransactionController2 {
@Autowired
private UserService userService;
@RequestMapping("/registry2")
@Transactional
public String registry2(String userName,String password){
userService.registryUser(userName,password);
int i = 10/0;
return "注册成功";
}
}
运行之后,虽然返回了注册成功,但是数据库并没有更新结果.
我们一般写事务的时候会在业务逻辑层来控制事务,因为在业务逻层中,一个业务功能可能会包含多个数据库访问操作,这样就可以把多个访问数据库的操作 合并在同一个事务中.
- 该注解还有一个妙用,就是我们可以在测试类
@Test
上添加上一个@Transactional
注解,如果该测试方法中涉及到数据库相关的操作,那么在运行完成之后就会自动进行回滚,不会对我们的数据库有污染.
2.3 @Transactional的作用
@Transactional
可以用来修饰方法或者是类.
修饰方法的时候,只对public修饰的方法生效,修饰其他方法也不会报错,但是也不会生效.
修饰类的时候,对类中的所有public方法生效.
在程序出现异常的时候,如果异常没有被捕获,这时候事务就会被回滚,但是如果异常被捕获,就会被认为是正常执行,依然会提交事务.
修改上述代码:
@RestController
@RequestMapping("/user2")
public class TransactionController2 {
@Autowired
private UserService userService;
@RequestMapping("/registry2")
@Transactional
public String registry2(String userName,String password){
userService.registryUser(userName,password);
try {
int i = 10/0;
}catch (ArithmeticException e) {
e.printStackTrace();
}
return "注册成功";
}
}
运行程序之后,虽然出错了,由于异常得到了捕获,事务便得到了提交.
以下两种情况,事务依然会回滚:
- 重新抛出异常
try {
int i = 10/0;
}catch (ArithmeticException e) {
throw e;
}
- 手动回滚事务
使用TransactionAspectSupport.currentTransactionStatus()
得到当前事务,并使用setRollbackOnly
回滚.
try {
int i = 10/0;
}catch (ArithmeticException e) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
3. @Transactional详解
我们主要学习@Transactional
注解的三个属性:
1. rollbackFor:异常回滚属性,指定能够触发事务回滚的异常类型.可以指定多个异常类型.
2. Isolation:事务的隔离级别,默认是Isolation.DEFAULT.
3. propagation:事务的传播机制.默认值为propagation.REQUIRED
3.1 rollbackFor
异常回滚的时候,@Transactional
默认在遇到Error或者运行时异常时才会回滚.
接下来我们来使用代码验证:
@Transactional
@RequestMapping("/registry3")
public String registry3(String userName,String password) throws IOException {
userService.registryUser(userName,password);
if (true){
throw new IOException();
}
return "注册成功";
}
向服务器提交数据:
我们看到,虽然抛出了异常,但是数据库的数据仍然被修改了.
如果我们要想指定回滚异常的类型,我们需要通过@Transactional
的rollbackFor属性来完成,给属性传入异常的类对象来实现对回滚异常的指定,
@RequestMapping("/registry3")
@Transactional(rollbackFor = Exception.class)
public String registry3(String userName,String password) throws IOException {
userService.registryUser(userName,password);
if (true){
throw new IOException();
}
return "注册成功";
}
运行程序:
我们看到,事务并没有进行提交,被回滚了,数据库的数据并没有更行.
3.2 事务隔离级别
3.2.1 回顾MySQL中的事务隔离级别
https://lilesily12385.blog.csdn.net/article/details/137935719
3.3.2 Spring事务隔离级别
Spring中事务的隔离级别有5种:
- Isolation.DEFAULT: 以连接数据库的隔离级别为准.
- Isolation.READ_UNCOMMITTED:读未提交
- Isolating.READ_COMMITTED:读提交.
- Isolation.REPEATABLE_READ:可重复读.
- Isolation.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
属性进行设置.
@RequestMapping("/registry3")
@Transactional(isolation = Isolation.DEFAULT)
public String registry3(String userName,String password) throws IOException {
userService.registryUser(userName,password);
return "注册成功";
}
3.3 Spring事务传播机制
3.3.1 概念
事务的传播机制就是:多个事务方法存在调用关系的时候,事务是如何在这些方法之间进行传播的.事务的传播机制就解决的是一个事务在多个节点上(方法)中的传递.
比如有两个方法A和B,他们都被@Transactional
修饰,方法A调用了方法B.A方法运行的时候,会开启一个新的事物,当A调用B的时候,B方法本身也有事务,此时B方法运行的时候,是假如A事务,还是创建一个新的事务呢?下面我们就来介绍一下事务的传播机制.
3.3.2 事务的传播机制分类
@Transactional
注解支持事务的传播机制的设置,我们可以通过propagation
属性来设置传播行为.
- Propagation.REQUIRED:加入事务,默认的事务传播级别.如果当前存在事务,则加入该事务,如果当前没有事务,则创建一个新的事务.(加入事务,就是共用一个事务,一个事务发生异常,全部回滚.)
- Propagation.SUPPORTS:如果当前存在事务,则加入该事务.如果当前没有事务,则以非事务的方式继续运行.
- Propagation.MANDATORY:强制性,如果当前存在事务,则加入该事务,如果当前没有事务,则抛出异常.
- Propagation.REQUIERS_NEW:新建事务,创建一个新事务如果当前存在事务,则把当前事务挂起.(发生异常,不影响其他事务)也就是不管外部方法是否开启事务,
Propagation.REQUIERS_NEW
修饰的内部方法都会新开启自己的事务,且开启的事务相互独立,互不干涉. - Propagation.NOT_SUPPORTED: 以非事务的方式运行,如果当前存在事务则把当前事务挂起.
- Propagation.NEVER: 不支持当前事务,以非事务的方式运行,如果存在事务,则抛出异常.
- Propagation.NESTED: 嵌套事务,如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行,如果当前没有事务,则该取值等价于
Propagation.REQUIRED
.
1,4对应,2,5对应,3,6对应.
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.REQUIRES: 如果你有房子,就住你的房子(加入事务),如果你没有房子,我们就一起买房子(创建一个新事务).
- Propagation.SUPPORTS: 如果你有房子,我们就住你的房子(加入事务),如果没有房子,我们就租房子(以非事务的方式运行).
- Propagation.MANDATORY: 要求必须有房子(加入事务),如果没有房子,就不结婚(抛出异常)
- Propagation.REQUIERS_NEW: 必须买新房,不管你有没有房子,必须两个人一起买房(创建一个新事务),即使有房也不住(当前事务挂起).
- Propagation.NOT_SUPPORTED: 不管你有没有房子,我都不住(挂起当前事务),必须租房(以非事务的方式运行).
- Propagation.NEVER:不能有房子(当前存在事务),有房子就不结婚(抛出异常).
- Propagation.NESTED :如果你没房,就⼀起买房.如果你有房,我们就以房子为根据地,做点下生意.(如果如果当前存在事务,则创建⼀个事务作为当前事务的嵌套事务来运行.如果当前没有事务,则该取值等价于PROPAGATION_REQUIRED )
3.3.3 Spring事务传播机制代码演示
重点关注两个: REQUIRED,REQUIERS_NEW.
- REQUIRED(加入事务)
用户注册,插入一条数据,并记录操作日志.
@RequestMapping("/r4")
@Transactional
public String registry4(String userName,String password){
userService.registryUser(userName,password);
logService.insertLog(userName,"用户注册");
return "注册成功";
}
@Service
public class UserService {
@Autowired
public 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 i = 10/0;
//记录⽤⼾操作
logInfoMapper.insertLog(name,"⽤户注册");
}
}
我们在执行之后,发现数据库中并没有插入任何数据,这就是因为insertLog
方法发生了异常,事务发生回滚,当事务回滚之后,registry4
方法也发生了回滚,导致了registryUser
也发生了回滚,导致数据库中没有插入数据.
- REQUIRES_NEW(新建事务)
将上面的UserService和LogService的事务传播机制改为Propagation.REQUIRES_NEW.
@RequestMapping("/r4")
@Transactional
public String registry4(String userName,String password){
userService.registryUser(userName,password);
logService.insertLog(userName,"用户注册");
return "注册成功";
}
@Service
public class LogService {
@Autowired
private LogInfoMapper logInfoMapper;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void insertLog(String name,String op){
int i = 10/0;
//记录⽤⼾操作
logInfoMapper.insertLog(name,"⽤户注册");
}
}
@Service
public class UserService {
@Autowired
public UserInfoMapper userInfoMapper;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void registryUser(String name,String password) {
userInfoMapper.insert(name, password);
}
}
在执行之后,我们会发现,日志表中并没有插入新的数据,但是用户表中插入了新的数据,这是由于UserService
,LogService
与registry4
属于不同的事务,LogService
出现异常回滚之后不会影响registry4
和UserService
的执行.
- NEVER(不支持当前事务)
把REQUIRED
代码的UserService
中的对应方法的事务传播机制修改为Propagation.NEVER
.并去掉制造的异常.
@Transactional(propagation = Propagation.NEVER)
public void insertLog(String name,String op){
//记录⽤⼾操作
logInfoMapper.insertLog(name,"用户注册");
}
运行之后,程序抛出异常,日志表和用户表均没有数据插入.
- NESTED(嵌套事务)
将上述REQUIRED
的UserService
中的对应方法的事务传播机制修改为Propagation.NESTED
.
@Transactional(propagation = Propagation.NESTED)
public void insertLog(String name,String op){
int i = 10/0;
//记录用户操作
logInfoMapper.insertLog(name,"用户注册");
}
@Transactional(propagation = Propagation.NESTED)`在这里插入代码片`
public void registryUser(String name,String password) {
userInfoMapper.insert(name, password);
}
运行程序之后,两张表中都没有插入任何数据,如果我们对出现异常的方法进行手动回滚:
@Transactional(propagation = Propagation.NESTED)
public void insertLog(String name,String op){
try {
int i = 10/0;
}catch (Exception e){
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
//记录用户操作
logInfoMapper.insertLog(name,"用户注册");
}
我们看到,用户表插入成功了,但是日志表的数据被回滚了.
区分REQUIRED和NESTED
REQUIRED:当其中一个事务出现异常的时候,所有事务,包括子事务和父事务都会回滚,即只允许全部回滚.
NESTED : 当子事务出现异常的时候,可以只有子事务进行回滚,父事务可以继续执行,即允许部分回滚.
REQUIRED传播:
[ 主事务 ]
|
|-- [ 内部操作 ] → 异常
|
↓
整个事务回滚
NESTED传播:
[ 主事务 ]
|
|-- [ 嵌套事务保存点 ] → 异常
|
↓
仅回滚嵌套部分,主事务继续
面试题:MySQL的事务和Spring的事务的区别(*)
- 事务管理范围
MySQL仅限于数据库操作,有数据库系统管理,保证数据的一致性和完整性,管理范围只有数据库层.Spring的事务不仅仅涵盖数据库操作,还可以涵盖其他的资源(如消息队列,文件管理),进行更加灵活的事务控制,管理范围是应用层.- 事务的隔离级别
支持标准的事务隔离级别(读未提交,读提交,可重复读,串行化),通过sql语句来设置,Spring同样支持这些事务,只不过是在应用层通过注解或者是配置来设置,之后传递给数据库.也可以设置默认以数据库的事务隔离级别为准.- 事务的使用方式
MySQL使用start transaction开启事务,使用commit提交事务,而Spring的事务一般使用@Transactional注解来标注需要开启事务的方法.- 事务的传播行为
在MySQL中没有事务的传播,而在Spring中的事务中,如果一个方法中调用了其他的方法,事务会在这几个方法中按照一定的规则进行传播.
3.4 事务的失效
3.4.1 Spring的事务什么时候失效?
- 方法是private修饰的事务会失效:** Spring的事务是基于AOP实现的,而AOP又是基于代理模式实现的**,如果是private修饰,如果是JDK动态代理代理的是接口,根本不考虑private的情况,如果是CGLIB动态代理,是通过生成目标类的子类对象来完成了代理,则子类无法重写private的方法.
@Transactional
private void saveData() { // 事务失效!
// ...
}
- Transactional默认只对
RuntimeException
和Error
进行回滚,如果是其他类型的异常,默认不会回滚,比如IOExecption
,需要使用rollbackFor属性指定需要回滚的异常类型.
@Transactional
public void save() throws IOException {
// 抛出 IOException 时事务不回滚!
}
- 如果带有事务的方法在当前类中的方法中被调用,则事务不会生效,这是由于这种调用方法不会经过代理对象,而是直接调用了目标方法.
@Service
public class OrderService {
public void createOrder() {
this.deductStock(); // 直接调用,事务失效!
}
@Transactional
public void deductStock() {
// ...
}
}
- 有可能是数据库本身不支持事务,比如MySQL的MyISAM存储引擎.
- 方法本身是
final
或者是static
修饰的.导致动态代理无法覆盖到这样的方法,不能拦截static
方法是因为动态代理是基于对象实例的代理,只能拦截实例方法.而且静态方法是编译器就已经绑定了,是一种静态绑定,而动态代理在运行时才会绑定,是一种动态绑定的机制.由于final
修饰的方法无法被子类重写,因此在CGLIB动态代理中,他是通过生成目标类的子类来创建代理对象,final
修饰方法都无法在子类中重写.而JDK动态代理由于只能重写实现接口的类,而接口中就不可以有final
修饰的方法.
@Transactional
public final void save() { // 事务失效!
// ...
}
3.4.2 怎么解决Spring事务失效
- 首先针对private修饰的方法,事务会失效的问题,可以把方法的修饰改为public.
- 针对异常类型不匹配产生的事务失效,可以使用
rollbackFor
属性来指定需要回滚的异常. - 针对同一类中的方法调用带有事务的方法,可以把被调用的这个方法添加到其他的类,以免AOP中的代理对象被绕过.
- 针对数据库不支持事务,可以使用支持事务的存储引擎,比如MySQL的Innodb引擎.
- 可以把static修饰符或者是final修饰符去掉,避免绕开AOP的动态代理.
4. 事务的实现原理
Spring事务管理的实现原理基于的是AOP(面相切面编程)和事务管理器(TransactionManager)的协同工作,核心是通过动态代理对目标方法进行拦截,在方法执行前后管理事务的生命周期.
4.1 核心组件
@Transactional
: 声明事务的注解,定义事务的传播行为,隔离级别,超时时间等属性.TransactionManager
: 声明管理器的接口TransactionInterceptor
: AOP拦截器,负责在方法调用前后处理事务(开启,提交,回滚)- AOP动态代理: 通过JDK动态代理或者是CGLIB动态代理生成代理对象,拦截
@Transactional
方法.
4.2 实现流程
以下是一个方法调用是的事务处理流程:
- 步骤1: 代理对象创建
- Spring容器启动的时候,扫描带有
@Transactional
的类或者是方法. - 通过
ProxyFactory
创建目标对象的代理(JDK动态代理或CGLIB动态代理)
- Spring容器启动的时候,扫描带有
- 步骤2: 方法拦截
当调用代理对象的方法时:- 拦截器链(TransactionInterceptor)被触发
- 检查当前方法是否需要事务(根据
@Transactional
配置)
- 步骤3: 事务管理
public class TransactionInterceptor extends TransactionAspectSupport {
public Object invoke(MethodInvocation invocation) throws Throwable {
// 1. 获取事务属性(@Transactional 配置)
TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(invocation.getMethod(), targetClass);
// 2. 获取事务管理器(PlatformTransactionManager)
PlatformTransactionManager tm = determineTransactionManager(txAttr);
// 3. 根据传播行为决定是否创建新事务
TransactionStatus status = tm.getTransaction(txAttr);
try {
// 4. 执行目标方法
Object result = invocation.proceed();
// 5. 提交事务
tm.commit(status);
return result;
} catch (Exception ex) {
// 6. 回滚事务(根据异常类型判断)
completeTransactionAfterThrowing(status, txAttr, ex);
throw ex;
}
}
}
-
关键步骤如下:
- 事务传播行为: 通过
TransactionDefinition
控制 - 事务隔离级别: 决定事务的可见性
- 回滚规则: 默认对RuntimeException 和 Error 回滚,可以通过rollbackFor自定义.
- 事务传播行为: 通过
-
总结:Spring事务的实现原理可以归纳为:
- 代理机制: 通过AOP动态代理拦截
@Transactional
方法 - 事务管理: 由
TransactionManager
控制事务的开启,提交和回滚
- 代理机制: 通过AOP动态代理拦截
更多推荐
所有评论(0)