@Transational最简单粗暴的使用方法就是在一个public方法上加上该注解,然后开始洋洋洒洒写上几百上千行代码,其中除了DB写操作部分代码,也可能包含了接口/方法入参校验、外部系统接口调用、业务逻辑、数据计算、集合转换等逻辑。
如此写,理论上是没什么大问题的,但绝大部分情况是到了最后部分才真正执行写DB的操作,此时才需用上@Transational,而在方法一开始就开启事务,很可能存在以下2种情况:
其中,第2种情况正是笔者踩过的坑,且听下文详解。
在一个加了@Transational注解的方法里,先查询了外部系统接口,再进行写DB(MySQL 5.7)的操作。
某天,该方法打印了一行error日志,内容如下:
加@Transational注解就是为了抛异常后回滚,还能回滚失败?于是查看了更多日志,内容如下:
其中,Connection timed out日志正是查询外部系统接口打印的。
搜索了一下Communications link failure(参考:MySql的Communications link failure解决办法),错误的原因:MySQL服务在长时间不连接之后断开了,断开之后的首次请求会抛出这个异常,其中提到了MySQL的两个系统变量,interactive_timeout和wait_timeout,笔者查询到使用的MySQL的这2个值均为120s,与日志打印的The last packet successfully received from the server was 127261 ms ago的时间正好相符。此外,打印接口超时的前后日志相差也是2分钟(见下图)。
真相基本水落石出,开启事务后,程序调用外部系统接口2分钟超时,此时再操作写DB操作,超过了MySQL的interactive_timeout/wait_timeout,写DB失败。
验证代码如下:
@Transactional(rollbackFor = Exception.class)
public void test1() {
// 写DB、抛异常
insertAndThrowException();
}
@Transactional(rollbackFor = Exception.class)
public void test2() {
// 让程序先睡121秒
log.error("begin sleep...");
try {
TimeUnit.SECONDS.sleep(121L);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.error("wake up...");
// 写DB、抛异常
insertAndThrowException();
}
private void insertAndThrowException() {
// 写DB
insert();
// 抛异常
System.out.println(1 / 0);
}
private void insert() {
// insert into...
}
test1在执行System.out.println(1 / 0)时抛出了ArithmeticException,且insert操作被回滚,日志如下:
test2在执行insert()抛出了com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure,未执行到System.out.println(1 / 0),日志如下:
需要加@Transational注解的逻辑,先将非写DB操作的逻辑处理完,再调用加了@Transational注解的方法,该方法除了写DB的逻辑,最好啥都不做。
众所周知,@Transational注解需加在public方法,且被外部类调用时,才生效。
如下代码所示时,@Transational注解是失效的,原因是抽象父类调用具体子类方法时,是内部调用,而不是外部调用,代码如下:
@Service
public class TestService {
@Autowired
private AbstractClass abstractClass;
public void test() {
// A行
abstractClass.parent();
}
}
public abstract class AbstractClass {
public void parent() {
// B行
child();
}
abstract void child();
}
@Component
@Primary
public class ChildClass extends AbstractClass {
@Override
@Transactional(rollbackFor = Exception.class)
public void child() {
// C行
// 写DB、抛异常
insertAndThrowException();
}
private void insertAndThrowException() {
// 写DB
insert();
// 抛异常
System.out.println(1 / 0);
}
private void insert() {
// insert into...
}
}
乍一看,@Transational注解加在了public方法,也是被外部类调用的,但父类调用子类方法实则应属于内部调用,而方法内部调用@Transational注解的public方法会失效。
通过debuug也可发现,执行A行时会先进入CglibAopProxy代理,再执行B行,但B行执行C行时,并未进入代理。
在执行System.out.println(1 / 0)时抛出了ArithmeticException,但insert操作并未被回滚,日志如下:
将@Transational注解加在AbstractClass.parent()而非ChildClass.child()可解决,但可能违背了代码结构的初衷。
建议子类再调用其他类的@Transational注解public方法,或摒弃父子类的结构。
听说@Transational碰到锁也会有坑,有兴趣的同学可以看看当 Transactional 碰到锁,有个大坑!
@Transational使用需谨慎!!!
作者:曼特宁
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/vipshop_fin_dev/article/details/121889116
内容来源于网络,如有侵权,请联系作者删除!