springboot中的事务是由AOP代理进行的,首先代理开启事务,执行自身代码,提交事务。当非事务方法内调用类内的事务方法时,会发生事务失效。因为这样的调用是通过this调用的,this并不是代理对象。

在软工学习项目的上传图片模块,需要将图片上传到minio,并保存信息到数据库。由于上传到minio是网络请求,可能会消耗大量的时间而导致事务一直无法提交,消耗数据库资源。因此将事务注解@Transactional加在保存数据库的方法上(为了提高代码复用性,单独抽离成了一个方法)。但这就导致了事务失效的问题。

1
2
3
4
5
6
7
8
9
10
11
public UploadFileResultDto uploadFile(Long companyId, MultipartFile filedata){
//..
//入库文件信息
MediaFiles mediaFiles = addMediaFilesToDb(companyId, FileMd5,uploadFileParamsDto,bucket_mediafiles, objectName);
// ...
}

@Transactional
public MediaFiles addMediaFilesToDb(Long companyId,String fileMd5,UploadFileParamsDto uploadFileParamsDto,String bucket,String objectName){
// ...
}

解决事务失效:

  • 注入自身的依赖
1
2
3
4
5
6
@Slf4j
@Service
public class MediaFileServiceImpl implements MediaFileService {
@Autowired
MediaFileService currentProxy;
}

此方法还需要在接口类(即MediaFileService)中暴露你需要调用的方法。

1
2
3
public interface MediaFileService {
public MediaFiles addMediaFilesToDb(Long companyId,String fileMd5,UploadFileParamsDto uploadFileParamsDto,String bucket,String objectName);
}

这样通过currentProxy调用类内的事务方法,就可以避免事务失效,因为此时currentProxy对象是代理对象。

1
2
3
4
5
6
public UploadFileResultDto uploadFile(Long companyId, MultipartFile filedata){
//..
//入库文件信息
MediaFiles mediaFiles = currentProxy.addMediaFilesToDb(companyId, FileMd5,uploadFileParamsDto,bucket_mediafiles, objectName);
// ...
}

缺点是可能造成循环依赖,springboot可以解决比较简单的循环依赖,但在比较复杂的情景下可能会失效。

  • 新建一个类

将数据库方法抽离到另一个service类中,也可以避免事务失效

  • 使用编程式事务
1
2
3
4
5
6
7
8
9
10
11
12
public class OriginalService {
@Resource
private TransactionTemplate transactionTemplate;

public UploadFileResultDto uploadFile(...) {
// ...
MediaFiles mediaFiles = transactionTemplate.execute(status -> {
return addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket, objectName);
});
// ...
}
}
  • 使用AspectJ模式的事务

在Spring配置中启用AspectJ模式:

1
@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)