天天看点

项目难点——【1】事务控制@Transactional项目难点——【1】事务控制@Transactional

项目难点——【1】事务控制@Transactional

1 现状

做项目的时候需要涉及到入库的操作,考虑到后面的场景,此处应该做事务控制,于是在uploadFile接口上添加了

@Transactional

的注解。可以成功控制事务【需要抛出异常,不能只打印日志

throw new RuntimeException(e.getMessage());

service的接口如下:

@Service
@Slf4j
public class MediaFileServiceImpl implements MediaFileService {
	@Override
	public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, byte[] bytes, String folder, String objectName) {
			....
	    try {
	    //调用私有方法,将文件上传到MinIO
	    addMediaFileToMinIO();
	    
	      //调用私有方法addMediaFileToDB,将文件上传到数据库
	      addMediaFileToDB();
	    } catch (Exception e) {
	        log.debug("上传文件失败:{}", e.getMessage());
	    }
	    //return null;
	}
	
	
	
	//将文件上传到分布式文件系统MinIO
	private void addMediaFileToMinIO(byte[] bytes, String bucket, String objectName) {
		....
	}
	
	//将文件上传到数据库
	private MediaFiles addMediaFileToDB(Long companyId, String fileId, UploadFileParamsDto uploadFileParamsDto, String bucket, String objectName) {
		...
	}

}
           
  1. 但是接口中的操作涉及到两部分,一部分是调用私有方法将文件上传到分布式文件系统(

    addMediaFileToMinIO

    )。另一部分是调用私有方法将文件存储到数据库(

    addMediaFileToDB

    )。
此时涉及到一个优化,因为上传到分布式文件系统受网络情况影响。因此考虑到接口性能,将

@Trasactional

加到

addMediaFileToDB

方法中,这样就减小了事务控制的范围,就算受到网络影响没有上传成功到MinIO,依然可以通过重试的方式。
  1. 于是,代码如下:
@Service
@Slf4j
public class MediaFileServiceImpl implements MediaFileService {
	@Override
	public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, byte[] bytes, String folder, String objectName) {
			....
	    try {
	    //调用私有方法,将文件上传到MinIO
	    addMediaFileToMinIO();
	    
	      //调用私有方法addMediaFileToDB,将文件上传到数据库
	      addMediaFileToDB();
	    } catch (Exception e) {
	        log.debug("上传文件失败:{}", e.getMessage());
	    }
	    //return null;
	}
	
	
	
	//将文件上传到分布式文件系统MinIO
	private void addMediaFileToMinIO(byte[] bytes, String bucket, String objectName) {
		....
	}
	
	
	//将文件上传到数据库
	@Transactional
	private MediaFiles addMediaFileToDB(Long companyId, String fileId, UploadFileParamsDto uploadFileParamsDto, String bucket, String objectName) {
		...
	}

}
           
结果发现,Spring并没有帮助我们完成事务控制

分析:

①Spring帮助我们完成事务控制需要两个条件

- 通过代理对象调用
- 方法上添加@Transactional注解
           
第二个条件已经满足,现在就是判断第一个条件是否满足,我们知道方法中如果直接调用私有方法,是可以省略

this

的(

this.addMediaFileToDB();

),现在只需要判断this是否是代理对象即可
public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, byte[] bytes, String folder, String objectName) {
			....
	    try {
	    //调用私有方法,将文件上传到MinIO
	    addMediaFileToMinIO();
	    
	      //调用私有方法addMediaFileToDB,将文件上传到数据库
	      System.out.println(this);
	      addMediaFileToDB();
	    } catch (Exception e) {
	        log.debug("上传文件失败:{}", e.getMessage());
	    }
	    //return null;
	}
           
在上传文件接口的位置之前,打印这个this即可: System.out.println(this);

通过debug调试:

项目难点——【1】事务控制@Transactional项目难点——【1】事务控制@Transactional
结果发现并不是Spring的对象完成,因此没有完成事务控制

②如果在uploadFile方法上添加@Transactional注解,代理对象执行此方法前会开启事务,如下图:

项目难点——【1】事务控制@Transactional项目难点——【1】事务控制@Transactional

③如果在uploadFile方法上没有@Transactional注解,代理对象执行此方法前不进行事务控制,如下图:

项目难点——【1】事务控制@Transactional项目难点——【1】事务控制@Transactional

④加上我们开始已经知道了

uploadFile方法中去调用addMediaFilesToDb方法不是通过代理对象去调用

解决思路:

  • 注入MediaService(注入自己,变为代理对象)
  • 将addMediaFilesToDb抽成为接口
  • addMediaFilesToDb方法上添加

    @Transactional

2 解决办法

2.1 注入MediaFileService的代理对象

