问题产生背景
有用户反馈在系统中操作修改时有报错,查询日志发现报错信息如下。看起来就是事务回滚了,因为被标记为只能回滚。
报错信息
2024-11-13 10:45:59.743 ERROR [io-8080-exec-40] .c.c.c.AbstractExceptionAdvice : Transaction rolled back because it has been marked as rollback-only
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) ~[spring-tx-5.3.9.jar!/:5.3.9]
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:707) ~[spring-tx-5.3.9.jar!/:5.3.9]
at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:654) ~[spring-tx-5.3.9.jar!/:5.3.9]
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:407) ~[spring-tx-5.3.9.jar!/:5.3.9]
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-5.3.9.jar!/:5.3.9]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.9.jar!/:5.3.9]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750) ~[spring-aop-5.3.9.jar!/:5.3.9]
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:692) ~[spring-aop-5.3.9.jar!/:5.3.9]
at com.serviceWrapper$$EnhancerBySpringCGLIB$$43f15635.update(<generated>) ~[manage-2.1.3-RELEASE.jar!/:2.1.3-RELEASE]
2024-11-13 10:45:59.383 ERROR [io-8080-exec-40] ConfigService : error for
### Error flushing statements. Cause: org.apache.ibatis.executor.BatchExecutorException: com.ConfigMapper.insert (batch index #1) failed. Cause: java.sql.BatchUpdateException: Duplicate entry 'xxxx' for key 'uk_code'
### Cause: org.apache.ibatis.executor.BatchExecutorException: com.ConfigMapper.insert (batch index #1) failed. Cause: java.sql.BatchUpdateException: Duplicate entry 'xxxx' for key 'uk_code'
org.apache.ibatis.exceptions.PersistenceException:
### Error flushing statements. Cause: org.apache.ibatis.executor.BatchExecutorException: com.ConfigMapper.insert (batch index #1) failed. Cause: java.sql.BatchUpdateException: Duplicate entry 'xxxx' for key 'uk_code'
### Cause: org.apache.ibatis.executor.BatchExecutorException: com.ConfigMapper.insert (batch index #1) failed. Cause: java.sql.BatchUpdateException: Duplicate entry 'xxxx' for key 'uk_code'
at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30) ~[mybatis-3.5.1.jar!/:3.5.1]
at org.apache.ibatis.session.defaults.DefaultSqlSession.flushStatements(DefaultSqlSession.java:254) ~[mybatis-3.5.1.jar!/:3.5.1]
at com.baomidou.mybatisplus.extension.service.impl.ServiceImpl.saveBatch(ServiceImpl.java:128) ~[mybatis-plus-extension-3.1.0.jar!/:3.1.0]
at com.baomidou.mybatisplus.extension.service.IService.saveBatch(IService.java:58) ~[mybatis-plus-extension-3.1.0.jar!/:3.1.0]
at com.baomidou.mybatisplus.extension.service.IService$$FastClassBySpringCGLIB$$f8525d18.invoke(<generated>) ~[mybatis-plus-extension-3.1.0.jar!/:3.1.0]
问题定位
搜了一下解释,是说事务在提交的时候报错了,因为被标记为只能回滚,无法提交。按照一般的情况,是有异常抛出,事务回滚。当前这个场景,是异常抛出来,被事务管理器标记为只能回滚,但是程序又正常执行了,最后事务提交的时候报错了。
由以上排查,开始梳理代码逻辑(如下伪代码)。有看到报错信息里面有另一行error日志是C.c1方法打印的,但是这个异常已经被catch吞掉了,不会抛出。因为在设计上这里是弱业务,不需要影响主逻辑执行。所以就很奇怪,明明没有异常抛出,事务却被标记为只回滚了。
class A {
public void a1() {
// 业务处理
B.b1();
// 后置处理
}
}
class B {
@Transactional(rollbackFor = Exception.class)
public void b1() {
// 本地事务执行
// 外部服务调用
RPC.rpc();
// 弱业务处理,有异常不回滚
C.c1();
}
}
class C {
public void c1() {
try {
// mybstisPlus更新数据库
} catch (Exception e) {
// 吞掉异常
log.error("更新数据库失败", e);
}
}
}
class RPC {
public void rpc() {
// 业务逻辑
}
}
我当时傻傻的一直在看其他方法,就是没想到去看C.c1里面的逻辑,因为是直接调mybatisPlus的,直到一位同事路过提醒了一句, 点进去看了一眼,破案。mybatisPlus有加事务注解,并且抛出了异常,所以外面C.c1方法catch异常的时候,事务管理器已经被标记为回滚了。
解决方案
将C.c1方法设置为以非事务方式执行。
class C {
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void c1() {
try {
// mybstisPlus更新数据库
} catch (Exception e) {
// 吞掉异常
log.error("更新数据库失败", e);
}
}
}
思考
排查问题的时候不要固定思维,想当然。之前自己看的时候就天然默认mybatisPlus公共的里面不会有啥东西,一直在问题方法外面找哪里抛异常了,其实这是不符合事实的,如果外面有抛异常事务就会被正常回滚,而不会程序全执行完了去尝试提交事务。反而是catch吞掉异常那里,是最有问题的。下次注意!(一定🐶