Spring catch捕获了异常,全局事务依然回滚 @Transactional 踩坑记

问题描述

serviceA 和 serviceB 均有数据库插入和修改操作且都添加了 @Transactional注解

Controller层调用serviceA,serviceA调用serviceB, ServiceB内执行时抛出数据库sql异常, 并且该异常已被serviceB try catch, 所以程序依然能正常执行完成,但是最终程序正常执行完所有代码后,全局事务依然会回滚!(serviceA和serviceB都会回滚)

代码示例

@Service
public class ServiceAImpl{

	@Transactional(rollbackFor = Throwable.class)
	@Override
	public void serviceA(){
		// 插入数据
		XXX.insert();
		// 修改数据
		XXX.update();
	
		// 调用serviceB
		XXX.serviceB();
	
		// 继续serviceA 业务操作
		......
	}
}

@Service
public class ServiceBImpl{

	@Transactional(rollbackFor = Throwable.class)
	@Override
	public void serviceB(){
		// ...业务操作
		
		// 批量插入数据  
		try{
			/**
         	* 让这里运行时抛出数据库异常(数据表设置一个新的非空字段,不同步到实体即可抛出该异常) 
         	* java.sql.BatchUpdateException  注意模拟时不能手动 throw new BatchUpdateException() 
        	* 手动throw的异常无法重现问题
         	**/
			XXX.saveBatch(entityList);
		} catch (Exception e) {
            log.error("打印异常日志......", e);
        }
	}
}

最后serviceA执行完之后抛出的异常

2021-04-16 21:29:00.392 ERROR [com.xxxx.AmqpLogProducerImpl:36] - 
SysLogError(module=null, requestUri=null, requestMethod=null, requestParams=null, userAgent=null, ip=null, errorInfo=
org.springframework.transaction.UnexpectedRollbackException: 
Transaction rolled back because it has been marked as rollback-only
	at org.springframework.transaction.support.AbstractPlatformTransactionManager.processRollback(AbstractPlatformTransactionManager.java:870)
	at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:707)
	at ......

看到这里,也许有同学会说,将serviceB的事务注解添加一个属性,设置为

@Transactional(rollbackFor = Throwable.class, propagation = Propagation.REQUIRES_NEW)

也就是进入serviceB时,开启一个新的事务,但是经过测试,该方法依然不可行,并且这样子,serviceB执行完return之后,程序就直接终止并且全局回滚了,都不会继续执行serviceA的剩余的业务程序了。(PS:测试过Propagation的所有枚举值,除了NOT_SUPPORTED(不启用事务)之外,其他所有属性均无法解决全局事务回滚的问题)

定位问题

最终经过反复调试和网上查资料,仔细查阅spring的@Transactional 注解的事务传播机制、各类参数、事务提交和回滚的各种场景,最终定位到原因如下,我这里用白话结合本文示例给大家描述一下:

程序进入serviceA时,
此时会创建一个事务
spring会打印

 // 创建一个事务
 Creating new transaction

我们叫它事务A,

执行到serviceB时,如果serviceB的@Transactional没有指定propagation 属性,
则默认使用的Propagation.REQUIRED,spring会打印

 // 也就是serviceB里面也会参与事务A;
 Participating in existing transaction 

此时serviceB里抛出异常,虽然异常被捕获,但是事务已经被标记为需要回滚,spring会打印

 // 意思是参与事务失败,标记未需要回滚
 Participating transaction failed - marking existing transaction as rollback-only

所以最终serviceA执行完提交事务时,因为事务被标记为需要rollback,所以事务最终执行了回滚操作,源码如下:
AbstractPlatformTransactionManager.class

/**
 * This implementation of commit handles participating in existing
 * transactions and programmatic rollback requests.
 * Delegates to {@code isRollbackOnly}, {@code doCommit}
 * and {@code rollback}.
 * @see org.springframework.transaction.TransactionStatus#isRollbackOnly()
 * @see #doCommit
 * @see #rollback
 */
@Override
public final void commit(TransactionStatus status) throws TransactionException {
	if (status.isCompleted()) {
		throw new IllegalTransactionStateException(
				"Transaction is already completed - do not call commit or rollback more than once per transaction");
	}

	DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
	if (defStatus.isLocalRollbackOnly()) {
		if (defStatus.isDebug()) {
			logger.debug("Transactional code has requested rollback");
		}
		processRollback(defStatus, false);
		return;
	}
	// defStatus.isGlobalRollbackOnly() 因为事务在serviceB里面标记成了需要回滚 所以该结果返回了true,导致全局回滚
	if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
		if (defStatus.isDebug()) {
			logger.debug("Global transaction is marked as rollback-only but transactional code requested commit");
		}
		processRollback(defStatus, true);
		return;
	}

	processCommit(defStatus);
}

哪怕将serviceB的事务传播改为

@Transactional(rollbackFor = Throwable.class, propagation = Propagation.REQUIRES_NEW)

依然无法解决问题,因为即使程序进入serviceB时挂起了当前事务(事务A),创建了一个新事务(事务B),因为serviceB出现了异常,所以事务B被标记为需要回滚,但是因为serviceB捕获了该异常,所以程序能正常执行,但是最终serviceA提交事务时,因为此时有两个事务,事务A和事务B,事务A需要提交,事务B需要回滚,此时出现矛盾,所以最终全局事务都会回滚。

解决方案

大家先看正确的代码示例

@Service
public class ServiceAImpl{

	@Transactional(rollbackFor = Throwable.class)
	@Override
	public void serviceA(){
		// 插入数据
		XXX.insert();
		// 修改数据
		XXX.update();
	
		// 调用serviceB
		try{
			XXX.serviceB();
		}catch (Exception e) {
            log.error("打印异常日志......", e);
        }
		
	
		// 继续serviceA 业务操作
		......
	}
}

@Service
public class ServiceBImpl{

	@Transactional(rollbackFor = Throwable.class, propagation = Propagation.REQUIRES_NEW)
	@Override
	public void serviceB(){
		// ...业务操作
		
		// 批量插入数据  
		/**
       	* 让这里运行时抛出数据库异常(数据表设置一个新的非空字段,不同步到实体即可抛出该异常) 
       	* java.sql.BatchUpdateException  注意模拟时不能手动 throw new BatchUpdateException() 
      	* 手动throw的异常无法重现问题
       	**/
		XXX.saveBatch(entityList);
	}
}

结果 这样调整后,最终事务B正常回滚,事务A正常commit

个人理解
如果事务B的异常不能影响事务A,则应该由事务A去捕获事务B的异常,此时才能使事务A正常commit,为什么会这样呢?我个人的理解是这样的:

1、事务B里捕获异常
事务B里抛出了异常并进行捕获,此时spring会标记事务B需要回滚,然后事务A没有捕获到异常,则事务A需要正常提交,那么此时就出现了事务矛盾,最终spring会选择回滚事务。

2、事务B里抛出异常,由事务A进行捕获
事务B里抛出了异常,此时事务B被标记为回滚,程序直接返回事务A,事务A捕获了该异常并catch,则说明事务A需要继续正常执行,最终事务A正常commit,事务B回滚。


版权声明:本文为weixin_40623736原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
THE END
< <上一篇
下一篇>>