聊聊Spring事务控制策略以及@Transactional失效问题避坑
大家好,又见面了。
在大部分涉及到数据库操作的项目里面,事务控制、事务处理都是一个无法回避的问题。比如,需要对SQL执行过程进行事务的控制与处理的时候,其整体的处理流程会是如下的示意:

首先是要开启事务、然后执行具体SQL,如果执行异常则回滚事务,否则提交事务,最后关闭事务,完成整个处理过程。按照这个流程的逻辑,写一下对应的实现代码:
public void testJdbcTransactional(DataSource dataSource) {
Connection conn = null;
int result = 0;
try {
// 获取链接
conn = dataSource.getConnection();
// 禁用自动事务提交,改为手动控制
conn.setAutoCommit(false);
// 设置事务隔离级别
conn.setTransactionIsolation(
TransactionIoslationLevel.READ_COMMITTED.getLevel()
);
// 执行SQL
PreparedStatement ps =
conn.prepareStatement("insert into user (id, name) values (?, ?)");
ps.setString(1, "123456");
ps.setString(2, "Tom");
result = ps.executeUpdate();
// 执行成功,手动提交事务
conn.commit();
} catch (Exception e) {
// 出现异常,手动回滚事务
if (conn != null) {
try {
conn.rollback();
} catch (Exception e) {
// write log...
}
}
} finally {
// 执行结束,最终不管成功还是失败,都要释放资源,断开连接
try {
if (conn != null && !conn.isClosed()) {
conn.close();
}
} catch (Exception e) {
// write log...
}
}
}
不难发现,上面大段的代码逻辑并不复杂,对于业务而言其实仅仅只是执行了一个insert操作而已。但是杂糅的事务控制代码,显然干扰了业务自身的代码处理逻辑的阅读与理解。
常规项目的代码中,涉及到DB处理的场景很多,如果每个地方都有这么一段事务控制的逻辑,那么整体代码的可维护性将会比较差,想想都令人窒息。

好在,JAVA中很多项目现在都是基于Spring框架进行构建的。得益于 Spring框架的封装,业务代码中进行事务控制操作起来也很简单,直接加个 @Transactional注解即可,大大简化了对业务代码的侵入性。那么对 @Transactional事务注解了解的够全面吗?知道有哪些场景可能会导致 @Transactional注解并不会如你预期的方式生效吗?知道应该怎么使用 @Transactional才能保证对性能的影响最小化吗?
下面我们一起探讨下这些问题。
Spring声明式事务处理机制
为了简化业务开发场景对事务的处理复杂度,让开发人员可以更关注于业务自身的处理逻辑,Spring提供了声明式事务的能力支持。
Spring数据库事务约定处理逻辑流程如下图所示,对比前面示例中基于JDBC的事务处理,Spring的事务的处理操作交给了Spring框架处理,开发人员仅需要实现自己的业务逻辑即可,大大简化了事务方面的处理投入。

基于Spring事务机制,实现上述DB操作事务控制的代码,我们的代码会变得非常的简洁:
@Transactional
public void insertUser() {
userDao.insertUser();
}
与JDBC事务实现代码相比,基于Spring的方式只需要添加一个 @Transactional注解即可,代码中只需要实现业务逻辑即可,实现了事务控制机制对业务代码的低侵入性。
Spring支持的基于 Spring AOP实现的声明式事务功能,所谓声明式事务,即使用@Transactional注解进行声明标注,告诉Spring框架在什么地方启用数据库事务控制能力。@Transactional注解,可以添加在类或者方法上。如果其添加在类上时,表明此类中所有的public非静态方法都将启用事务控制能力。
既然@Transactional注解承载了Spring框架对于事务处理的相关能力,那么接下来我们就一起看下该注解的一些可选配置以及具体使用场景。

@Transactional主要可选配置
只读事务配置
通过readonly参数指定当前事务是否为一个只读事务。设置为true标识此事务是个只读事务,默认情况为false。
@Transactional(readOnly = true)
public DomResponse<CiCdItemDetail> queryCicdItemDetail(String appCode) {
return null;
}
这里涉及一个概念,叫做只读事务,其含义描述如下:
在多条查询语句一起执行的场景里面会涉及到的概念。表示在事务设置的那一刻开始,到整个事务执行结束的过程中,其他事务所提交的写操作数据,对该事务都不可见。
举个例子:
现在有一个复合查询操作,包含2条SQL查询操作:先获取用户表count数,再获取用户表中所有数据。
(1) 先执行完获取用户表count数,得到结果10
(2) 在还没开始执行后一条语句的时候,另一个进程操作了DB并往用户表中插入一条新数据
(3) 复合操作的第二条SQL语句,获取用户列表的操作被执行,返回了11条记录
很明显,复合操作中的两条SQL语句获取的数据结果无法匹配上。原因就是非原子性操作导致,即2条查询操作执行的间隔内,有另一个写操作修改了目标读取的数据,导致了此问题的出现。

