问题描述
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回滚。