背景
系统告警:
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
事务提交时发现事务已被标记为回滚(rollback-only)。
复现示例:
@RestController
public class TestController {
@Autowired
TestService testService;
@GetMapping("/test")
public Object test() {
testService.test();
return "test success";
}
}
@Service
public class TestService {
@Autowired
private DealService dealService;
@Autowired
private UserService userService;
@Transactional
public void test() {
// 数据库更新
userService.createUser(new UucUser(94951L));
userService.updateUser(new UucUser(94951L));
try {
dealService.deal();
} catch (Exception e) {
System.out.println("业务异常");
e.printStackTrace();
}
}
}
@Service
public class DealService {
@Transactional
public void deal() {
throw new RuntimeException();
}
}
请求接口/test会复现UnexpectedRollbackException异常。
原因是在调用TestService的事务方法test时,spring会创建一个事务,在调用DealService的事务方法deal时,spring默认传播Propagation_Required,即被调用时如存在事务则使用该事务,因为deal方法的事务和test方法的事务是同一个事务。当deal方法抛出异常后,spring会将该事务标记为回滚,但是异常被test方法捕获了,因此test方法返回时想要提交事务,由此发生了冲突!
去掉@Transactional注解或者加上@EnableAsync和@Async,该问题都不会复现。
参考资料:https://www.jianshu.com/p/f3aeda1d262a
系统源码分析
@Service
public class TenementServiceImpl implements TenementApi {
@Autowired
EleBusinessLicenseApi eleBusinessLicenseApi;
@Transactional
@Override
public ResponseVo saveCompanyTenementAndAuth {
eleBusinessLicenseApi.notifyPlat(cubaCompanyAuthentication, cubaUser);
}
}
@Service
@Slf4j
public class EleBusinessLicenseServiceImpl implements EleBusinessLicenseApi {
@Autowired
CompanyApi companyApi;
@Override
public void notifyPlat(CubaCompanyAuthentication authenticationDB, CubaUser cubaUser) {
try {
companyApi.asyncNotifyPlat(url);
} catch (Exception throwable) {
log.error("notifyPlat fail : ", throwable);
}
}
}
@Service
@Transactional
@Slf4j
public class CompanyApiImpl implements CompanyApi {
@Async
@Override
@SneakyThrows
public void asyncNotifyPlat( String url) {
HttpClientVo httpClientVo = new HttpClientVo();
httpClientVo.setMimeType("application/x-www-form-urlencoded");
httpClientVo.setUrl(url);
HttpResVo httpResVo = HttpClientUtils.post4Code(httpClientVo, paramMap);
log.info(url+"回调返回结果:"+httpResVo.toString());
}
}
可以看出的是业务处理后想异步回调业务平台,通知业务变化,但由于通知异常导致系统报错。
原因分析
通过日志和源码,会有两个疑问:
即log.error("notifyPlat fail : ", throwable);为什么能打印来自asyncNotifyPlat方法抛出的异常?难道这是@Async的特性?
- 既然主线程catch了异常,并且方法asyncNotifyPlat不仅有@Async而且并没有事务注解,为什么会出现事务相关的问题?为什么调用一个普通方法而且捕获了异常,却导致事务问题呢?
问题1
异步压根没生效!该方法所在服务压根没开启@EnableAsync,所以@Async压根没生效!
问题2
asyncNotifyPlat所在类CompanyApiImpl 上统一标注了@Transactional;
也就是说事务是传播到了asyncNotifyPlat方法内,该方法抛出异常,导致事务被标记为回滚。
解决方案
- 异步
- 去掉事务注解