为了避免此情况的发生,可以给复合查询操作添加上只读事务,这样事务控制范围内,事务外的写操作就不可见,这样就保证了事务内多条查询语句执行结果的一致性。
那为什么要设置为只读事务、而不是常规的事务呢?主要是从执行效率角度的考虑。因为这个里的操作都是一些只读操作,所以设置为只读事务,数据库会为只读事务提供一些优化手段,比如不启动回滚段、不记录回滚log之类的。

回滚条件设定
@Transactional有提供4个不同属性,可以支持传入不同的参数,设定需要回滚的条件:
| 参数 | 含义说明 |
|---|---|
| rollbackFor | 用于指定需要回滚的特定异常类型,可以指定一个或者多个。当指定rollbackFor或者rollbackForClassName之后,方法执行逻辑中只有抛出指定的异常类型,才会触发事务回滚 |
| rollbackForClassName | 与rollbackFor相同,设置字符串格式的类名 |
| noRollbackFor | 用于指定不需要进行回滚的异常类型,当方法中抛出指定类型的异常时,不进行事务回滚。而其余的类型的异常将会触发事务回滚。 |
| noRollbackForClassName | 与noRollbackFor相同,设置字符串格式的类名 |
其中,rollbackFor支持指定单个或者多个异常类型,只要抛出指定类型的异常,事务都将被回滚掉:
// 指定单个异常
@Transactional(rollbackFor = DemoException.class)
public void insertUser() {
// do something here
}
// 指定多个异常
@Transactional(rollbackFor = {DemoException.class, DemoException2.class})
public void insertUser2() {
// do something here
}
rollbackFor和rollbackForClassName作用相同,只是提供了2个不同的指定方法,允许执行Class类型或者ClassName字符串。
// 指定异常名称
@Transactional(rollbackForClassName = {"DemoException"})
public void insertUser() {
// do something here
}
同理,noRollbackFor和noRollbackForClassName的使用与上面示意的相似,只是其含义功能点是相反的。

事务传播行为
propagation用于指定此事务对应的传播类型。所谓的事务传播类型,即当前已经在一个事务的上下文中时,又需要开始一个事务,这个时候来处理这个将要开启的新事务的处理策略。
主要有7种类型的事务传播类型:
| 传播类型 | 含义描述 |
|---|---|
| REQUIRED | 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务 |
| SUPPORTS | 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行 |
| MANDATORY | 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常 |
| REQUIRES_NEW | 创建一个新的事务,如果当前存在事务,则把当前事务挂起 |
| NOT_SUPPORTED | 以非事务方式运行,如果当前存在事务,则把当前事务挂起 |
| NEVER | 以非事务方式运行,如果当前存在事务,则抛出异常 |
| NESTED | 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于REQUIRED |
事务的传播行为,将会影响到事务控制的结果,比如最终是在同一事务中,一旦遇到异常,所有操作都会被回滚掉,而如果是在多个事务中,则某一个事务的回滚,不影响已提交的其余事务的回滚。

实际编码的时候,可以通过@Transactional注解中的 propagation参数来指定具体的传播类型,取值由 org.springframework.transaction.annotation.Propagation枚举类提供。如果不指定,则默认取值为 Propagation.REQUIRED,也即如果当前存在事务,则加入该事务,如果当前没有事务,则创建一个新的事务。
/**
* The transaction propagation type.
* <p>Defaults to {@link Propagation#REQUIRED}.
* @see org.springframework.transaction.interceptor.TransactionAttribute#getPropagationBehavior()
*/
Propagation propagation() default Propagation.REQUIRED;

事务超时设定
可以使用timeout属性来设置事务的超时秒数,默认值为-1,表示永不超时。
@Transactional失效场景避坑
同一个类中方法间调用
Spring的事务实现原理是AOP,而AOP的原理是动态代理。

