声明式事务:
在实现下载进度展示的时候发现:原先的代码需要等待所有的操作完成后才会将数据持久化到数据库中,这样子导致前端无法查询下载进度
ini
@Override
@Transactional
public BaseResponse<DownloadTaskAddVO> initDownloadTask(DownloadTaskAddRequest downloadTaskAddRequest, HttpServletRequest request) {
String userId = downloadTaskAddRequest.getUserId();
String targetId = downloadTaskAddRequest.getTargetId();
String type = downloadTaskAddRequest.getType();
// String zipTaskId = downloadTaskAddRequest.getZipTaskId();
// 鉴权
String loginUserId = userService.getLoginUser(request).getId().toString();
if (!loginUserId.equals(userId)) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "你无权操作");
}
// 判断一下要下载的文件或是文件夹是否是存在的
File file = fileService.getById(targetId);
if (file == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "要下载的文件或文件夹不存在");
}
Downloadtask downloadtask = new Downloadtask();
downloadtask.setUserId(userId);
downloadtask.setTargetId(targetId);
downloadtask.setType(type);
downloadtask.setExpireTime(LocalDateTime.now().plusDays(7));
if (!this.save(downloadtask)) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "数据插入失败");
}
// 计算要下载的文件(夹)总字节大小
long totalSize = file.getSize();
// 判断是否是文件夹
if (file.getIsFolder() == 1) {
totalSize = 0;
// 初始化一下压缩任务
String taskId = ziptaskService.initTask(loginUserId, targetId);
LambdaUpdateWrapper<Downloadtask> updateWrapper =
new LambdaUpdateWrapper<Downloadtask>()
.eq(Downloadtask::getTaskId, downloadtask.getTaskId())
.set(Downloadtask::getZipTaskId, taskId);
if (!this.update(updateWrapper)) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "zipTaskId记录失败");
}
// 对文件夹进行压缩
GetFolderZipResult data = fileService.getFolderZipById(downloadtask.getTaskId(), targetId, request).getData();
if (!data.isCompleted()) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "ZIP 文件尚未生成完成");
}
String url = data.getUrl();
// 完善一下压缩任务
boolean update = ziptaskService.updateField(taskId, url);
if (!update) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "下载任务完善失败");
}
String key = FilePathUtil.parseStoragePath(url);
COSClient cosClient = cosClientConfig.cosClient();
COSObject object = cosClient.getObject(cosClientConfig.getBucket(), key);
long fileSizeInBytes = object.getObjectMetadata().getContentLength();
totalSize = fileSizeInBytes;
}
LambdaUpdateWrapper<Downloadtask> updateWrapper = new LambdaUpdateWrapper<Downloadtask>()
.eq(Downloadtask::getTaskId, downloadtask.getTaskId())
.set(Downloadtask::getTotalSize, totalSize);
if (!this.update(updateWrapper)) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "文件大小更新失败");
}
// 大于10M,提前初始化逻辑分片
if (totalSize >= 10 * 1024 * 1024) {
List<Downloadchunk> chunks = new ArrayList<>();
// 计算分片数量(向上取整)
long totalChunks = (totalSize + CHUNK_SIZE - 1) / CHUNK_SIZE;
// 生成分片记录
for (int i = 0; i < totalChunks; i++) {
long startOffset = i * CHUNK_SIZE;
long endOffset = Math.min(startOffset + CHUNK_SIZE, totalSize) - 1;
long actualChunkSize = endOffset - startOffset + 1;
Downloadchunk chunk = new Downloadchunk();
chunk.setTaskId(downloadtask.getTaskId());
// chunk.setChunkNumber(i + 1); // 序号从1开始
chunk.setChunkNumber(i); // 序号从0开始
chunk.setChunkSize(actualChunkSize);
chunk.setStartOffset(startOffset);
chunk.setEndOffset(endOffset);
chunk.setResourceType(file.getIsFolder() == 1 ? "folder" : "file");
chunk.setStatus("pending");
chunks.add(chunk);
}
// 批量保存分片记录
if (!chunks.isEmpty() && !downloadchunkService.saveBatch(chunks)) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "分片初始化失败");
}
}
// 封装返回类型
DownloadTaskAddVO downloadTaskAddVO = new DownloadTaskAddVO();
downloadTaskAddVO.setDownloadTaskId(downloadtask.getTaskId());
downloadTaskAddVO.setTotalSize(totalSize);
return ResultUtils.success(downloadTaskAddVO);
}
一开始以为是插入顺序的问题导致要整个流程执行完才会将数据持久化到数据库中,然后询问ai,发现给出的代码其实还是一样的,还是会导致开头的问题
scss
@Override
@Transactional
public BaseResponse<DownloadTaskAddVO> initDownloadTask(DownloadTaskAddRequest downloadTaskAddRequest, HttpServletRequest request) {
String userId = downloadTaskAddRequest.getUserId();
String targetId = downloadTaskAddRequest.getTargetId();
String type = downloadTaskAddRequest.getType();
// 鉴权
String loginUserId = userService.getLoginUser(request).getId().toString();
if (!loginUserId.equals(userId)) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "无权操作");
}
// 校验目标文件存在性
File file = fileService.getById(targetId);
if (file == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "下载目标不存在");
}
// --- 核心修改点:立即初始化下载任务并保存 ---
Downloadtask downloadtask = new Downloadtask();
downloadtask.setUserId(userId);
downloadtask.setTargetId(targetId);
downloadtask.setType(type);
downloadtask.setExpireTime(LocalDateTime.now().plusDays(7));
if (!this.save(downloadtask)) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "任务创建失败");
}
// --- 后续步骤:基于已创建的下载任务进行处理 ---
try {
long totalSize = file.getSize();
String zipTaskId = null;
// 处理文件夹类型
if (file.getIsFolder() == 1) {
// 初始化压缩任务
zipTaskId = ziptaskService.initTask(loginUserId, targetId);
// 绑定压缩任务ID到下载任务
downloadtask.setZipTaskId(zipTaskId);
if (!this.updateById(downloadtask)) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "任务更新失败");
}
// 生成ZIP并获取实际大小
GetFolderZipResult zipResult = fileService.getFolderZipById(downloadtask.getTaskId(), targetId, request).getData();
if (!zipResult.isCompleted()) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "ZIP未就绪");
}
String url = zipResult.getUrl();
ziptaskService.updateField(zipTaskId, url); // 更新压缩任务URL
// 从COS获取ZIP文件大小
String cosKey = FilePathUtil.parseStoragePath(url);
COSObject cosObject = cosClientConfig.cosClient().getObject(cosClientConfig.getBucket(), cosKey);
totalSize = cosObject.getObjectMetadata().getContentLength();
}
// 更新任务总大小
downloadtask.setTotalSize(totalSize);
if (!this.updateById(downloadtask)) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "大小更新失败");
}
// 大文件分片预处理
if (totalSize >= 10 * 1024 * 1024) {
initializeChunks(downloadtask.getTaskId(), totalSize, file.getIsFolder() == 1);
}
// 构造响应
DownloadTaskAddVO vo = new DownloadTaskAddVO();
vo.setDownloadTaskId(downloadtask.getTaskId());
vo.setTotalSize(totalSize);
return ResultUtils.success(vo);
} catch (Exception e) {
// 异常时清理任务(可选)
this.removeById(downloadtask.getTaskId());
throw e;
}
}
// 分片初始化逻辑封装
private void initializeChunks(String taskId, long totalSize, boolean isFolder) {
List<Downloadchunk> chunks = new ArrayList<>();
long chunkCount = (totalSize + CHUNK_SIZE - 1) / CHUNK_SIZE;
for (int i = 0; i < chunkCount; i++) {
long start = i * CHUNK_SIZE;
long end = Math.min(start + CHUNK_SIZE, totalSize) - 1;
Downloadchunk chunk = new Downloadchunk();
chunk.setTaskId(taskId);
chunk.setChunkNumber(i);
chunk.setStartOffset(start);
chunk.setEndOffset(end);
chunk.setChunkSize(end - start + 1);
chunk.setResourceType(isFolder ? "folder" : "file");
chunk.setStatus("pending");
chunks.add(chunk);
}
if (!chunks.isEmpty() && !downloadchunkService.saveBatch(chunks)) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "分片初始化失败");
}
}
发现这个代码其实和原来的代码的执行流程其实基本是差不多,都是先将下载任务先保存到数据库中
但是执行发现其实虽然执行的过程中会有相应执行的sql产生,但是其实并没有真正的将数据持久化到数据库中
最后一番排查之后发现其实是声明式事务搞的鬼