在MediaFileService的实现类中注入MediaFileService的代理对象,如下:

@Autowired
MediaFileService currentProxy;
           

2.2 将事务代理方法抽成为接口

将addMediaFilesToDb方法提成接口
@Transactional
MediaFiles addMediaFileToDB(Long companyId, String fileId, UploadFileParamsDto uploadFileParamsDto, String bucket, String objectName);
           

2.3 改为代理对象调用被事务控制的接口

调用addMediaFilesToDb方法的代码处改为如下:
try {
	.....
    //写入文件表
    mediaFiles = currentProxy.addMediaFilesToDb(companyId,fileId,uploadFileParamsDto,bucket_Files,objectName);
    ....
           

2.4 代码如下

MediaFileService:

public interface MediaFileService {
    /**
     * 上传文件通用接口
     * @param companyId
     * @param uploadFileParamsDto
     * @param bytes
     * @param folder
     * @param objectName
     * @return
     */
    UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, byte[] bytes, String folder, String objectName);


    /**
     * 上传文件到数据库
     * @param companyId
     * @param fileId
     * @param uploadFileParamsDto
     * @param bucket
     * @param objectName
     * @return
     */
    @Transactional
    MediaFiles addMediaFileToDB(Long companyId, String fileId, UploadFileParamsDto uploadFileParamsDto, String bucket, String objectName);
}
           

MediaFileServiceImpl:

@Service
@Slf4j
public class MediaFileServiceImpl implements MediaFileService {

    @Autowired
    MediaFilesMapper mediaFilesMapper;

    @Autowired
    MinioClient minioClient;

    //注入代理对象,用来控制事务【抽取方法为接口,被@Service标识和@Autowired的对象,自动被spring管理,成为代理对象】
    @Autowired
    MediaFileService currentProxy;

    @Value("${minio.bucket.files}")
    private String bucket_files;

    /**
     * 上传文件通用接口
     *
     * @param companyId
     * @param uploadFileParamsDto
     * @param bytes
     * @param folder
     * @param objectName
     * @return
     */

    @Override
    public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, byte[] bytes, String folder, String objectName) {
      ...
        try {
            //上传到MinIO【文件夹路径+文件名】
            addMediaFileToMinIO(bytes, bucket_files, objectName);

            //保存到数据库[存储文件使用的是md5值],改用代理对象调用
            MediaFiles mediaFiles = currentProxy.addMediaFileToDB(companyId, fileMd5, uploadFileParamsDto, bucket_files, objectName);
            //准备返回数据
     	...
            return uploadFileResultDto;
        } catch (Exception e) {
            log.debug("上传文件失败:{}", e.getMessage());
            throw new RuntimeException(e.getMessage());
        }
        //return null;
    }

    //上传文件到文件系统
    private void addMediaFileToMinIO(byte[] bytes, String bucket, String objectName) {
      	//上传文件到MinIO
        } catch (Exception e) {
            e.printStackTrace();
            log.debug("上传文件到文件系统出错:{}", e.getMessage());
            XcPlusException.cast("上传文件到文件系统出错");
        }
    }


    //上传文件到数据库
    @Override
    @Transactional
    public MediaFiles addMediaFileToDB(Long companyId, String fileId, UploadFileParamsDto uploadFileParamsDto, String bucket, String objectName) {
        ...
            //插入文件表
            int insert = mediaFilesMapper.insert(mediaFiles);
            //手动添加异常,测试效果
            int i = 1 / 0;
            //if(insert < 0){
            //    XcPlusException.cast("保存文件信息到数据库失败");
            //}
        }
        return mediaFiles;

    }
}
           
手动在addMediaFileToDB方法中添加异常,测试效果
项目难点——【1】事务控制@Transactional项目难点——【1】事务控制@Transactional

通过md5值查询,同一个文件md5值相同,可以先成功上传一次,记录下md5值,然后删除记录

项目难点——【1】事务控制@Transactional项目难点——【1】事务控制@Transactional

3 总结

  1. Spring事务控制条件
- 想要被事务控制的方法添加@Transactional注解
- 需要是代理对象调用
//如果有异常需要抛出异常
           
  1. 解决办法
1. 通过代理对象调【注入自己】
2. 将方法抽成为接口
3. 接口上添加@Transactional
           

4 bug:CodeGenerationException’ exception. Cannot evaluate com.xuecheng.media.service.impl.MediaFileServiceImp

Method threw ‘org.springframework.cglib.core.CodeGenerationException’ exception. Cannot evaluate com.xuecheng.media.service.impl.MediaFileServiceImpl E n h a n c e r B y S p r i n g C G L I B EnhancerBySpringCGLIB EnhancerBySpringCGLIB9ee40a27.toString()

未解决。