在类内部方法之间相互调用的时候,本质上是类对象自身的调用,而不是使用代理对象去调用,也就不会触发AOP,这样其实Spring也就无法将事务控制的代码逻辑织入到调用代码流程中,所以这里的事务控制就无法生效。
public void insertUser() {
writeDataIntoDb();
}
@Transactional
public void writeDataIntoDb() {
// ...
}
所以遇到同一个类中多个方法之间相互调用,且调用的方法需要做事务控制的时候需要特别注意下这个问题。解决方式,可以建2个不同的类,然后将方法放到两个类中,这样跨类调用,Spring事务机制就可以生效。
添加在非public方法上
如果将@Transactional注解添加在protected、private修饰的方法上,虽然代码不会有任何的报错,但是实际上注解是不会生效的。
@Transactional
private void writeDataIntoDb() {
// ...
}
方法内部Try Catch吞掉相关异常
这个其实很容易理解,业务代码中将所有的异常给catch并吞掉了,等同于业务代码认为被捕获的异常不需要去触发回滚。对框架而言,因为异常被捕获了,业务逻辑执行都在正常往下运行,所以也不会触发异常回滚机制。
// catch了可能的异常,导致DB操作失败的时候事务不会触发回滚
@Transactional
public void insertUser() {
try {
UserEntity user = new UserEntity();
user.setWorkId("123456");
user.setUserName("王小二");
userRepository.save(user);
} catch (Exception e) {
log.error("failed to create user");
// 直接吞掉了异常,这样不会触发事务回滚机制
}
}
在业务处理逻辑中,如果确实需要知晓并捕获相关处理的异常进行一些额外的业务逻辑处理,如果要保证事务回滚机制生效,最后需要往外抛出 RuntimeException异常,或者是继承RuntimeException实现的业务自定义异常。如下:
// catch了可能的异常,对外抛出RuntimeException或者其子类,可触发事务回滚
@Transactional
public void insertUser() {
try {
UserEntity user = new UserEntity();
user.setWorkId("123456");
user.setUserName("王小二");
userRepository.save(user);
} catch (Exception e) {
log.error("failed to create user");
// @Transactional没有指定rollbackFor,所以抛出RuntimeException或者其子类,可触发事务回滚机制
throw new RuntimeException(e);
}
}
当然,如果@Transactional注解指定了 rollbackFor为某个具体的异常类型,则最终需要保证异常时对外抛出相匹配的异常类型,才可以触发事务处理逻辑。如下:
// catch了指定异常,对外抛出对应类型的异常,可触发事务回滚
@Transactional(rollbackFor = DemoException.class)
public void insertUser() {
try {
UserEntity user = new UserEntity();
user.setWorkId("123456");
user.setUserName("王小二");
userRepository.save(user);
} catch (Exception e) {
log.error("failed to create user");
// @Transactional有指定rollbackFor,抛出异常要与rollbackFor指定异常类型一致
throw new DemoException();
}
}
对应数据库引擎类型不支持事务
以MySQL数据库而言,常见的数据库引擎有 InnoDB和 Myisam等类型,但是MYISAM引擎类型是不支持事务的。所以如果建表时设置的引擎类型设置为 MYISAM的话,即使代码里面添加了@Transactional最终事务也不会生效的。
@Transactional使用策略
因为事务处理对性能会有一定的影响,所以事务也不是说任何地方都可以随便添加的。对于一些性能敏感场景,需要注意几点:
- 仅在必要的场合添加事务控制
(1)不含有DB操作相关,无需添加事务控制
(2)单条查询语句,没必要添加事务控制
(3)仅有查询操作的多条SQL执行场景,可以添加只读事务控制
(4)单条insert/update/delete语句,其实也不需要添加@Transactional事务处理,因为单条语句执行其实数据库有隐性事务控制机制,如果执行失败,是属于SQL报错,数据不会更新成功,自然也无需回滚。
- 尽可能缩小事务控制的代码段处理范围
主要从性能层面考虑,事务机制,类似于并发场景的加锁处理,范围越大对性能影响越明显
- 事务控制范围内的业务逻辑尽可能简单、避免非事务相关耗时处理逻辑
也是从性能层面考虑,尽量将耗时的逻辑放到事务控制之外执行,事务内仅保留与DB操作切实相关的逻辑
总结
好啦,关于Spring中事务控制的相关用法,以及@Transactional使用过程中可能的一些失效场景,就探讨到这里了。那么你对事务这块有哪些自己的理解呢?或者是否有遇到相关的问题呢?欢迎一起交流下咯。

我是悟道,聊技术、又不仅仅聊技术~
如果觉得有用,请点个关注,也可以关注下我的公众号【架构悟道】,获取更及时的更新。
期待与你一起探讨,一起成长为更好的自己。

