项目难点——【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调试:
在这里插入图片描述

结果发现并不是Spring的对象完成,因此没有完成事务控制

②如果在uploadFile方法上添加@Transactional注解,代理对象执行此方法前会开启事务,如下图:
在这里插入图片描述
③如果在uploadFile方法上没有@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方法中添加异常,测试效果

在这里插入图片描述
通过md5值查询,同一个文件md5值相同,可以先成功上传一次,记录下md5值,然后删除记录
在这里插入图片描述

3 总结

  1. Spring事务控制条件
- 想要被事务控制的方法添加@Transactional注解
- 需要是代理对象调用
- @Transactional控制的方法需要是public
//如果有异常需要抛出异常,否则不会回滚事务
  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()

未解决。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值