一 问题的出现&产生背景
媒资的视频关联相关字幕时,在视频已经推送至翻译系统的前提下,需要将字幕通过http接口推送至翻译系统。同时,如果改视频如果已经至乐高,则将字幕也同步至乐高(RPC接口)。
由于功能依赖于第三方服务,整体流程较长。且存在第三方服务存在不确定性(超时,服务挂掉) ,第三方服务不应该与保存字幕功能强耦合,故调用第三方服务设计为异步操作,并加入重试机制。
推送至翻译系统的流程为:关联字幕需要先copy至翻译系统对应的fft账号【http接口】,然后将fft返回的fftUUID作为参数投递给翻译系统【http】.
相关代码如下:
/**
* 将字幕推送至翻译系统之前,不判断字幕与视频之间的关联关系,直接推送
* @param videoHomemadeMediumId
* @param subtitleMediumId
*/
private void publishSubtitleToTranslateSystemWithoutJudge(Long videoHomemadeMediumId,Long subtitleMediumId){
Map<String, String> paramMap = new HashMap<>();
HomemadeMedium homemadeMedium = this.homemadeMediumService.findOne(subtitleMediumId);
HomemadeMedium videoHomemadeMedium = this.homemadeMediumService.findOne(videoHomemadeMediumId);
String type = homemadeMedium.getMediumType() == Constants.MEDIUM_TYPE_SUBTITLE ? "2" : "3";//2代表字幕,3代表文本
paramMap.put("type", type);
this.fillProjectInfo(homemadeMedium.getParentId(),paramMap);
String fftUUID = this.homemadeMediumService.translateSystemCopyToFFT(homemadeMedium);
String resourceUrl = Constants.FFT_REQUEST_URL + fftUUID;
paramMap.put("resourceUrl",resourceUrl);
paramMap.put("resourceId", homemadeMedium.getId() + "");
paramMap.put("resourceName", homemadeMedium.getName());
paramMap.put("associationId", videoHomemadeMedium.getId() + "");
boolean result = this.httpRequestService.pushToTranslateSystem(paramMap);
log.info("[PublishInfoServiceImpl][publishSubtitleToTranslateSystem][step=end][type=subtitle][videoHomemadeMediumId={}][subtitleMediumId={}][param={}][result={}]", videoHomemadeMediumId, subtitleMediumId, JSON.toJSONString(paramMap), result);
if(!result){
throw new RuntimeException(String.format("[videoHomemadeMediumId=%s,subtitleMediumId=%s]推送至翻译系统失败",videoHomemadeMediumId,subtitleMediumId));
}
}
由于存储云开通copy的fft账号的token没有分配直接通过quickImport的权限,导致
String fftUUID = this.homemadeMediumService.translateSystemCopyToFFT(homemadeMedium);
报403错误:
出现这个报错是正常的,但是同时出现异常:
问题来了,为什么会出现?
Transaction was marked for rollback only; cannot commit; nested exception is org.hibernate.TransactionException: Transaction was marked for rollback only; cannot commit
二 模拟&问题复现
2.1 模拟问题
com.biz.TransactionalTestApplicationTests#testPushToTranslateSystem
测试调用:
@Test
public void testPushToTranslateSystem(){
Student s = new Student("颜回");
Teacher t = new Teacher("孔子");
this.combineService.pushToTranslateSystem(s,t);
}
combineService:
@org.springframework.transaction.annotation.Transactional(propagation = Propagation.REQUIRED)
@Override
public void pushToTranslateSystem(Student s, Teacher t) {
this.studentService.addStudentRequired(s);//模拟数据库操作:调用第一个服务模拟权限校验,判断是否能推送至翻译系统
try{
this.teacherService.invokeHttpAndPushToTranslateSystem(t);//模拟推送至翻译系统并且在调用时,http接口报错
}catch (Exception e){
logger.error("[pushToTranslateSystem][Teacher={}]", JSON.toJSONString(t),e);
}
}
studentService:
@Transactional(propagation = Propagation.REQUIRED)
@Override
public void addStudentRequired(Student s) {
this.studentRepository.save(s);
}
teacherService:
@Transactional(propagation = Propagation.REQUIRED)
@Override
public void invokeHttpAndPushToTranslateSystem(Teacher t) {
this.teacherRepository.save(t);
this.httpService.notity(t);
}
httpService:
public class HttpServiceImpl implements HttpService {
@Override
public void notity(Teacher t) {
System.out.println(String.format("s=%s", JSONObject.toJSONString(t)));
throw new RuntimeException("报错了!");
}
}
调用报错:
rg.springframework.transaction.TransactionSystemException: Could not commit JPA transaction; nested exception is javax.persistence.RollbackException: Transaction marked as rollbackOnly
at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:526)
报错原因分析:
com.biz.service.combine.CombineServiceImpl#pushToTranslateSystem启用了事务,并且事务的传播行为设置为
propagation = Propagation.REQUIRED。
同时
this.studentService.addStudentRequired(s)
this.teacherService.invokeHttpAndPushToTranslateSystem(t)
传播行为均为:propagation = Propagation.REQUIRED
当调用TeacherServiceImpl.invokeHttpAndPushToTranslateSystem抛出异常后,由于事务的传播机制,transaction中的rollBackOnly标志位被标记为了true,
nested exception is javax.persistence.RollbackException: Transaction marked as rollbackOnly
因此即便是捕获了异常,在上层事务提交的时候,仍然会检查标识位是否设置为rollbackOnly=true,一旦设置为true则会抛出异常。
源码:org.hibernate.jpa.internal.TransactionImpl#commit
public void commit() {
if ( tx == null || tx.getStatus() != TransactionStatus.ACTIVE ) {
throw new IllegalStateException( "Transaction not active" );
}
if ( rollbackOnly ) {//当标志位已经被标记为true时候,事务先回滚,随后抛出异常。
tx.rollback();
throw new RollbackException( "Transaction marked as rollbackOnly" );
}
try {
tx.commit();
}
catch (Exception e) {
Throwable wrappedException;
if ( e instanceof PersistenceException ) {
Throwable cause = e.getCause() == null ? e : e.getCause();
if ( cause instanceof HibernateException ) {
wrappedException = entityManager.convert( (HibernateException) cause );
}
else {
wrappedException = cause;
}
}
else if ( e instanceof HibernateException ) {
wrappedException = entityManager.convert( (HibernateException) e );
}
else {
wrappedException = e;
}
try {
//as per the spec we should rollback if commit fails
tx.rollback();
}
catch (Exception re) {
//swallow
}
throw new RollbackException( "Error while committing the transaction", wrappedException );
}
finally {
rollbackOnly = false;
}
//if closed and we commit, the mode should have been adjusted already
//if ( entityManager.isOpen() ) entityManager.adjustFlushMode();
}
2.2 问题思考
如果我们将嵌套事务设置于CombineService内部,是否还会抛出Transaction marked as rollbackOnly 异常?为什么?
测试代码:
com.biz.TransactionalTestApplicationTests#testPushToTranslateSystemInner
三 解决方式
方法一(当前采用的方案)在最外层try …catch异常,而不是在服务层try catch,整个服务整体抛出异常后再进行重试。
方法二 如果一定要在方法层将嵌套的事务传播行为设置为PROPAGATION_NOT_SUPPORTED,使得嵌套方法以非事务的方式运行,当嵌套方法执行时候,挂起事务,执行完毕,重新恢复事务。
四 事务的传播行为
4.1 基本概念
**事务的传播行为的基本概念:**事务传播行为用来描述由某一个事务传播行为修饰的方法被嵌套进另一个方法的时事务如何传播。
4.2 事务传播行为分类
PROPAGATION_REQUIRED–支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。
PROPAGATION_SUPPORTS–支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY–支持当前事务,如果当前没有事务,就抛出异常。
PROPAGATION_REQUIRES_NEW–新建事务,如果当前存在事务,把当前事务挂起。
PROPAGATION_NOT_SUPPORTED–以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER–以非事务方式执行,如果当前存在事务,则抛出异常。
五 参考链接
https://juejin.im/entry/5a8fe57e5188255de201062b
https://blog.csdn.net/wwh578867817/article/details/51736723
ATION_NOT_SUPPORTED–以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER–以非事务方式执行,如果当前存在事务,则抛出异常。