总结点 什么情况下会导致事务失效

代理对象调用
方法上添加Transcation 注解 采用声明式事务
1)方法中捕获异常没有抛出
spring控制事务式基于AOP环绕通知实现的,如果方法抛出了异常就会回滚,如果没有抛出就不会生效
2)非事务方法调用事务方法
在内部调用由于不是代理对象,因此失效
3)事务方法调用事务方法
可以被控制 但要选对事务传播行为 只要不是通过代理对象去掉 那么开启一个新事务是不可能的
4)transaction 如果标记的不是public 方法也不可以
5)抛出的异常类型 默认是RuntimeException
6) 数据库本身不支持事务 例如mysql 的myisam存储引擎
7)传播行为导致事务失效 比如某些事务传播行为 压根不支持事务

下面给出一个非事务方法调用事务方法时 不是代理对象的解决方案

方案一 编程式事务
这个就没什么好说的了,网上很多

方案二 声明式事务
首先分析原因式因为没有采用代理对象调用,因为该事务方法与非事务方法定义在一个service中,那么就需要想办法将其变为代理对象调用,我们知道注入的对象为代理对象,因此在本service中注入本service。
但是又引申出另一个问题,本service 注入本service 会造成循环依赖,本循环依赖并不会通过spring 三级缓存解决,需要使用@Lazy 注解延迟加载来打破循环依赖。
补充:什么是三级缓存
第一级 单例对象缓存池,已经实例化并且属性赋值,存放成熟对象
第二级 单例对象缓存池,已经实例化但尚未付属性值,存放半成品对象
第三级 单例工厂的缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
package com.jhj.media.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;
import com.jhj.base.exception.UltimateException;
import com.jhj.base.model.PageParams;
import com.jhj.base.model.PageResult;
import com.jhj.media.mapper.MediaFilesMapper;
import com.jhj.media.model.dto.QueryMediaParamsDto;
import com.jhj.media.model.dto.UploadFileParamsDto;
import com.jhj.media.model.dto.UploadFileResultDto;
import com.jhj.media.model.po.MediaFiles;
import com.jhj.media.service.MediaFileService;
import io.minio.MinioClient;
import io.minio.UploadObjectArgs;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.File;
import java.io.FileInputStream;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;

/**
* @author jhj
* @version 1.0
* @description TODO
* @date 2022/9/10 8:58
*/
@Service
@Slf4j
public class MediaFileServiceImpl implements MediaFileService {
@Autowired
MediaFilesMapper mediaFilesMapper;

@Autowired
MinioClient minioClient;

@Autowired
@Lazy
MediaFileService currentProxy;

//存储普通文件
@Value("${minio.bucket.files}")
private String bucket_mediafiles;

//存储视频
@Value("${minio.bucket.videofiles}")
private String bucket_video;


@Override
public PageResult<MediaFiles> queryMediaFiels(Long companyId, PageParams pageParams, QueryMediaParamsDto queryMediaParamsDto) {

//构建查询条件对象
LambdaQueryWrapper<MediaFiles> queryWrapper = new LambdaQueryWrapper<>();

//分页对象
Page<MediaFiles> page = new Page<>(pageParams.getPageNo(), pageParams.getPageSize());
// 查询数据内容获得结果
Page<MediaFiles> pageResult = mediaFilesMapper.selectPage(page, queryWrapper);
// 获取数据列表
List<MediaFiles> list = pageResult.getRecords();
// 获取数据总数
long total = pageResult.getTotal();
// 构建结果集
PageResult<MediaFiles> mediaListResult = new PageResult<>(list, total, pageParams.getPageNo(), pageParams.getPageSize());
return mediaListResult;

}

@Override
//为什么不在这里加事务 是因为防止minio 网络连接时间长而导致数据库连接超时 占用数据库事务资源
public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, String localFilePath) {
//文件名
String filename = uploadFileParamsDto.getFilename();
//拿到扩展名
String extension = filename.substring(filename.lastIndexOf("."));
//拿到mimeType
String mimeType = getMimeType(extension);
//文件的md5
String fileMd5 = getFileMd5(new File(localFilePath));
//目录
String defaultFolderPath = getDefaultFolderPath();
String objectName = defaultFolderPath + fileMd5 + extension;
//将文件上传Minio
boolean result = addMediaFilesToMinio(localFilePath, mimeType, bucket_mediafiles, objectName);
if(!result){
UltimateException.cast("上传文件失败");
}

//将文件信息保存到数据库
MediaFiles mediaFiles = currentProxy.addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_mediafiles, objectName);
if(mediaFiles==null){
UltimateException.cast("文件上传后,保存信息失败");
}

//返回对象
UploadFileResultDto uploadFileResultDto = new UploadFileResultDto();
BeanUtils.copyProperties(mediaFiles,uploadFileResultDto);
return uploadFileResultDto;
}

//根据扩展名获取mimeType
private String getMimeType(String extension) {
if (extension == null) {
extension = "";
}
ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE; //通用mimeType
if (extensionMatch != null) {
mimeType = extensionMatch.getMimeType();
}
return mimeType;
}

/**
* 将文件上传到minio
* @param localFilePath 本地路径
* @param mimeType 媒体类型
* @param bucket 桶
* @param objectName 文件名
* @return
*/
private boolean addMediaFilesToMinio(String localFilePath, String mimeType, String bucket, String objectName) {
try {
minioClient.uploadObject(
UploadObjectArgs.builder()
.bucket(bucket)
.filename(localFilePath)
.object(objectName)
.contentType(mimeType)
.bucket(bucket).build()
);
log.debug("上传文件到minio成功,bucket:{},objectName:{}",bucket,objectName);
return true;
} catch (Exception e) {
e.printStackTrace();
log.error("上传文件出错,bucket:{},objectName:{},错误信息:{}",bucket,objectName,e.getMessage());
}
return false;
}

private String getDefaultFolderPath(){
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String folder = sdf.format(new Date()).replace("-", "/") + "/";
return folder;
}

private String getFileMd5(File file){
try(FileInputStream fileInputStream=new FileInputStream(file)){
String fileMd5 = DigestUtils.md5Hex(fileInputStream);
return fileMd5;
}catch (Exception e){
e.printStackTrace();
return null;
}
}

/**
* 将文件信息保存到数据库
* @param companyId 机构id
* @param fileMd5 md5
* @param uploadFileParamsDto 文件信息
* @param bucket 桶信息
* @param objectName 路径
* @return
*/
@Transactional
//事务只能用于public 方法 原理是Aop jdk cglib 两种方式 访问不了private
//注意看是不是被代理对象调用 如果不是 则事务不会生效
//同serive 调用同类的事务方法 事务是无法控制的 因为用的是this 并不是代理对象
//注入进去的都是代理对象 this就是对象本身 解决办法 把service 在本类中进行注入
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);
//文件id
mediaFiles.setId(fileMd5);
mediaFiles.setFileId(fileMd5);
//机构id
mediaFiles.setCompanyId(companyId);
//桶
mediaFiles.setBucket(bucket);
//file_path
mediaFiles.setFilePath(objectName);
//url
mediaFiles.setUrl("/"+bucket+"/"+objectName);
//上传时间
mediaFiles.setCreateDate(LocalDateTime.now());
//状态
mediaFiles.setStatus("1");
//审核状态
mediaFiles.setAuditStatus("002003");
//插入数据库
int insert = mediaFilesMapper.insert(mediaFiles);
int i=1/0;
if(insert<0){
log.error("向数据库保存文件失败,bucket:{},objectName:{}",bucket,objectName);
return null;
}
return mediaFiles;
}
return mediaFiles;
}
}