聊聊Spring事务控制策略以及@Transactional失效问题避坑的更多相关文章
- Spring事务什么时候会失效?
面试官:Spring事务什么时候会失效? 应聘者: 访问权限问题 方法用final修饰 未被Spring管理 错误的传播特性 自己吞了异常 手动抛了别的异常 自定义了回滚异常 方法内部调用 1.访问权 ...
- 聊聊spring事务失效的12种场景,太坑了
前言 对于从事java开发工作的同学来说,spring的事务肯定再熟悉不过了. 在某些业务场景下,如果一个请求中,需要同时写入多张表的数据.为了保证操作的原子性(要么同时成功,要么同时失败),避免数据 ...
- Spring事务专题(五)聊聊Spring事务到底是如何实现的
前言 本专题大纲: 本文为本专题倒数第二篇文章. 在上篇文章中我们一起学习了Spring中的事务抽象机制以及动手模拟了一下Spring中的事务管理机制,那么本文我们就通过源码来分析一下Spring中的 ...
- Spring事务管理中@Transactional的参数配置
Spring作为低侵入的Java EE框架之一,能够很好地与其他框架进行整合,其中Spring与Hibernate的整合实现的事务管理是常用的一种功能. 所谓事务,就必须具备ACID特性,即原子性.一 ...
- Spring事务管理中@Transactional
最近写的一个消息推送的接口,供订单生成后调用,发现每次传过来的时候订单id是存在的,可是利用订单id去查订单信息做后续操作时发现查不到数据,最终发现是订单生成时候业务处理写在service层,加了Sp ...
- Spring事务管理中@Transactional的propagation参数
所谓事务传播性,就是被调用者的事务与调用者的事务之间的关系.举例说明. ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 //in A.java Class A { @Tr ...
- java面试记录二:spring加载流程、springmvc请求流程、spring事务失效、synchronized和volatile、JMM和JVM模型、二分查找的实现、垃圾收集器、控制台顺序打印ABC的三种线程实现
注:部分答案引用网络文章 简答题 1.Spring项目启动后的加载流程 (1)使用spring框架的web项目,在tomcat下,是根据web.xml来启动的.web.xml中负责配置启动spring ...
- @Transactional spring 事务失效(转载)
原文地址:http://hwak.iteye.com/blog/1611970 1. 在需要事务管理的地方加@Transactional 注解.@Transactional 注解可以被应用于接口定义和 ...
- Spring事务注解@Transactional失效的问题
在项目中发现事务失效,使用@Transactional注解标注的Service业务层实现类方法全部不能回滚事务了,最终发现使用因为Spring与shiro进行整合之后导致的问题,将所有的Service ...
随机推荐
- 使用 oh-my-posh 美化 windows terminal,让其接近oh-my-zsh
本文旨在快速让你进行美化,少踩一些坑,原文出自我的博客:prettier-windows-terminal-with-oh-my-posh 为了同 iterm2 下的 oh-my-zsh 保持基本一致 ...
- pymysql.err.OperationalError: (1054, "Unknown column 'aa' in 'field list'")(已解决)
错误描述: 今天使用python连接mysql数据库进行数据添加时,出现报错"pymysql.err.OperationalError: (1054, "Unknown colum ...
- 无法启动报,To install it, you can run: npm install --save @/components/xxxx.vue
运行的过程中后台报错 npm install --save @/components/xxx.vue 重装了node_modules依然没有用. 其实是组件路径写错了 总结 以后出现提醒安装那个vue ...
- SpringBoot+Mybatis-Plus整合Sharding-JDBC5.1.1实现单库分表【全网最新】
一.前言 小编最近一直在研究关于分库分表的东西,前几天docker安装了mycat实现了分库分表,但是都在说mycat的bug很多.很多人还是倾向于shardingsphere,其实他是一个全家桶,有 ...
- Kafka消息的压缩机制
最近在做 AWS cost saving 的事情,对于 Kafka 消息集群,计划通过压缩消息来减少消息存储所占空间,从而达到减少 cost 的目的.本文将结合源码从 Kafka 支持的消息压缩类型. ...
- HMS Core 视频编辑服务开放模板能力,助力用户一键Get同款酷炫视频
前言 短视频模板,是快捷创作短视频的一种方式,一般由专业设计师或模板创作人制作,用户只需替换视频模板中的部分素材,便可生成一支与模板一样的创意视频.这种省时省力.无需"烧脑"构思创 ...
- .NET中线程锁的使用
更新记录 本文迁移自Panda666原博客,原发布时间:2021年7月1日. 一.说明 由于经常需要在多线程代码中使用Monitor进行同步,并且需要自己去手写try/finally块.因此C#提供了 ...
- 不忍了,快速下载Visual Studio Code
更新记录 本文迁移自Panda666原博客,原发布时间:2021年5月2日. 奇怪的原因 因为一些众所周知的原因,在国内下载Visual Studio Code的速度比较慢,所以我们需要一些方法来加快 ...
- JAVA面向对象之封装和调用
一 面向对象 面向对象的三大特性 1 封装:概念:封装是把过程和数据私有化,打包封存起来,对数据访问只能通过指定的方式.简单的可以理解为你把钱存到银行里,银行专员首先要给你开一个账户,之后你的 ...
- c++可视化性能测试
阅读前注意 本文所有代码贴出来的目的是帮助大家理解,并非是要引导大家跟写,许多环境问题文件问题没有详细说明,代码也并不全面,达不到跟做的效果.建议直接阅读全文即可,我在最后会给出详细代码地址,对源代码 ...