mySpace项目遇到的问题

声明式事务:

在实现下载进度展示的时候发现:原先的代码需要等待所有的操作完成后才会将数据持久化到数据库中,这样子导致前端无法查询下载进度

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的三级缓存进行处理

除了上面的这种操作之后也可以通过手动补偿机制或是嵌套事务解决,这里就大家自己研究,方法肯定还是有很多的,结合自己的项目挑选合适的就行。

相关推荐
Victor3563 分钟前
MongoDB(2)MongoDB与传统关系型数据库的主要区别是什么?
后端
JaguarJack4 分钟前
PHP 应用遭遇 DDoS 攻击时会发生什么 从入门到进阶的防护指南
后端·php·服务端
BingoGo5 分钟前
PHP 应用遭遇 DDoS 攻击时会发生什么 从入门到进阶的防护指南
后端
Victor3566 分钟前
MongoDB(3)什么是文档(Document)?
后端
牛奔2 小时前
Go 如何避免频繁抢占?
开发语言·后端·golang
想用offer打牌7 小时前
MCP (Model Context Protocol) 技术理解 - 第二篇
后端·aigc·mcp
KYGALYX8 小时前
服务异步通信
开发语言·后端·微服务·ruby
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
爬山算法9 小时前
Hibernate(90)如何在故障注入测试中使用Hibernate?
java·后端·hibernate
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端