spring声明式事务(@Transactional)开发常犯的几个错误及解决办法
spring声明式事务(@Transactional)开发常犯的几个错误及解决办法
目前JAVA的微服务项目基本都是SSM结构(即:springCloud +springMVC+Mybatis),而其中Mybatis事务的管理也是交由spring来管理,大部份都是使用声明式事务(@Transactional)来进行事务一致性的管理,然后在实际日常开发过程中,发现很多开发同学都用错了spring声明式事务(@Transactional)或者说使用非常不规范,导致出现各种事务问题。我(梦在旅途)今天周日休息,花了几个小时把目前我已知的开发常犯的几个错误都列举出来并逐一分析根本原因同时针对原因给出解决方案及示例,希望能帮助到广大JAVA开发者。
1. 事务不生效
问题现象:明明有事务注解,在事务方法内部有抛错,但事务却没有回滚,该执行的SQL都执行了。示例代码如下:(doInsert方法是有事务注解的)
/**
* @author zuowenjun
* @see wwww.zuowenjun.cn
*/
@Service
public class DemoUserService {
//... ...
public DemoUser doGet() {
try {
doInsert(1);
} catch (Exception ex) {
System.out.println("insert error: " + ex.toString());
}
return demoUserMapper.get(1);
} @Transactional
public int doInsert(int id) {
DemoUser user = new DemoUser(id, "zs", 18, new BigDecimal("8888.88"),
"shenzhen,cn", new Timestamp(System.currentTimeMillis()), new Timestamp(System.currentTimeMillis())); int result = demoUserMapper.insert(user); throw new RuntimeException("mock insert ex"); //模拟抛错 return result;
}
} //演示调用,最终打印出了ID为1的那条记录,事务并没有回滚
DemoUser result = demoUserService.doGet();
System.out.println(result != null ? result.toString() : "none");
根本原因:没有执行事务AOP切面,因为在BEAN方法内部直接调用另一个公开的事务方法,是原生的方法之间调用,并非是被代理后的BEAN方法,所以SPRING事务注解在这种情况下失去作用。
解决方案:不论是在BEAN外部或BEAN方法内部,要确保一定是调用代理BEAN的公开事务方法,确保调用事务方法有被SPRING事务拦截处理,示例代码如下:【在BEAN内部则需要先注入BEAN本身的代理BEAN实例(有很多中获取当前BEAN的代理BEAN方案,在此不细说),然后通过代理BEAN调事务方法即可。】
/**
* @author zuowenjun
* @see wwww.zuowenjun.cn
*/
@Service
public class DemoUserService {
@Autowired
@Lazy //加上这个,是防止循环自依赖
private DemoUserService selfService; //注入自己的代理BEAN实例 //... ... public DemoUser doGet() {
try {
selfService.doInsert(1); //这里改为使用代理BEAN调doInsert的事务方法,确保走切面
} catch (Exception ex) {
System.out.println("insert error: " + ex.toString());
}
return demoUserMapper.get(1);
} @Transactional
public int doInsert(int id) {
DemoUser user = new DemoUser(id, "zs", 18, new BigDecimal("8888.88"),
"shenzhen,cn", new Timestamp(System.currentTimeMillis()), new Timestamp(System.currentTimeMillis())); int result = demoUserMapper.insert(user); throw new RuntimeException("mock insert ex"); // return result;
}
} //演示调用,最终打印出了none,说明事务有回滚,无法查出ID为1的那个记录
DemoUser result = demoUserService.doGet();
System.out.println(result != null ? result.toString() : "none");
2. 事务提交报错
问题现象:事务方法内有catch住错误,但却无法正常提交事务,报错:Transaction rolled back because it has been marked as rollback-only,示例代码如下:
/**
* @author zuowenjun
* @see wwww.zuowenjun.cn
*/
@Service
public class DemoUserService {
@Autowired
@Lazy //加上这个,是防止循环自依赖
private DemoUserService selfService; //注入自己的代理BEAN实例 //... ... @Transactional
public DemoUser doGet() {
try {
selfService.doInsert(1);
} catch (Exception ex) { //有catch错误,但当doGet返回时却报错了
System.out.println("insert error: " + ex.toString());
}
return demoUserMapper.get(1);
} @Transactional
public int doInsert(int id) {
DemoUser user = new DemoUser(id, "zs", 18, new BigDecimal("8888.88"),
"shenzhen,cn", new Timestamp(System.currentTimeMillis()), new Timestamp(System.currentTimeMillis())); int result = demoUserMapper.insert(user); if (id==1) {
throw new RuntimeException("mock insert ex");
}
return result;
}
} //演示调用,最终有报错:Transaction rolled back because it has been marked as rollback-only
DemoUser result = demoUserService.doGet();
System.out.println(result != null ? result.toString() : "none");
根本原因:事务继承“惹的祸”【事务传播特性】,入口事务方法内部再调其他事务方法,其他事务方法若有抛错则会在方法返回时被事务切面标记当前事务仅能回滚,若最后入口事务方法执行完成并想提交事务时却因为事务是继承的且有被标记为仅能回滚后则只能报错
解决方案:避免事务继承 或 确保事务方法内部不再调用其他事务方法(即:事务方法变成普通方法,小技巧参照我之前文章:任何Bean通过实现ProxyableBeanAccessor接口即可获得动态灵活的获取代理对象或原生对象的能力 - 梦在旅途 - 博客园 (cnblogs.com)),示例代码如下:
/**
* @author zuowenjun
* @see wwww.zuowenjun.cn
*/
@Service
public class DemoUserService {
@Autowired
@Lazy //加上这个,是防止循环自依赖
private DemoUserService selfService; //注入自己的代理BEAN实例 //... ... @Transactional
public DemoUser doGet() {
try {
selfService.doInsert(1);
// doInsert(1); 方案二:内部直接doInsert方法,此时是原生方法调用,不走事务切面,也就不会触发事务记录的情况
} catch (Exception ex) { //有catch错误
System.out.println("insert error: " + ex.toString());
}
selfService.doInsert(2);
return demoUserMapper.get(2);
} @Transactional(propagation = Propagation.REQUIRES_NEW) //方案一:这里加上REQUIRES_NEW、或NOT_SUPPORTED,确保不继承外部事务即可
public int doInsert(int id) {
DemoUser user = new DemoUser(id, "zs", 18, new BigDecimal("8888.88"),
"shenzhen,cn", new Timestamp(System.currentTimeMillis()), new Timestamp(System.currentTimeMillis())); int result = demoUserMapper.insert(user); if (id==1) {
throw new RuntimeException("mock insert ex");
}
return result;
}
} //演示调用,最终正确打印了ID为2的记录,说明虽然插入ID=1的记录失败了,但插入2的记录是正确的,入口事务有正确的提交
DemoUser result = demoUserService.doGet();
System.out.println(result != null ? result.toString() : "none");
3. 事务不回滚
问题现象:事务方法内部有报错,但事务却仍提交了,示例代码如下:
/**代码片段
* @author zuowenjun
* @see wwww.zuowenjun.cn
*/ //第一种情况:错误被catch住了
@Transactional
public DemoUser doGet1() {
try {
doInsert(1); //doInsert原生调用,代码看似有事务,实际此时无事务,也就不存在事务回滚的情况
} catch (Exception ex) { //catch错误,doGet事务正常提交
System.out.println("insert error: " + ex.toString());
}
selfService.doInsert(2);
return demoUserMapper.get(2);
} //第二种情况:外层报错,内层事务正常提交
@Transactional
public DemoUser doGet2() {
selfService.doInsert(2); //doInsert切面调用,有事务且单独事务,执行完即提交
throw new RuntimeException("mock doGet ex");//这里抛错不影响doInsert的提交
return demoUserMapper.get(2);
} @Transactional(propagation = Propagation.REQUIRES_NEW)
public int doInsert(int id) {
DemoUser user = new DemoUser(id, "zs", 18, new BigDecimal("8888.88"),
"shenzhen,cn", new Timestamp(System.currentTimeMillis()), new Timestamp(System.currentTimeMillis())); int result = demoUserMapper.insert(user); if (id==1) {
throw new RuntimeException("mock insert ex");
} return result;
} //演示调用,第1种情况
DemoUser result = demoUserService.doGet1();
System.out.println(result != null ? result.toString() : "none"); //演示调用,第2种情况
DemoUser result = demoUserService.doGet2();
System.out.println(result != null ? result.toString() : "none");
根本原因:一是错误被catch住了,这种情况下事务切面认为是正常的则会正常执行提交事务,二是根本就没有事务或事务并非同一个事务(与事务传播特性有关),这种情况就好理解,没事务就不存在事务提交(方法中的每个SQL即为一个小事务,执行即提交),若是事务方法内部有嵌套调用其他事务方法,入口的外层事务会受内部其他事务方法的影响,反之若其他事务方法与外层事务不是同一个事务,那么外层事务有报错并不会影响内部其他事务方法
- 这里还补充一种特殊情况,若在事务方法中异步调用其他事务方法(@Async 或线程池直接调用等情况),那么由于不在同一个线程上下文,即使默认是继承的传播特性也无变成2个不相干的事务各自执行,异步事务方法的报错不会影响外层的事务方法
解决方案:若需保证事务的完整性,需确保若有异常一定要抛错而非catch错误,另外需确保一定有事务,当事务方法内部有嵌套调用其他事务方法时,若希望被调用的事务方法与当前事务保持一致,那么就应确保是事务继承,否则就说明可以允许局部事务不一致,示例代码如下:
/**代码片段
* @author zuowenjun
* @see wwww.zuowenjun.cn
*/ @Transactional
public DemoUser doGet() {
doInsert(1);//不要catch,若catch后记录日志后再抛出,总之一定要抛错
selfService.doInsert(1);//这种也可以,当doInsert报错,则doInsert与doGet方法均回滚(本质是同一个事务)
selfService.doInsert(2);
return demoUserMapper.get(2);
} @Transactional(propagation = Propagation.REQUIRED) //若需与外层事务这一致,这里建议采用REQUIRED的传播特性
public int doInsert(int id) {
DemoUser user = new DemoUser(id, "zs", 18, new BigDecimal("8888.88"),
"shenzhen,cn", new Timestamp(System.currentTimeMillis()), new Timestamp(System.currentTimeMillis())); int result = demoUserMapper.insert(user); if (id==1) {
throw new RuntimeException("mock insert ex");
} return result;
}
4. 死锁
问题现象:执行SQL有报死锁,示例代码如下:
/**代码片段
* @author zuowenjun
* @see wwww.zuowenjun.cn
*/ @Transactional
public DemoUser doGetX() {
selfService.doInsert(1);
DemoUser user=selfService.get(1);
user.setName("xxx");
update(user); //这里是原生方法调用,等同于在doGetX同一个事务方法内部执行 user.setName("xxx2");
selfService.update(user); //这里新开事务调用,由于doGetX中已经有调用update(id=1)且事务还未提交,故这里需要等待doGetX事务提交以便释放锁,而doGetX事务则因为这里等待无法往下执行,形成事务循环自依赖了
return demoUserMapper.get(1);
} @Transactional(propagation = Propagation.REQUIRES_NEW) //这里新开事务
public int update(DemoUser demoUser) {
return demoUserMapper.update(demoUser);
} //演示调用,执行报错,不同DB的报错提示可能有所不同
DemoUser result = demoUserService.doGetX();
System.out.println(result != null ? result.toString() : "none");
根本原因:事务被循环自依赖了,再准确的说就是同一个记录被2个事务相互依赖,导致相互等待获取锁
解决方案:避免事务被循环自依赖,示列代码如下:
/**代码片段
* @author zuowenjun
* @see wwww.zuowenjun.cn
*/ //优化一
@Transactional
public DemoUser doGetX() {
selfService.doInsert(1);
DemoUser user=selfService.get(1);
user.setName("xxx");
update(user); //这里是原生方法调用,等同于在doGetX同一个事务方法内部执行 user.setName("xxx2");
update(user); //这里也改为原生方法调用,等同于在doGetX同一个事务方法内部执行
return demoUserMapper.get(1);
} //优化二
@Transactional
public DemoUser doGetX() {
selfService.doInsert(1);
DemoUser user=selfService.get(1);
user.setName("xxx");
selfService.update(user); //这里是代理BEAN方法调用,新开事务,直接执行并提交,与doGetX事务互不影响 user.setName("xxx2");
selfService.update(user); //这里是代理BEAN方法调用,新开事务,直接执行并提交,与doGetX事务互不影响
return demoUserMapper.get(1);
} @Transactional(propagation = Propagation.REQUIRES_NEW) //这里新开事务
public int update(DemoUser demoUser) {
return demoUserMapper.update(demoUser);
} //演示调用,执行报错,不同DB的报错提示可能有所不同
DemoUser result = demoUserService.doGetX();
System.out.println(result != null ? result.toString() : "none");
5. 在事务提交后回调事件方法中开事务不生效
问题现象:在事务提交后回调事件方法中【即:afterCommit】开启事务不生效(即:添加了@Transactional,也执行了代理方法的调用,但就像没有事务一样,出现报错事务不回滚,也无法在事务方法中再次注册事务提交后回调事务件方法),示例代码如下:
/**代码片段
* @author zuowenjun
* @see wwww.zuowenjun.cn
*/ @Transactional
public DemoUser doGetX() {
doInsert(1);
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
selfService.doInsert(2);//走切面调用,确保执行代理的事务方法,但实际还是无事务,报错也不会回滚
}
}); return demoUserMapper.get(1);
} @Transactional(propagation = Propagation.REQUIRED)
public int doInsert(int id) {
DemoUser user = new DemoUser(id, "zs", 18, new BigDecimal("8888.88"),
"shenzhen,cn", new Timestamp(System.currentTimeMillis()), new Timestamp(System.currentTimeMillis())); int result = demoUserMapper.insert(user); if (id==2) {
throw new RuntimeException("mock insert ex");
} return result;
} //演示调用:虽然doGetX有报错,但最终doInsert方法均有执行,且都能查出ID=1 与2的记录
try {
DemoUser result = demoUserService.doGetX();
System.out.println(result != null ? result.toString() : "none");
}catch (Exception e){
System.out.println("error " + e.toString());
} DemoUser result1 =demoUserService.get(1);
System.out.println(result1 != null ? result1.toString() : "none"); DemoUser result2 =demoUserService.get(2);
System.out.println(result2 != null ? result2.toString() : "none");
根本原因:在事务提交后回调事件方法中【即:afterCommit】,spring事务的管理状态仍保留(即:仍是事务激活状态)但DB事务其实已提交,当回调方法中又遇到有事务注解的方法时且判断已有事务(即spring事务的管理状态是激活状态transactionActive=true)时,若是默认继承状态则不会再开启新事务,仅复用DB连接
解决方案:在事务提交后回调事件方法中【即:afterCommit】开启新事务(即:传播特性为:REQUIRES_NEW) 或者 执行前强制清除事务状态【需要编写事务状态清除工具类】,示例代码如下:
/**代码片段
* @author zuowenjun
* @see wwww.zuowenjun.cn
*/ @Transactional
public DemoUser doGetX() {
TxManagerUtils.clearTxStatus();//方案二:通过事务状态清除工具类注册事务回调后首先清除事务状态,二选其一即可
doInsert(1);
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
selfService.doInsert(2);//走切面调用,确保执行代理的事务方法
}
}); return demoUserMapper.get(1);
} @Transactional(propagation = Propagation.REQUIRES_NEW) //方案一:这里强制开启新事务,二选其一即可
public int doInsert(int id) {
DemoUser user = new DemoUser(id, "zs", 18, new BigDecimal("8888.88"),
"shenzhen,cn", new Timestamp(System.currentTimeMillis()), new Timestamp(System.currentTimeMillis())); int result = demoUserMapper.insert(user); if (id==2) {
throw new RuntimeException("mock insert ex");
} return result;
} //演示调用:虽然doGetX有报错,但只能查出ID=1的记录,ID=2由于报错事务回滚了,说明afterCommit中再开启事务是OK的
try {
DemoUser result = demoUserService.doGetX();
System.out.println(result != null ? result.toString() : "none");
}catch (Exception e){
System.out.println("error " + e.toString());
} DemoUser result1 =demoUserService.get(1);
System.out.println(result1 != null ? result1.toString() : "none"); DemoUser result2 =demoUserService.get(2);
System.out.println(result2 != null ? result2.toString() : "none");
事务状态清除工具类如下:
package org.springframework.jdbc.datasource; //必需放在这个包目录下,因为connectionHolder.setTransactionActive 是protected方法 import com.example.springwebapp.utils.SpringUtils;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import org.springframework.transaction.support.TransactionSynchronizationManager; import javax.sql.DataSource; /**
* @author zuowenjun
* @see wwww.zuowenjun.cn
*/
public class TxManagerUtils { //建议在每个事务方法的第一行调用,避免事务方法内部中途若有其他方法需要注册事务提交后回调方法
public static void clearTxStatus() {
DataSource dataSource = SpringUtils.getBean(DataSource.class);
ConnectionHolder connectionHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource); TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public int getOrder() {
return Integer.MIN_VALUE; //确保最先执行
} @Override
public void afterCommit() {
doClearTxStatus(); //第一个回调事件中先清除事务状态
} @Override
public void afterCompletion(int status) {
TransactionSynchronizationManager.bindResource(dataSource, connectionHolder); //恢复DB连接绑定,避免执行事务清理时报错
}
});
} private static void doClearTxStatus() {
DataSource dataSource = SpringUtils.getBean(DataSource.class);
TransactionSynchronizationManager.setActualTransactionActive(false); //设置事务状态为非激活
ConnectionHolder connectionHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
connectionHolder.setTransactionActive(false);//设置事务状态为非激活
TransactionSynchronizationManager.unbindResource(dataSource); //暂时解绑DB连接
} }注:后面我预计还会针对spring事务这块进行其他方面的分享(比如:spring事务在多数据源中切换数据源不生效、事务隔离级别下的并发处理等),敬请期待,原创不易,若有不足欢迎指出,谢谢!
最后预祝大家2024年龙年大吉,新春快乐!
spring声明式事务(@Transactional)开发常犯的几个错误及解决办法的更多相关文章
- Spring声明式事务@Transactional 详解,事务隔离级别和传播行为
@Transactional注解支持9个属性的设置,这里只讲解其中使用较多的三个属性:readOnly.propagation.isolation.其中propagation属性用来枚举事务的传播行为 ...
- Spring声明式事务管理基于@Transactional注解
概述:我们已知道Spring声明式事务管理有两种常用的方式,一种是基于tx/aop命名空间的xml配置文件,另一种则是基于@Transactional 注解. 第一种方式我已在上文为大 ...
- Spring 声明式事务与编程式事务详解
本文转载自IBM开发者论坛:https://developer.ibm.com/zh/articles/os-cn-spring-trans 根据自己的学习理解有所调整,用于学习备查. 事务管理对于企 ...
- 深刻理解Spring声明式事务
问题引入 Spring中事务传播有哪几种,分别是怎样的? 理解注解事务的自动配置? SpringBoot启动类为什么不需要加@EnableTransactionManagement注解? 声明式事务的 ...
- spring声明式事务管理总结
事务配置 首先在/WEB-INF/applicationContext.xml添加以下内容: <!-- 配置事务管理器 --> <bean id="transactionM ...
- spring 声明式事务管理
简单理解事务: 比如你去ATM机取5000块钱,大体有两个步骤:首先输入密码金额,银行卡扣掉5000元钱:然后ATM出5000元钱.这两个步骤必须是要么都执行要么都不执行.如果银行卡扣除了5000块但 ...
- Spring声明式事务管理基于tx/aop命名空间
目的:通过Spring AOP 实现Spring声明式事务管理; Spring支持编程式事务管理和声明式事务管理两种方式. 而声明式事务管理也有两种常用的方式,一种是基于tx/aop命名空间的xml配 ...
- Spring声明式事务配置管理方法
环境配置 项目使用SSH架构,现在要添加Spring事务管理功能,针对当前环境,只需要添加Spring 2.0 AOP类库即可.添加方法: 点击项目右键->Build Path->Add ...
- 161117、使用spring声明式事务抛出 identifier of an instance of
今天项目组有成员使用spring声明式事务出现下面异常,这里跟大家分享学习下. 异常信息: org.springframework.orm.hibernate3.HibernateSystemExce ...
- Spring声明式事务管理与配置详解
转载:http://www.cnblogs.com/hellojava/archive/2012/11/21/2780694.html 1.Spring声明式事务配置的五种方式 前段时间对Spring ...
随机推荐
- package.json文件中版本号
"5.0.3"表示安装指定的5.0.3版本,"-5.0.3"表示安装5.0.X中最新的版本,"^5.0.3"表示安装5.X.X中最新的版本.
- babel相关配置
npm install --save-dev @babel/core @babel/cli @babel/preset-env @babel/node npm install --save @babe ...
- Node版本升级和降级
https://blog.csdn.net/qq_29262849/article/details/52973095
- AXI Channel
AXI Channel axi与ahb不同就是分为不同的channel write address channel - 表明一个transaction基本的属性,包含本次传输的地址\类型\大小(多少字 ...
- 上下文中找不到org.springframework.boot.web.servlet.server.ServletWebServerFactory bean
1.问题 报错如下: Description: Web application could not be started as there was no org.springframework.boo ...
- [kubernetes]服务健康检查
前言 进程在运行,但是不代表应用是正常的,对此pod提供的探针可用来检测容器内的应用是否正常.k8s对pod的健康状态可以通过三类探针来检查:LivenessProbe.ReadinessProbe和 ...
- [转帖]性能优化:Swap调优
目标:解决大量Log写入占用大量的File Cache,内容利用不充分导致swap 基本原则:尽量使用内存,减少swap,同时,尽早flush到外存,早点释放内存给写cache使用.---特别在持续的 ...
- [转帖]解决Harbor在服务器重启后无法自启动的问题
问题 当部署Harbor的服务器在重启之后,可能会出现Harbor无法跟随系统自启动 解决方案 现假设Harbor的安装目录位置为/usr/local/harbor,在Harbor安装完成之后,在此目 ...
- [转帖]rsar - Extract data from plain-text sar files
sar -A -t -f /tmp/sa11 >/tmp/sar11 https://github.com/ryran/rsar When dealing with sysstat sar da ...
- Oracle 查询用户下表名,表列数,表行数,表大小的SQL
最近想分析下数据库的信息, 然后写了这个SQL. 比较lowB一些. 因为Oracle的 deferred_segment_creation 参数的影响. 很多表如果是0行,那么是不会创建extent ...