把它注销掉之后就会发现能够提前将数据持久化到数据库中了
虽然但是能达到我们的目的,但是听起来就很离谱,怎么可能通过这么不聪明的方法来实现这个目的
所以还要另寻他法
这里先回顾一下声明式事务:
SpringAop 的声明式事务:
- 开始 :当
initDownloadTask
方法被调用时,Spring 会开启一个事务。 - 执行 :所有数据库操作(如
this.save(downloadtask)
)会生成 SQL 语句并缓存在事务上下文中,但不会立即写入数据库。 - 提交/回滚:
-
- 如果方法正常执行完成,事务提交,所有 SQL 一次性写入数据库。
- 如果方法抛出异常,事务回滚,所有 SQL 被丢弃,数据库保持原状。
也就是说这里的 this.save(downloadtask) 不会真正写入数据库
那么肯定有人要问了,我不想自己手动编写事务,那么能不能直接将这两步拆分两个方法,然后把 his.save(downloadtask) 下面的逻辑拆成另外一个方法,然后在这个方法中直接去调用另外一个方法,然后在另外一个方法去加事务,这个方法不加事务
首先回答一下,这个肯定是可以的,但是有一个前提就是这两个方法必须写在两个类中
由于 Spring 的 AOP 代理机制是基于 类级别 的动态代理,自调用(同一类中方法A调用方法B)不会触发事务。
例如:
typescript
@Service
public class MyService {
// 方法A:无事务注解
public void methodA() {
methodB(); // 调用有事务注解的方法B
}
// 方法B:有事务注解
@Transactional
public void methodB() {
// 数据库操作...
}
}
typescript
@Service
public class ServiceA {
@Autowired
private ServiceB serviceB;
// 方法A:无事务注解
public void methodA() {
serviceB.methodB(); // 调用另一个Service的事务方法
}
}
@Service
public class ServiceB {
// 方法B:有事务注解
@Transactional
public void methodB() {
// 数据库操作...
}
}
那如果要确保同一个类中还是事务生效的话:通过代理对象调用,而不是自己调用:(不太推荐)
typescript
@Service
public class MyService {
@Autowired
private MyService selfProxy; // 通过代理对象调用方法B
public void methodA() {
selfProxy.methodB(); // 通过代理调用,触发事务
}
@Transactional
public void methodB() {
// 数据库操作...
}
}
那么我们可以怎么做呢?
其实也很好做,首先我们再创建一个新的类,然后将提前持久化的代码放在这个类中,然后进行当注册一个事务同步器来监听主事务的回滚操作
ini
@Override
@Transactional
public BaseResponse<DownloadTaskAddVO> initDownloadTask(DownloadTaskAddRequest downloadTaskAddRequest, HttpServletRequest request) {
String userId = downloadTaskAddRequest.getUserId();
String targetId = downloadTaskAddRequest.getTargetId();
String type = downloadTaskAddRequest.getType();
// String zipTaskId = downloadTaskAddRequest.getZipTaskId();
// 鉴权
String loginUserId = userService.getLoginUser(request).getId().toString();
if (!loginUserId.equals(userId)) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "你无权操作");
}
// 判断一下要下载的文件或是文件夹是否是存在的
File file = fileService.getById(targetId);
if (file == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "要下载的文件或文件夹不存在");
}
Downloadtask downloadtask = new Downloadtask();
downloadtask.setUserId(userId);
downloadtask.setTargetId(targetId);
downloadtask.setType(type);
downloadtask.setExpireTime(LocalDateTime.now().plusDays(7));
System.out.println("执行到这里了1");
// 强制保存数据
independentService.saveDownloadTask(downloadtask);
// 注册事务同步器,监听主事务的回滚事件
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCompletion(int status) {
if (status == TransactionSynchronization.STATUS_ROLLED_BACK) {
// 主事务回滚,触发删除操作
independentService.deleteDownloadTaskInNewTransaction(downloadtask.getTaskId());
}
}
});
// 计算要下载的文件(夹)总字节大小
long totalSize = file.getSize();
// 判断是否是文件夹
if (file.getIsFolder() == 1) {
totalSize = 0;
// 初始化一下压缩任务
String taskId = ziptaskService.initTask(loginUserId, targetId);
LambdaUpdateWrapper<Downloadtask> updateWrapper =
new LambdaUpdateWrapper<Downloadtask>()
.eq(Downloadtask::getTaskId, downloadtask.getTaskId())
.set(Downloadtask::getZipTaskId, taskId);
if (!this.update(updateWrapper)) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "zipTaskId记录失败");
}
// 对文件夹进行压缩
long start = System.currentTimeMillis();
GetFolderZipResult data = fileService.getFolderZipById(downloadtask.getTaskId(), targetId, request).getData();
long end = System.currentTimeMillis();
System.out.println("执行压缩文件消耗的时间是:" + (end - start));
if (!data.isCompleted()) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "ZIP 文件尚未生成完成");
}
String url = data.getUrl();
// 完善一下压缩任务
boolean update = ziptaskService.updateField(taskId, url);
if (!update) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "下载任务完善失败");
}
String key = FilePathUtil.parseStoragePath(url);
COSClient cosClient = cosClientConfig.cosClient();
COSObject object = cosClient.getObject(cosClientConfig.getBucket(), key);
long fileSizeInBytes = object.getObjectMetadata().getContentLength();
totalSize = fileSizeInBytes;
}
LambdaUpdateWrapper<Downloadtask> updateWrapper = new LambdaUpdateWrapper<Downloadtask>()
.eq(Downloadtask::getTaskId, downloadtask.getTaskId())
.set(Downloadtask::getTotalSize, totalSize);
if (!this.update(updateWrapper)) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "文件大小更新失败");
}
// 大于10M,提前初始化逻辑分片
if (totalSize >= 10 * 1024 * 1024) {
List<Downloadchunk> chunks = new ArrayList<>();
// 计算分片数量(向上取整)
long totalChunks = (totalSize + CHUNK_SIZE - 1) / CHUNK_SIZE;
// 生成分片记录
for (int i = 0; i < totalChunks; i++) {
long startOffset = i * CHUNK_SIZE;
long endOffset = Math.min(startOffset + CHUNK_SIZE, totalSize) - 1;
long actualChunkSize = endOffset - startOffset + 1;
Downloadchunk chunk = new Downloadchunk();
chunk.setTaskId(downloadtask.getTaskId());
chunk.setChunkNumber(i); // 序号从0开始
chunk.setChunkSize(actualChunkSize);
chunk.setStartOffset(startOffset);
chunk.setEndOffset(endOffset);
chunk.setResourceType(file.getIsFolder() == 1 ? "folder" : "file");
chunk.setStatus("pending");
chunks.add(chunk);
}
// 批量保存分片记录
if (!chunks.isEmpty() && !downloadchunkService.saveBatch(chunks)) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "分片初始化失败");
}
}
// 封装返回类型
DownloadTaskAddVO downloadTaskAddVO = new DownloadTaskAddVO();
downloadTaskAddVO.setDownloadTaskId(downloadtask.getTaskId());
downloadTaskAddVO.setTotalSize(totalSize);
return ResultUtils.success(downloadTaskAddVO);
}
typescript
@Service
public class IndependentServiceImpl implements IndependentService {
@Resource
private DownloadtaskService downloadtaskService;
/**
* 将数据持久化到数据库中
* @param downloadtask
*/
@Transactional(propagation = Propagation.REQUIRES_NEW) // 开启新事务
public void saveDownloadTask(Downloadtask downloadtask) {
System.out.println("执行到这里了2");
if (!downloadtaskService.save(downloadtask)) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "数据插入失败");
}
}
/**
* 开启新线程删除事务
* @param taskId
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void deleteDownloadTaskInNewTransaction(String taskId) {
downloadtaskService.removeById(taskId);
}
}
不过要注意循环依赖问题,可以通过 @Lazy 注解开启spring的三级缓存进行处理
除了上面的这种操作之后也可以通过手动补偿机制或是嵌套事务解决,这里就大家自己研究,方法肯定还是有很多的,结合自己的项目挑选合适的就行。