一、问题产生背景
应用上线的时候,正常调用Tomcat的shutdown.sh脚本,事务执行一半异常提交。伪代码如下:
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void insert(PaymentOrder paymentOrder) {
try{
paymentOrderDao.update(paymentOrder);
PaymentOrderDao.insert(paymentOrder)
}catch(Exception e){
logger.error(" 操作支付订单失败 biz " + paymentOrder.getBiz() + " bizOrder " + paymentOrder.getBizOrder(), e);
Throw e;
}
}
上面是一段伪代码,实际在tomcat重启的时候,上面update语句提交,而insert没有。
二、思路解析
1、直接将Tomcat服务kill掉能否重现问题
按之前的理解是,Tomcat重启事务中断,数据库在事务连接超时后会回滚事务。那么写一段代码试一下,使用Kill -9命令中断tomcat服务后发现数据库事务竟然回滚了。
2、分析Tomcat的shutdown.sh命令
其实shutdown.sh命令最终调用的是catalina.sh命令脚本,看sh源码如下:
exec "$_RUNJAVA" $JAVA_OPTS $CATALINA_OPTS \
-Djava.endorsed.dirs="$JAVA_ENDORSED_DIRS" -classpath "$CLASSPATH" \
-Dcatalina.base="$CATALINA_BASE" \
-Dcatalina.home="$CATALINA_HOME" \
-Djava.io.tmpdir="$CATALINA_TMPDIR" \
org.apache.catalina.startup.Bootstrap "$@" stop
其实最终是调用的Bootstrap这个类来关闭服务的,我们再来看这个类的内容。
if (server instanceof Lifecycle) {
try {
((Lifecycle) server).stop();
} catch (LifecycleException e) {
log.error("Catalina.stop", e);
}
}
Tomcat是将注册进来的服务循环逐个关闭,这时候在关闭的时候可能会因为前一个资源关闭而造成后一个资源抛出异常,注意这个异常有可能是Throwable,也可能是Exception,后面详细解释。
3、分析Spring注解事务源码
在Tomcat关闭的时候,抛出的异常和上面代码的异常没有匹配成功,spring异常匹配采用迭代当前异常的所有父类与目标异常匹配,匹配不到后检查当前异常是否为Error或者RuntimeException的实例,是的话也能匹配上,但是没有匹配是否为Throwable的实例
三、问题总结
通过上面的问题分析,可以把代码写成如下样式:
@Override
@Transactional(rollbackFor = Throwable.class, propagation = Propagation.REQUIRED)
public void insert(PaymentOrder paymentOrder) {
try{
paymentOrderDao.update(paymentOrder);
PaymentOrderDao.insert(paymentOrder)
}catch(Throwable e){
logger.error(" 操作支付订单失败 biz " + paymentOrder.getBiz() + " bizOrder " + paymentOrder.getBizOrder(), e);
Throw e;
}
}
采用Throwable捕获方能确保Tomcat异常重启,事务能够正确回滚。