什么是 Spring 事务传播机制?
Spring 事务传播机制是指,包含多个事务的方法在相互调用时,事务是如何在这些方法间传播的。
例如:methodA
事务方法调用methodB
事务方法时,methodB
是继承methodA
的事务运行呢,还是为自己开启一个新事务运行,这就是由methodB
的事务传播行为决定的。
Spring 的事务传播机制
Spring 事务传播机制使用 @Transactional(propagation=Propagation.REQUIRED)
来定义,Spring 一共定义了 7 中传播行为:
REQUIRED
:默认的事务传播级别。如果当前没有事务,Spring 会创建一个新的事务;如果已经存在事务,则加入该事务。这意味着被调用的方法会在调用者的事务上下文中运行。SUPPORTS
:如果当前存在事务,方法会在这个事务中运行;如果没有事务,则方法在非事务的环境中执行。MANDATORY
:如果当前存在事务,则加入该事务;如果当前没有事务,它会抛出异常。这种传播行为要求调用方法必须在一个现有的事务中运行。REQUIRES_NEW
:总会创建一个新的事务,如果当前存在事务,则将当前事务挂起。也就是说不管外部方法是否开启事务,REQUIRES_NEW
修饰的方法都会新开启自己的事务,且开启的事务相互独立,互不干扰。NOT_SUPPORTED
:以非事务方式运行,如果当前存在事务,则把当前事务挂起。NEVER
:以非事务方式运行,如果当前存在事务,它会抛出异常。这种传播行为确保方法永远不在事务环境中运行。NESTED
:如果当前存在事务,则在嵌套事务内运行。如果当前事务回滚,嵌套事务也会回滚,但嵌套事务的回滚不会导致当前事务回滚。如果当前没有事务,则其行为像REQUIRED。
七种传播行为,可以分为三大类,如下图:
现在我们来详细看看这七种传播行为。为了下面的演示,我们需要新建两张表 :
create table user(
`id` int(11) NOT NULL AUTO_INCREMENT comment 'id',
name varchar(100) not null comment '用户名',
PRIMARY KEY (id)
)ENGINE=InnoDB AUTO_INCREMENT=10000 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=DYNAMIC comment '用户信息表';
create table blog(
`id` int(11) NOT NULL AUTO_INCREMENT comment 'id',
title varchar(100) not null comment '标题',
PRIMARY KEY (id)
)ENGINE=InnoDB AUTO_INCREMENT=10000 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=DYNAMIC comment '博客信息表';
表够简单吧。^_^....
利用如下代码来验证:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private BlogService blogService;
// 这里添加不同的事务传播行为
@Transactional(propagation = Propagation.XXXX)
public void addUser() {
User user = new User();
user.setName("大明哥");
userMapper.insertSelective(user);
blogService.addBlog();
}
}
@Service
public class BlogService {
@Autowired
private BlogMapper blogMapper;
// 这里添加不同的事务传播行为
@Transactional(propagation = Propagation.XXXX)
public void addBlog() {
Blog blog = new Blog();
blog.setTitle("Java 面试 600 讲");
blogMapper.insertSelective(blog);
}
}
由于需要观察 Spring 管理事务的日志,需要将 Spring 日志相关的级别调整为 DEBUG,如下:
<Logger name="org.springframework.transaction.interceptor" level="DEBUG"/>
大明哥分析前面 5 种,NEVER
和 NESTED
就不分析了,使用场景没有相对会少些。
REQUIRED
如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
例如 :
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
// 调用 B
methodB();
}
@Transactional(propagation = Propagation.REQUIRED)
public void methodB() {
}
如果我们单独执行 methodB()
,由于当前上下文不存在事务,则会开启一个新的事务。如果我们调用 methodA()
,由于当前上下文不存在事务,所以会开启一个新的事物,再调用 methodB()
,methodB()
发现当前上下文已经存在了事务,则会加入到 methodA()
的事务中。
我们将上面的示例代码的传播行为调整为 REQUIRED
,执行代码,打印如下日志:
- 1、表示创建了一个事务,传播行为为
PROPAGATION_REQUIRED
,事务隔离级别为ISOLATION_DEFAULT
。addUser()
开启事务。 - 2、表示加入已经存在的事务。
addUser()
创建了事务,然后调用addBlog()
,addBlog()
发现当前已经有事务了 ,直接加入即可 - 3、提交事务
如果在方法执行过程中发生了异常,并且这个异常被标记为触发回滚,整个事务都会回滚。比如我们在 addBlog()
方法中加入 int i = 1 / 10;
,如下:
@Transactional(propagation = Propagation.REQUIRED)
public void addBlog() {
Blog blog = new Blog();
blog.setTitle("Java 面试 600 讲");
int i = 1 / 0;
blogMapper.insertSelective(blog);
}
执行,日志如下:
addUser()
在正常提交,但是 addBlog()
抛出 ArithmeticException
异常,导致整个事务回滚。
我们知道异常分为运行时异常(RuntimeException
)、检查型异常(Exception
)和错误(Error
),如果我们不使用 rollbackFor
或者 noRollbackFor
来定义事务回滚行为,那么 Spring 事务只会在遇到运行时异常(RuntimeException
)和错误(Error
)时触发回滚。我们将 addBlog()
调整为这样:
@Transactional(propagation = Propagation.REQUIRED)
public void addBlog() throws Exception {
Blog blog = new Blog();
blog.setTitle("Java 面试 600 讲");
blogMapper.insertSelective(blog);
throw new Exception("抛个异常玩玩..");
}
执行,日志如下:
抛出 Exception
,事务依然正常提交。
SUPPORTS
如果当前存在事务,方法会在这个事务中运行;如果没有事务,则方法在非事务的环境中执行。
例如:
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
// 调用 B
methodB();
}
@Transactional(propagation = Propagation.SUPPORTS)
public void methodB() {
}
如果我们单纯调用 methodB()
,则它会以非事务的方式运行。如果我们调用 methodA()
,由于 methodA()
有事务,所以 methodB()
会加入到 methodA()
的使用中运行。
下面我们实战演示下。将 addBlog()
的事务传播行为调整为 SUPPORTS
:
@Transactional(propagation = Propagation.SUPPORTS)
public void addBlog() throws Exception {
//...
}
我们单独调用下 addBlog()
,日志如下:
没有任务事务相关的日志。
我们在通过 addUser()
来调用 addBlog()
,日志如下:
对于事务回滚,如果没有事务的话,及时出现了异常也不会回滚。但是如果有事务的话,则回滚由异常类型和 rollbackFor
配置相关。
MANDATORY
如果当前存在事务,则加入该事务;如果当前没有事务,它会抛出异常。他要求调用方必须在一个现有的事务中运行。
例如:
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
// 调用 B
methodB();
}
@Transactional(propagation = Propagation.MANDATORY)
public void methodB() {
}
如果我们单独调用 methodB()
,则会抛出 IllegalTransactionStateException
异常。如果通过 methodA()
调用 methodB()
,由于 methodA()
存在事务中,所以 methodB()
会加入当前事务中。
我们来实例演示下,将 addBlog()
的事务传播行为调整为 MANDATORY
。
@Transactional(propagation = Propagation.MANDATORY)
public void addBlog() throws Exception {
//...
}
先单独调用 addBlog()
,运行日志如下:
调用方法没有事务,则抛出 IllegalTransactionStateException
异常。
我们在通过 addUser()
来调用,运行日志如下:
addUser()
有事务,addBlog()
加入到 addUser()
中的事务来。
REQUIRES_NEW
总会创建一个新的事务,如果当前存在事务,则将当前事务挂起。也就是说不管外部方法是否开启事务,
REQUIRES_NEW
修饰的方法都会新开启自己的事务,且开启的事务相互独立,互不干扰。
例如:
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
// 调用 B
methodB();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
}
无论你怎么调用 methodB(),它都会新开一个自己的事务。
下面代码演示下。我们将 addBlog()
的事务传播行为调整为 REQUIRES_NEW
:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void addBlog() throws Exception {
// ...
}
单独调用 addBlog()
,日志如下:
单独调用它会创建一个新的事务,传播行为为 REQUIRES_NEW
。
如果通过 addUser()
去调用呢?日志如下:
- 1、
addUser()
事务传播行为为REQUIRED
,调用它时由于当前没有事务,所以它会另开一个事务。 - 2、
addUser()
调用addBlog()
,尽管addBlog()
当前处于事务当中,但是由于它的的事务传播行为是REQUIRES_NEW
,所以它会重新开启一个新的事务Suspending current transaction
:表示挂起当前事务 ,即将 addUser() 的事务暂停。creating new transaction with name
:表示创建一个新的事务。
- 3、
addBlog()
提交事务 - 4、
addUser()
提交事务Resuming suspended transaction after completion of inner transaction
:恢复暂停的事务。
上面提到 REQUIRES_NEW
与当前事务是相互独立,互不干扰的。我们让 addBlog()
抛个异常看看 :
你会发现两个事务都回滚了,什么原因?因为 addBlog()
抛了异常后,addUser()
对这个异常没有hold 住,也在抛异常,事务肯定会回滚。那如果 addUser()
抛出异常呢?日志如下:
addBlog()
依然提交了事务,但是 addUser()
回滚了事务。
NOT_SUPPORTED
以非事务方式运行,如果当前存在事务,则把当前事务挂起。
直接演示吧。将 addBlog()
事务传播行为调整为 NOT_SUPPORTED,单独调用 addBlog()
,日志如下:
单独调用,没有事务。如果通过 addUser()
调用呢 ?
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。