@Transational踩坑

x33g5p2x  于2021-12-13 转载在 其他  
字(2.8k)|赞(0)|评价(0)|浏览(337)

踩坑1:@Transational里非写DB逻辑太多

@Transational最简单粗暴的使用方法就是在一个public方法上加上该注解,然后开始洋洋洒洒写上几百上千行代码,其中除了DB写操作部分代码,也可能包含了接口/方法入参校验、外部系统接口调用、业务逻辑、数据计算、集合转换等逻辑。

如此写,理论上是没什么大问题的,但绝大部分情况是到了最后部分才真正执行写DB的操作,此时才需用上@Transational,而在方法一开始就开启事务,很可能存在以下2种情况:

  1. 程序还未执行到写DB逻辑,就return了,此时@Transational浪费了。
  2. 程序执行到写DB逻辑时,经历时间过长,导致写DB失败。

其中,第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的逻辑,最好啥都不做。

踩坑2:抽象类调用具体类的public方法,@Transational注解失效

众所周知,@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使用需谨慎!!!

作者:曼特宁

相关文章