目录
一.业务场景
当前正在做一个图片上传的功能。
图片上传的流程为:
1.前端上传图片,请求服务
2.媒资服务将图片文件上传文件系统Minio
3.构建文件信息(图片地址和其他属性信息)保存到数据库
二.事务的优化
在2中涉及到了网络连接,这个过程是比较耗时的。我们知道,数据库连接是有限的,大概就几十个,如果一个事务长时间无法结束,会直接导致系统崩溃。
所以需要尽可能缩小事务粒度,在本项目中,其实只需要在3上面加事务,所以我们将两个过程拆解开。
可以看到,只在addMediaFilesToDb这个写入数据库的方法上面加了事务控制。
代码如下:
@Override
public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, byte[] bytes, String folder, String objectName) {
//生成文件id,文件的md5值
String fileId = DigestUtils.md5Hex(bytes);
//文件名称
String filename = uploadFileParamsDto.getFilename();
//构造objectname
if (StringUtils.isEmpty(objectName)) {
objectName = fileId + filename.substring(filename.lastIndexOf("."));
}
if (StringUtils.isEmpty(folder)) {
//通过日期构造文件存储路径
folder = getFileFolder(new Date(), true, true, true);
} else if (folder.indexOf("/") < 0) {
folder = folder + "/";
}
//对象名称
objectName = folder + objectName;
MediaFiles mediaFiles = null;
try {
//上传至文件系统
addMediaFilesToMinIO(bytes,bucket_Files,objectName);
//写入文件表 向数据库中写入
// mediaFiles = this.addMediaFilesToDb(companyId,fileId,uploadFileParamsDto,bucket_Files,objectName);
mediaFiles = addMediaFilesToDb(companyId,fileId,uploadFileParamsDto,bucket_Files,objectName);
UploadFileResultDto uploadFileResultDto = new UploadFileResultDto();
BeanUtils.copyProperties(mediaFiles, uploadFileResultDto);
return uploadFileResultDto;
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("上传过程中出错");
}
}
/**
* @description 将文件写入minIO
* @param bytes 文件字节数组
* @param bucket 桶
* @param objectName 对象名称
* @return void
* @author Mr.M
* @date 2022/10/12 21:22
*/
public void addMediaFilesToMinIO(byte[] bytes, String bucket, String objectName) {
//转为流
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
//扩展名
String extension = null;
if(objectName.indexOf(".")>=0){
//文件扩展名
extension = objectName.substring(objectName.lastIndexOf("."));
}
String contentType = getMimeTypeByExtension(extension);
try {
PutObjectArgs putObjectArgs = PutObjectArgs.builder().bucket(bucket).object(objectName)
//-1表示文件分片按5M(不小于5M,不大于5T),分片数量最大10000,
.stream(byteArrayInputStream, byteArrayInputStream.available(), -1)
.contentType(contentType)
.build();
minioClient.putObject(putObjectArgs);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("上传文件到文件系统出错");
}
}
private String getMimeTypeByExtension(String extension){
String contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
if(StringUtils.isNotEmpty(extension)){
ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
if(extensionMatch!=null){
contentType = extensionMatch.getMimeType();
}
}
return contentType;
}
/**
* @description 将文件信息添加到文件表
* @param companyId 机构id
* @param fileMd5 文件md5值
* @param uploadFileParamsDto 上传文件的信息
* @param bucket 桶
* @param objectName 对象名称
* @return com.xuecheng.media.model.po.MediaFiles
* @author Mr.M
* @date 2022/10/12 21:22
*/
@Transactional
public MediaFiles addMediaFilesToDb(Long companyId,String fileMd5,UploadFileParamsDto uploadFileParamsDto,String bucket,String objectName){
//从数据库查询文件
MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
if (mediaFiles == null) {
mediaFiles = new MediaFiles();
//拷贝基本信息
BeanUtils.copyProperties(uploadFileParamsDto, mediaFiles);
mediaFiles.setId(fileMd5);
mediaFiles.setFileId(fileMd5);
mediaFiles.setCompanyId(companyId);
mediaFiles.setUrl("/" + bucket + "/" + objectName);
mediaFiles.setBucket(bucket);
mediaFiles.setFilePath(objectName);
mediaFiles.setCreateDate(LocalDateTime.now());
mediaFiles.setAuditStatus("002003");
mediaFiles.setStatus("1");
//保存文件信息到文件表
int insert = mediaFilesMapper.insert(mediaFiles);
// System.out.println(1/0);
if (insert < 0) {
throw new RuntimeException("保存文件信息失败");
}
}
return mediaFiles;
}
接着来测试一下事务控制是否生效,这里在addMediaFilesToDb中模拟一个除0异常,调用上传图片的接口,很不幸的发现,似乎事务并没有生效,数据依旧插入到了数据库。
三,事务失效
必要条件
我们知道事务生效的两个必要条件
1.方法需要被代理对象来调用
2.方法上面加@Transactional注解(这个不可能出错吧)
纠错
接着打几个断点来调试一下这段程序,
1.在Controller调用uploadFile的位置打上断点,查看service对象是否是代理对象。
发现这里没啥问题。
2.进入service中addMediaFilesToDb方法的实际调用位置,可以看到this并不是一个代理对象。而是就是当前对象,并没有被AOP接管。那么在addMediaFilesToDb出现异常,就无法回滚。
四,问题总结
原理在于Spring的事务管理依靠的动态代理模式,当在同一个类中调用非事务方法,是不会生成代理对象的,自然也不会触发事务。
1)@Transactional Spring 事务注解是基于 Spring AOP 来实现的,而 Spring AOP 又是基于动态代理实现的;
2)动态代理分 JDK 动态代理和 Cglib 动态代理,Spring AOP 是基于 Cglib 动态代理实现的;
3)也就是说 Spring AOP 是在动态代理类中在切点位置切了一刀,去执行而外的处理;
4)非事务方法调事务方法时实际走的不是动态代理类,而是被代理类包裹的实际实现类中自己的方法,所以不会被 Spring AOP 切到,也就导致 @Transactional 事务注解不生效;
那怎么样让代理对象来调用事务方法呢?
五,问题解决
在service类中注入service对象,这个对象本身是代理对象,然后通过它来调用方法。
@Autowired
private MediaFileService mediaFileService;
@Override
public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, byte[] bytes, String folder, String objectName) {
//生成文件id,文件的md5值
String fileId = DigestUtils.md5Hex(bytes);
//文件名称
String filename = uploadFileParamsDto.getFilename();
//构造objectname
if (StringUtils.isEmpty(objectName)) {
objectName = fileId + filename.substring(filename.lastIndexOf("."));
}
if (StringUtils.isEmpty(folder)) {
//通过日期构造文件存储路径
folder = getFileFolder(new Date(), true, true, true);
} else if (folder.indexOf("/") < 0) {
folder = folder + "/";
}
//对象名称
objectName = folder + objectName;
MediaFiles mediaFiles = null;
try {
//上传至文件系统
addMediaFilesToMinIO(bytes, bucket_Files, objectName);
//写入文件表 向数据库中写入
// mediaFiles = this.addMediaFilesToDb(companyId,fileId,uploadFileParamsDto,bucket_Files,objectName);
mediaFiles = mediaFileService.addMediaFilesToDb(companyId, fileId, uploadFileParamsDto, bucket_Files, objectName);
UploadFileResultDto uploadFileResultDto = new UploadFileResultDto();
BeanUtils.copyProperties(mediaFiles, uploadFileResultDto);
return uploadFileResultDto;
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("上传过程中出错");
}
}
六,事务失效场景
1.异常被捕获之后没有抛出(常见)
@Transactional
public void deleteUser() {
userMapper.deleteUserA();
try {
int i = 1 / 0;
userMapper.deleteUserB();
} catch (Exception e) {
e.printStackTrace();
}
}
2.抛出非运行时一场
3.方法内部直接调用(本文所述)
4.新开启一个线程
如下的方式deleteUserA()也不会回滚,因为spring实现事务的原理是通过ThreadLocal把数据库连接绑定到当前线程中,新开启一个线程获取到的连接就不是同一个了。
@Transactional
public void deleteUser() throws MyException{
userMapper.deleteUserA();
try {
//休眠1秒,保证deleteUserA先执行
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
int i = 1/0;
userMapper.deleteUserB();
}).start();
}
5.注解方法是private
6.数据库引擎不支持事务控制(emmmm)