【EasyPan】文件上传、文件秒传、文件转码、文件合并、异步转码、视频切割分析

news2025/4/23 20:55:00

【EasyPan】项目常见问题解答(自用&持续更新中…)汇总版

文件上传方法解析

一、方法总览

@Transactional(rollbackFor = Exception.class)
public UploadResultDto uploadFile(...)

核心能力

  • 秒传验证:通过MD5+文件大小实现文件秒传
  • 分片处理:支持大文件分块上传与合并
  • 空间管理:实时校验用户存储空间
  • 事务保障:数据库操作原子性
  • 异步转码:视频/图片文件后台处理
  • 自动重命名:同名文件自动添加序号

二、模块化解析

1. 秒传处理模块

// 首片触发秒传检查
if (chunkIndex == 0) {
    List<FileInfo> dbFileList = this.fileInfoMapper.selectList(infoQuery);
    if (!dbFileList.isEmpty()) {
        // 空间校验
        if (dbFile.getFileSize() + spaceDto.getUseSpace() > spaceDto.getTotalSpace()) 
        // 创建引用记录
        dbFile.setFileId(fileId);
        this.fileInfoMapper.insert(dbFile);
        // 返回秒传结果
        resultDto.setStatus(UploadStatusEnums.UPLOAD_SECONDS.getCode());
    }
}

设计亮点

  • 仅首片触发查询,减少数据库压力
  • 复用已有文件的物理存储路径(file_path)
  • 原子化更新用户空间

2. 分片处理模块

// 分片暂存逻辑
String tempFolderName = appConfig.getProjectFolder() + Constants.FILE_FOLDER_TEMP;
File newFile = new File(tempFolderName.getPath() + "/" + chunkIndex);
file.transferTo(newFile);

// 临时空间记录
redisComponent.saveFileTempSize(webUserDto.getUserId(), fileId, file.getSize());

// 合并条件判断
if (chunkIndex < chunks - 1) {
    return resultDto.setStatus(UploadStatusEnums.UPLOADING.getCode());
}

关键技术

  • 分片按序号存储:用户ID+文件ID/chunkIndex
  • Redis记录分片累计大小
  • 10MB缓冲区减少IO次数

3. 文件入库模块

FileInfo fileInfo = new FileInfo();
fileInfo.setFilePath(month + "/" + realFileName); // 按月份分目录
fileInfo.setStatus(FileStatusEnums.TRANSFER.getStatus()); // 转码中状态
this.fileInfoMapper.insert(fileInfo);

// 事务提交后触发异步操作
TransactionSynchronizationManager.registerSynchronization(() -> {
    fileInfoService.transferFile(fileId, webUserDto);
});

创新设计

  • 文件路径动态生成:年月目录/用户ID文件ID.后缀
  • 状态机管理:TRANSFER->USING/TRANSFER_FAIL
  • 事务边界控制:确保数据入库后再转码

4. 转码处理模块

@Async
public void transferFile(...) {
    // 合并分片
    union(dirPath, targetFilePath); 
    
    // 视频处理
    if (FileTypeEnums.VIDEO == fileTypeEnum) {
        cutFile4Video(); // HLS切片
        createCover4Video(); // 生成封面
    }
    
    // 更新文件状态
    updateInfo.setStatus(FileStatusEnums.USING.getStatus());
    fileInfoMapper.updateFileStatusWithOldStatus(...);
}

核心技术

  • FFmpeg视频转码:MP4->TS切片+m3u8索引
  • 缩略图生成:视频首帧+图片缩放
  • 异常恢复机制:失败状态可重新触发

三、流程主副线分析

主线流程

存在
不存在
上传首片
秒传检查
创建引用
接收分片
是否末片
合并文件
异步转码
返回上传中

副线处理(异常路径)

异常类型处理方式技术实现
空间不足立即终止Redis实时校验
MD5冲突重新计算文件内容比对
分片丢失断点续传Redis记录进度
转码失败状态标记人工介入恢复

四、时序图解析

用户 前端 服务层 Redis 数据库 文件存储 上传分片0 uploadFile(0) 秒传查询 返回文件记录 空间校验 插入引用记录 返回秒传成功 保存分片0 记录分片大小 返回继续上传 上传分片N uploadFile(N) 保存分片N 更新分片大小 上传末片 uploadFile(last) 合并分片 写入文件记录 更新空间使用 返回上传完成 启动异步转码 alt [秒传成功] [需要上传] 用户 前端 服务层 Redis 数据库 文件存储

五、性能优化策略

  1. 分片并行上传

    • 支持多分片并发上传
    • 分片大小动态调整(2MB-10MB)
  2. 内存管理

    byte[] b = new byte[1024 * 10]; // 10KB缓冲区
    
  3. 存储优化

    • 临时文件自动清理
    • 视频文件HLS自适应码率
  4. Redis优化

    redisUtils.setex(key, value, 1小时); // 临时数据自动过期
    
  5. 异步队列

    • 转码任务进入线程池
    • 失败任务重试机制

六、安全防护措施

  1. 校验机制

    • MD5+大小双校验防碰撞
    • 文件后缀白名单校验
  2. 防篡改保护

    if (!FileStatusEnums.TRANSFER.getStatus().equals(fileInfo.getStatus())) {
        return; // 状态校验
    }
    
  3. 临时文件清理

    finally {
        FileUtils.deleteDirectory(tempFileFolder);
    }
    

文件秒传处理模块深度解析

一、核心机制图解

用户上传文件
首片检查
MD5+大小查询
存在相同文件?
创建引用记录
继续分片上传
更新用户空间
返回秒传成功

二、代码模块拆解

1. 触发条件判断

// 仅在首片上传时触发秒传检查
if (chunkIndex == 0) {
    // 核心处理逻辑
}

设计考量:避免每个分片都进行数据库查询,减少系统压力

2. 秒传核验核心

FileInfoQuery infoQuery = new FileInfoQuery();
infoQuery.setFileMd5(fileMd5); // MD5指纹
infoQuery.setSimplePage(new SimplePage(0, 1)); // 限制查询1条
infoQuery.setStatus(FileStatusEnums.USING.getStatus()); // 仅检查可用文件
List<FileInfo> dbFileList = this.fileInfoMapper.selectList(infoQuery);

技术要点

  • 分页查询避免全表扫描
  • 状态过滤确保文件可用

3. 空间校验逻辑

if (dbFile.getFileSize() + spaceDto.getUseSpace() > spaceDto.getTotalSpace()) {
    throw new BusinessException("空间不足");
}

双重保障机制

  1. Redis实时校验:毫秒级响应
  2. 数据库事务保障:最终一致性

4. 引用记录创建

dbFile.setFileId(fileId); // 生成新文件ID
dbFile.setFilePid(filePid); // 继承目录结构
dbFile.setUserId(webUserDto.getUserId()); // 绑定新用户
dbFile.setCreateTime(curDate); // 更新时间戳
fileName = autoRename(filePid, userId, fileName); // 自动重命名
this.fileInfoMapper.insert(dbFile); // 创建新记录

创新设计

  • 物理文件复用:file_path直接引用原文件
  • 逻辑记录独立:文件树结构、权限信息隔离

5. 空间更新操作

private void updateUserSpace(SessionWebUserDto webUserDto, Long useSpace) {
    // 数据库更新
    userInfoMapper.updateUserSpace(userId, useSpace, null);
    // Redis更新
    spaceDto.setUseSpace(spaceDto.getUseSpace() + useSpace);
    redisComponent.saveUserSpaceUse(userId, spaceDto);
}

双写策略

  • Redis:高频访问数据缓存,保证实时性
  • MySQL:持久化存储,保证可靠性

三、异常处理机制

1. 碰撞处理流程

try {
    // 秒传核心逻辑
} catch (BusinessException e) {
    // 空间不足等业务异常
    logger.error("文件上传失败", e);
    throw e; 
} finally {
    // 清理临时文件
}

异常类型

  • CODE_904:存储空间不足
  • SQLIntegrityConstraintViolationException:唯一约束冲突

2. 事务回滚保障

@Transactional(rollbackFor = Exception.class)
public UploadResultDto uploadFile(...) {
    // 整个操作在事务中执行
}

原子性保证:出现任何异常时,引用记录创建和空间更新操作同时回滚

四、时序图解析

用户 前端 服务层 Redis 数据库 上传首片 请求秒传检查 获取用户空间数据 查询MD5记录 返回文件信息 创建引用记录 更新空间使用 返回秒传成功 返回继续上传 alt [存在相同文件] [需要上传] 展示操作结果 用户 前端 服务层 Redis 数据库

五、代码

    /**
     * 上传文件(含秒传处理)
     * 事务注解确保数据一致性:当空间不足时回滚数据库操作
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public UploadResultDto uploadFile(SessionWebUserDto webUserDto, String fileId, MultipartFile file, String fileName,
                                      String filePid, String fileMd5, Integer chunkIndex, Integer chunks) {
        // 初始化上传结果对象
        UploadResultDto resultDto = new UploadResultDto();
        try {
            // 生成文件唯一ID(类似网盘分享链接的短ID)
            if (StringTools.isEmpty(fileId)) {
                fileId = StringTools.getRandomString(Constants.LENGTH_10); // 生成10位随机字符串
            }
            resultDto.setFileId(fileId);

            // 获取用户空间使用情况(Redis缓存优化查询性能)
            UserSpaceDto spaceDto = redisComponent.getUserSpaceUse(webUserDto.getUserId());

            //==================== 秒传处理核心逻辑 ====================//
            if (chunkIndex == 0) { // 仅在上传第一个分片时触发秒传检查
                // 构建MD5查询条件(命中idx_md5_size索引)
                FileInfoQuery infoQuery = new FileInfoQuery();
                infoQuery.setFileMd5(fileMd5);          // MD5指纹
                infoQuery.setSimplePage(new SimplePage(0, 1)); // 限制返回1条记录
                infoQuery.setStatus(FileStatusEnums.USING.getStatus()); // 只查询可用文件

                // 查询数据库是否存在相同文件
                List<FileInfo> dbFileList = this.fileInfoMapper.selectList(infoQuery);
                if (!dbFileList.isEmpty()) {
                    FileInfo dbFile = dbFileList.get(0);
                    
                    // 双重空间校验(Redis缓存校验 + 数据库最终校验)
                    if (dbFile.getFileSize() + spaceDto.getUseSpace() > spaceDto.getTotalSpace()) {
                        throw new BusinessException(ResponseCodeEnum.CODE_904); // 空间不足异常
                    }

                    //==================== 创建秒传文件记录 ====================//
                    // 复用文件实体(类似创建快捷方式)
                    dbFile.setFileId(fileId);            // 新文件ID
                    dbFile.setFilePid(filePid);          // 继承目录结构
                    dbFile.setUserId(webUserDto.getUserId()); // 绑定当前用户
                    dbFile.setCreateTime(new Date());    // 重置时间戳
                    dbFile.setStatus(FileStatusEnums.USING.getStatus()); // 设置可用状态
                    dbFile.setDelFlag(FileDelFlagEnums.USING.getFlag()); // 删除标记
                    
                    // 自动重命名处理(类似"文件名(1).txt"的生成逻辑)
                    fileName = autoRename(filePid, webUserDto.getUserId(), fileName);
                    dbFile.setFileName(fileName); 

                    // 写入数据库(实际是创建新的元数据记录)
                    this.fileInfoMapper.insert(dbFile);
                    
                    // 更新用户空间使用量(原子操作)
                    updateUserSpace(webUserDto, dbFile.getFileSize());

                    // 返回秒传成功状态码
                    resultDto.setStatus(UploadStatusEnums.UPLOAD_SECONDS.getCode());
                    return resultDto;
                }
            }
            //==================== 正常上传流程继续执行 ====================//
            // ...(后续为普通上传处理逻辑)
            
        } catch (BusinessException e) {
            // 特殊异常处理(包含空间不足等业务异常)
            logger.error("文件上传失败", e);
            throw e; // 抛出异常触发事务回滚
        }
        return resultDto;
    }

    /**
     * 自动重命名策略(防止同一目录下文件重名)
     * 实现逻辑类似Windows的"同名文件(1)"处理
     */
    private String autoRename(String filePid, String userId, String fileName) {
        FileInfoQuery query = new FileInfoQuery();
        query.setUserId(userId);
        query.setFilePid(filePid);
        query.setDelFlag(FileDelFlagEnums.USING.getFlag());
        query.setFileName(fileName);
        
        // 查询同名文件数量(命中idx_user_pid_name索引)
        Integer count = this.fileInfoMapper.selectCount(query);
        if (count > 0) {
            // 调用字符串工具生成带序号的文件名(如"文档(1).pdf")
            return StringTools.rename(fileName); 
        }
        return fileName;
    }

    /**
     * 用户空间更新(双写策略)
     * 先更新数据库 -> 再更新Redis缓存
     */
    private void updateUserSpace(SessionWebUserDto webUserDto, Long useSpace) {
        // 数据库更新(使用乐观锁防止超卖)
        int count = userInfoMapper.updateUserSpace(webUserDto.getUserId(), useSpace, null);
        if (count == 0) { // 更新行数为0表示空间不足
            throw new BusinessException(ResponseCodeEnum.CODE_904);
        }
        
        // Redis缓存更新(保证读取性能)
        UserSpaceDto spaceDto = redisComponent.getUserSpaceUse(webUserDto.getUserId());
        spaceDto.setUseSpace(spaceDto.getUseSpace() + useSpace);
        redisComponent.saveUserSpaceUse(webUserDto.getUserId(), spaceDto);
    }

文件转码模块深度解析

一、核心流程图示

转码中
非转码中
视频
图片
获取文件信息
状态校验
合并临时文件
终止流程
文件类型判断
视频切片
生成缩略图
更新封面路径
更新文件状态

二、代码模块拆解

1. 状态校验模块

if (fileInfo == null || !FileStatusEnums.TRANSFER.getStatus().equals(fileInfo.getStatus())) {
    return;
}

功能:确保只有处于"转码中"状态的文件才会被处理
设计考量:防止重复处理或无效操作

2. 路径生成模块

String targetFolderName = appConfig.getProjectFolder() + Constants.FILE_FOLDER_FILE;
File targetFolder = new File(targetFolderName + "/" + month);

目录结构

project_root/
├── file/
│   └── 202309/
│       └── user123_file456.mp4

优化点:按月份分目录存储,避免单目录文件过多

3. 文件合并模块

union(fileFolder.getPath(), targetFilePath, true);

实现要点

  • 使用RandomAccessFile进行随机读写
  • 10KB缓冲区平衡内存与IO效率
  • 自动清理临时目录(delSource=true)

三、关键技术实现

1. 视频处理流程

// 视频转TS格式
final String CMD_TRANSFER_2TS = "ffmpeg -y -i %s -vcodec copy -acodec copy -vbsf h264_mp4toannexb %s";
// 生成HLS切片
final String CMD_CUT_TS = "ffmpeg -i %s -c copy -map 0 -f segment -segment_list %s -segment_time 30 %s/%s_%%4d.ts";

输出结构

video.mp4
├── video.m3u8
├── video_0001.ts
├── video_0002.ts
└── video_0003.ts

2. 缩略图生成

// 视频封面截取
ScaleFilter.createCover4Video(源文件, 150px, 输出路径);
// 图片缩略图生成
ScaleFilter.createThumbnailWidthFFmpeg(源文件, 150px, 输出路径);

降级策略:缩略图生成失败时直接复制原文件

3. 乐观锁实现

UPDATE file_info 
SET status = #{newStatus} 
WHERE file_id = #{fileId} 
  AND status = #{oldStatus}

并发控制:确保只有初始状态为TRANSFER的记录能被更新

四、异常处理机制

1. 错误日志记录

catch (Exception e) {
    logger.error("文件转码失败,文件ID:{},userId:{}", fileId, webUserDto.getUserId(), e);
    transferSuccess = false;
}

2. 状态回滚

finally {
    updateInfo.setStatus(transferSuccess ? USING : TRANSFER_FAIL);
    fileInfoMapper.updateFileStatusWithOldStatus(...);
}

五、代码

/**
 * 文件转码处理服务
 * 使用@Async实现异步处理,避免阻塞主线程
 */
@Async
public void transferFile(String fileId, SessionWebUserDto webUserDto) {
    // 初始化转码结果标识
    Boolean transferSuccess = true;
    String targetFilePath = null;  // 最终文件存储路径
    String cover = null;           // 封面图路径
    FileTypeEnums fileTypeEnum = null; // 文件类型枚举
    
    // 1. 查询文件基础信息
    FileInfo fileInfo = this.fileInfoMapper.selectByFileIdAndUserId(fileId, webUserDto.getUserId());
    try {
        // 2. 状态校验(双重检查)
        if (fileInfo == null || !FileStatusEnums.TRANSFER.getStatus().equals(fileInfo.getStatus())) {
            return; // 非转码状态文件直接返回
        }

        // 3. 准备文件存储路径
        // 临时文件目录:/temp/userId_fileId/
        String tempFolderName = appConfig.getProjectFolder() + Constants.FILE_FOLDER_TEMP;
        String currentUserFolderName = webUserDto.getUserId() + fileId;
        File fileFolder = new File(tempFolderName + currentUserFolderName);

        // 4. 解析文件信息
        String fileSuffix = StringTools.getFileSuffix(fileInfo.getFileName()); // 获取文件后缀
        String month = DateUtil.format(fileInfo.getCreateTime(), DateTimePatternEnum.YYYYMM.getPattern());
        
        // 5. 创建目标目录(按年月分类)
        // 最终存储路径:/file/yyyyMM/
        String targetFolderName = appConfig.getProjectFolder() + Constants.FILE_FOLDER_FILE;
        File targetFolder = new File(targetFolderName + "/" + month);
        if (!targetFolder.exists()) {
            targetFolder.mkdirs(); // 不存在则创建目录
        }

        // 6. 合并分片文件
        String realFileName = currentUserFolderName + fileSuffix; // 构建唯一文件名
        targetFilePath = targetFolder.getPath() + "/" + realFileName;
        union(fileFolder.getPath(), targetFilePath, fileInfo.getFileName(), true);

        // 7. 根据文件类型进行特殊处理
        fileTypeEnum = FileTypeEnums.getFileTypeBySuffix(fileSuffix);
        
        // 7.1 视频文件处理
        if (FileTypeEnums.VIDEO == fileTypeEnum) {
            // 视频切片(HLS协议)
            cutFile4Video(fileId, targetFilePath);
            
            // 生成视频封面(首帧截图)
            cover = month + "/" + currentUserFolderName + Constants.IMAGE_PNG_SUFFIX;
            String coverPath = targetFolderName + "/" + cover;
            ScaleFilter.createCover4Video(new File(targetFilePath), Constants.LENGTH_150, new File(coverPath));
        } 
        // 7.2 图片文件处理
        else if (FileTypeEnums.IMAGE == fileTypeEnum) {
            // 生成缩略图
            cover = month + "/" + realFileName.replace(".", "_."); // 缩略图命名规则
            String coverPath = targetFolderName + "/" + cover;
            
            // 尝试用FFmpeg生成缩略图
            Boolean created = ScaleFilter.createThumbnailWidthFFmpeg(
                new File(targetFilePath), 
                Constants.LENGTH_150, 
                new File(coverPath), 
                false
            );
            
            // 降级方案:生成失败直接复制原图
            if (!created) {
                FileUtils.copyFile(new File(targetFilePath), new File(coverPath));
            }
        }
    } catch (Exception e) {
        // 8. 异常处理
        logger.error("文件转码失败,文件ID:{},userId:{}", fileId, webUserDto.getUserId(), e);
        transferSuccess = false;
    } finally {
        // 9. 更新文件状态(使用乐观锁)
        FileInfo updateInfo = new FileInfo();
        updateInfo.setFileSize(new File(targetFilePath).length()); // 设置实际文件大小
        updateInfo.setFileCover(cover); // 设置封面路径
        updateInfo.setStatus(transferSuccess ? FileStatusEnums.USING.getStatus() : FileStatusEnums.TRANSFER_FAIL.getStatus());
        
        /**
         * 乐观锁实现说明:
         * UPDATE file_info 
         * SET status = #{newStatus} 
         * WHERE file_id = #{fileId} 
         *   AND user_id = #{userId} 
         *   AND status = #{oldStatus}
         *   
         * 确保只有状态为TRANSFER的记录会被更新,防止并发操作导致状态不一致
         */
        fileInfoMapper.updateFileStatusWithOldStatus(
            fileId, 
            webUserDto.getUserId(), 
            updateInfo, 
            FileStatusEnums.TRANSFER.getStatus()
        );
    }
}

/**
 * 视频切片处理方法
 */
private void cutFile4Video(String fileId, String videoFilePath) {
    // 创建切片目录(与视频文件同名目录)
    File tsFolder = new File(videoFilePath.substring(0, videoFilePath.lastIndexOf(".")));
    if (!tsFolder.exists()) {
        tsFolder.mkdirs();
    }
    
    // FFmpeg命令模板
    final String CMD_TRANSFER_2TS = "ffmpeg -y -i %s -vcodec copy -acodec copy -vbsf h264_mp4toannexb %s";
    final String CMD_CUT_TS = "ffmpeg -i %s -c copy -map 0 -f segment -segment_list %s -segment_time 30 %s/%s_%%4d.ts";
    
    // 1. 先转成TS格式
    String tsPath = tsFolder + "/" + Constants.TS_NAME;
    String cmd = String.format(CMD_TRANSFER_2TS, videoFilePath, tsPath);
    ProcessUtils.executeCommand(cmd, false);
    
    // 2. 生成HLS切片和m3u8索引
    cmd = String.format(CMD_CUT_TS, 
        tsPath, 
        tsFolder.getPath() + "/" + Constants.M3U8_NAME, 
        tsFolder.getPath(), 
        fileId
    );
    ProcessUtils.executeCommand(cmd, false);
    
    // 3. 清理临时TS文件
    new File(tsPath).delete();
}

文件合并模块深度解析

一、核心流程图示

检查目录存在
目录存在?
初始化目标文件
抛出异常
遍历分片文件
读取分片数据
写入目标文件
是否末片?
清理临时文件

二、代码模块拆解

1. 参数说明

/**
 • @param dirPath 分片存储目录(如:/temp/user123_file456/)

 • @param toFilePath 合并后文件路径(如:/file/202309/user123_file456.mp4)

 • @param fileName 原始文件名(仅用于日志记录)

 • @param delSource 是否删除源分片(true-合并后自动清理)

 */
private void union(String dirPath, String toFilePath, String fileName, Boolean delSource)

2. 文件校验模块

File dir = new File(dirPath);
if (!dir.exists()) {
    throw new BusinessException("目录不存在"); // 快速失败机制
}

设计考量:前置检查避免无效操作

3. 核心合并逻辑

try (RandomAccessFile writeFile = new RandomAccessFile(targetFile, "rw")) {
    byte[] buffer = new byte[1024 * 10]; // 10KB缓冲
    for (int i = 0; i < fileList.length; i++) {
        try (RandomAccessFile readFile = new RandomAccessFile(chunkFile, "r")) {
            while ((len = readFile.read(buffer)) != -1) {
                writeFile.write(buffer, 0, len); // 增量写入
            }
        }
    }
}

关键技术点

  • 使用RandomAccessFile实现随机读写
  • 固定10KB缓冲区平衡内存与IO效率
  • try-with-resource自动关闭资源

三、关键处理逻辑

1. 分片读取机制

while ((len = readFile.read(b)) != -1) {
    writeFile.write(b, 0, len);
}

工作流程

  1. read(b)从当前文件指针读取数据
  2. 返回实际读取字节数(len),-1表示EOF
  3. write(b,0,len)写入目标文件
  4. 指针自动后移,下次读取继续

2. 异常处理机制

catch (Exception e) {
    logger.error("合并分片失败", e);
    throw new BusinessException("合并分片失败"); // 业务异常封装
}
finally {
    if (null != writeFile) {
        writeFile.close(); // 确保资源释放
    }
}

保障措施

  • 记录详细错误日志
  • 异常转换(Exception -> BusinessException)
  • 资源释放兜底

四、代码

/**
 * 合并分片文件到完整文件
 * 
 * @param dirPath 分片文件存储目录(格式:/temp/userId_fileId/)
 * @param toFilePath 合并后的目标文件路径(格式:/file/yyyyMM/userId_fileId.ext)
 * @param fileName 原始文件名(仅用于日志记录)
 * @param delSource 是否删除源分片文件(true=合并后自动清理)
 * 
 * 实现原理:
 * 1. 顺序读取编号为0-N的分片文件
 * 2. 使用10KB缓冲区流式合并
 * 3. 支持事务回滚(异常时中断合并)
 */
private void union(String dirPath, String toFilePath, String fileName, Boolean delSource) {
    // 1. 校验分片目录是否存在
    File dir = new File(dirPath);
    if (!dir.exists()) {
        throw new BusinessException("目录不存在"); // 快速失败
    }

    // 2. 获取分片文件列表(按文件名排序)
    File[] fileList = dir.listFiles();
    File targetFile = new File(toFilePath);
    RandomAccessFile writeFile = null;
    
    try {
        // 3. 初始化目标文件(随机访问模式)
        writeFile = new RandomAccessFile(targetFile, "rw");
        byte[] buffer = new byte[1024 * 10]; // 10KB缓冲区
        
        // 4. 遍历所有分片文件(命名格式:0,1,2...)
        for (int i = 0; i < fileList.length; i++) {
            File chunkFile = new File(dirPath + "/" + i);
            RandomAccessFile readFile = null;
            
            try {
                // 5. 打开当前分片文件(只读模式)
                readFile = new RandomAccessFile(chunkFile, "r");
                int bytesRead;
                
                // 6. 流式读取分片内容(自动维护文件指针)
                while ((bytesRead = readFile.read(buffer)) != -1) {
                    // 7. 写入目标文件(追加模式)
                    writeFile.write(buffer, 0, bytesRead);
                }
            } catch (Exception e) {
                logger.error("合并分片[{}]失败", i, e);
                throw new BusinessException("合并分片失败");
            } finally {
                // 8. 确保关闭当前分片文件
                if (readFile != null) {
                    try {
                        readFile.close();
                    } catch (IOException e) {
                        logger.warn("关闭分片文件失败", e);
                    }
                }
            }
        }
    } catch (Exception e) {
        logger.error("合并文件[{}]失败", fileName, e);
        throw new BusinessException("合并文件" + fileName + "出错了");
    } finally {
        // 9. 资源清理工作
        try {
            if (writeFile != null) {
                writeFile.close(); // 关闭目标文件
            }
            
            // 10. 按需删除源分片(事务提交后执行)
            if (delSource && dir.exists()) {
                try {
                    FileUtils.deleteDirectory(dir); // 递归删除目录
                    logger.debug("已清理分片目录:{}", dirPath);
                } catch (IOException e) {
                    logger.error("删除分片目录失败", e);
                }
            }
        } catch (IOException e) {
            logger.error("关闭文件流失败", e);
        }
    }
}

/**
 * 技术要点说明:
 * 1. 文件指针机制:
 *    - RandomAccessFile自动维护读取位置指针
 *    - 每次read()都会从上次结束位置继续读取
 * 
 * 2. 内存优化:
 *    - 固定10KB缓冲区避免大内存占用
 *    - 流式处理支持超大文件合并
 * 
 * 3. 异常处理:
 *    - 分片级异常记录具体失败分片编号
 *    - 文件级异常携带原始文件名
 * 
 * 4. 资源管理:
 *    - 使用finally确保文件句柄释放
 *    - 删除操作放在最后确保主流程完成
 */

异步转码机制技术解析

一、代码片段关键逻辑说明

TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
    @Override
    public void afterCommit() {
        fileInfoService.transferFile(fileInfo.getFileId(), webUserDto);
    }
});

此代码实现了:

  1. 事务边界控制:确保转码操作在数据库事务提交后触发
  2. 异步执行保障:通过Spring代理调用实现真正的异步
  3. 数据可见性:保证转码任务读取到已持久化的文件记录

二、异步转码的必要性

1. 防止事务未提交导致数据不可见

场景同步转码异步转码(当前方案)
事务提交前可能读取到未提交的临时数据不会触发转码任务
事务提交后正常执行但阻塞主线程通过事务回调保证数据可见性

2. 性能优化对比

// 同步方式(伪代码)
@Transactional
public void uploadFile() {
    saveToDB();          // 耗时1ms
    transcodeFile();      // 耗时30s → 接口响应延迟30s+
}

// 异步方式(当前实现)
@Transactional
public void uploadFile() {
    saveToDB();          // 耗时1ms
    registerAsyncTask(); // 耗时0.5ms → 接口响应延迟≈1.5ms
}

三、具体技术实现分析

1. 事务同步器工作原理

App Transaction AsyncTask 开启事务 执行数据库操作 注册同步回调 提交事务 触发afterCommit回调 执行转码任务 App Transaction AsyncTask

2. 关键组件说明

组件作用
TransactionSynchronizationSpring事务同步器接口,提供事务生命周期钩子
afterCommit()事务成功提交后的回调入口点
@Async代理机制通过CGLIB生成代理类,实现线程池任务提交

四、设计优势体现

1. 数据一致性保障

// 文件信息插入语句(事务内)
this.fileInfoMapper.insert(fileInfo); 

// 转码任务执行时(事务已提交)
FileInfo dbFile = fileInfoMapper.selectByFileId(fileId); // 确保读取到已提交数据

2. 异常处理机制

场景处理方式
事务回滚afterCommit()不会执行,转码任务不会被触发
转码失败通过finally块更新状态为TRANSFER_FAIL,记录详细日志
服务重启通过TRANSFER状态的任务扫描机制进行补偿

五、扩展设计思考

1. 消息队列增强方案

// 事务提交后发送MQ消息
@Transactional
public void uploadFile() {
    saveToDB();
    TransactionSynchronizationManager.registerSynchronization(() -> {
        rocketMQTemplate.sendAsync("transcode_topic", fileId);
    });
}

// 消费者端
@RocketMQMessageListener(topic = "transcode_topic")
public class TranscodeConsumer {
    public void process(String fileId) {
        fileService.transferFile(fileId);
    }
}

2. 分布式事务保障

1. 写数据库
2. 写事务消息
3. 投递消息
4. 完成转码
5. 更新状态
上传事务
主事务
RocketMQ
转码服务
回调通知

该设计通过事务同步机制与异步处理的结合,实现了:

  1. 高响应速度:主流程耗时从秒级降到毫秒级
  2. 数据强一致:通过事务边界控制保证可见性
  3. 资源隔离:转码任务使用独立线程池
  4. 系统可扩展:可平滑升级为分布式任务系统

视频切割处理流程解析

核心处理步骤

1. 准备切片目录

File tsFolder = new File(videoFilePath.substring(0, videoFilePath.lastIndexOf(".")));
tsFolder.mkdirs();  // 创建与视频同名的目录用于存放切片

示例
/data/video.mp4/data/video/ 目录

2. 视频转TS格式(关键步骤)

final String CMD_TRANSFER_2TS = 
    "ffmpeg -y -i %s -vcodec copy -acodec copy -vbsf h264_mp4toannexb %s";

参数说明

参数作用
-vcodec copy视频流直接复制(无重编码)
-acodec copy音频流直接复制
-vbsf h264_mp4toannexb将MP4封装转为TS支持的格式

执行效果
生成临时TS文件:/data/video/index.ts

3. 生成HLS切片(核心处理)

final String CMD_CUT_TS = 
    "ffmpeg -i %s -c copy -map 0 -f segment -segment_list %s -segment_time 30 %s/%s_%%4d.ts";

关键参数

参数作用
-f segment启用分段模式
-segment_time30每段30秒
-segment_listx.m3u8生成索引文件
%4d.ts切片命名格式(0001.ts)

输出结构

/data/video/
├── playlist.m3u8    # HLS主索引文件
├── file_0001.ts    # 第一段切片
├── file_0002.ts    # 第二段切片
└── ...             # 其他切片

4. 清理临时文件

new File(tsPath).delete();  // 删除中间文件index.ts

技术原理图解

原始MP4
转TS格式
切片处理
m3u8索引
0001.ts
0002.ts

典型HLS文件结构

playlist.m3u8 示例
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:30
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:30.000000,
file_0001.ts
#EXTINF:28.000000,
file_0002.ts
#EXT-X-ENDLIST

代码

/**
 * 视频文件切割处理方法(HLS协议)
 * @param fileId 文件唯一标识(用于生成切片文件名)
 * @param videoFilePath 原始视频文件完整路径
 */
private void cutFile4Video(String fileId, String videoFilePath) {
    // 1. 创建切片存储目录(与视频文件同名目录)
    // 示例:/data/video.mp4 -> /data/video/
    File tsFolder = new File(videoFilePath.substring(0, videoFilePath.lastIndexOf(".")));
    if (!tsFolder.exists()) {
        tsFolder.mkdirs(); // 递归创建多级目录
    }

    // 2. 定义FFmpeg命令模板
    // 命令1:将MP4转换为TS格式(不重新编码)
    final String CMD_TRANSFER_2TS = "ffmpeg -y -i %s -vcodec copy -acodec copy -vbsf h264_mp4toannexb %s";
    // 命令2:将TS文件切片并生成m3u8索引
    final String CMD_CUT_TS = "ffmpeg -i %s -c copy -map 0 -f segment -segment_list %s -segment_time 30 %s/%s_%%4d.ts";

    // 3. 生成中间TS文件路径
    // 示例:/data/video/index.ts
    String tsPath = tsFolder + "/" + Constants.TS_NAME;

    // 4. 执行格式转换(MP4->TS)
    // 参数说明:
    // -y 覆盖输出文件
    // -vcodec copy 视频流直接复制
    // -acodec copy 音频流直接复制
    // -vbsf h264_mp4toannexb 转换视频比特流格式
    String cmd = String.format(CMD_TRANSFER_2TS, videoFilePath, tsPath);
    ProcessUtils.executeCommand(cmd, false); // 执行命令行

    // 5. 执行切片操作并生成m3u8索引
    // 参数说明:
    // -c copy 音视频流都不重新编码
    // -map 0 处理所有数据流
    // -f segment 启用分段模式
    // -segment_time 30 每段30秒
    // -segment_list 生成m3u8索引文件路径
    // %04d.ts 生成形如0001.ts的切片文件
    cmd = String.format(
        CMD_CUT_TS, 
        tsPath, 
        tsFolder.getPath() + "/" + Constants.M3U8_NAME, // m3u8文件路径
        tsFolder.getPath(), // 切片输出目录
        fileId // 切片文件名前缀
    );
    ProcessUtils.executeCommand(cmd, false);

    // 6. 清理临时文件(中间TS文件)
    // 示例:删除/data/video/index.ts
    new File(tsPath).delete(); 
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2341026.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Ubuntu数据连接访问崩溃问题

目录 一、分析问题 1、崩溃问题本地调试gdb调试&#xff1a; 二、解决问题 1. 停止 MySQL 服务 2. 卸载 MySQL 相关包 3. 删除 MySQL 数据目录 4. 清理依赖和缓存 5.重新安装mysql数据库 6.创建程序需要的数据库 三、验证 1、动态库更新了 2、头文件更新了 3、重新…

Spark-Streaming简介和核心编程

Spark-Streaming简介 概述&#xff1a;用于流式数据处理&#xff0c;支持Kafka、Flume等多种数据输入源&#xff0c;可使用Spark原语运算&#xff0c;结果能保存到HDFS、数据库等。它以DStream&#xff08;离散化流&#xff09;为抽象表示&#xff0c;是RDD在实时场景的封装&am…

Docker 快速入门教程

1. Docker 基本概念 镜像(Image): 只读模板&#xff0c;包含创建容器的指令 容器(Container): 镜像的运行实例 Dockerfile: 用于构建镜像的文本文件 仓库(Repository): 存放镜像的地方&#xff08;如Docker Hub&#xff09; 2. 安装Docker 根据你的操作系统选择安装方式:…

【锂电池SOH估计】BP神经网络锂电池健康状态估计,锂电池SOH估计(Matlab完整源码和数据)

目录 效果一览程序获取程序内容研究内容基于BP神经网络的锂电池健康状态估计研究摘要关键词1. 引言1.1 研究背景1.2 研究意义1.3 研究目标2. 文献综述2.1 锂电池SOH估计理论基础2.2 传统SOH估计方法2.3 基于BP神经网络的SOH估计研究进展2.4 研究空白与创新点3. BP神经网络原理3…

Python常用的第三方模块之二【openpyxl库】读写Excel文件

openpyxl库模块是用于处理Microsoft Excel文件的第三方库&#xff0c;可以对Excel文件中的数据进行写入和读取。 weather.pyimport reimport requests#定义函数 def get_html():urlhttps://www.weather.com.cn/weather1d/101210101.shtml #爬虫打开浏览器上的网页resprequests.…

成熟软件项目解决方案:360°全景影像显控软件系统

​若该文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://hpzwl.blog.csdn.net/article/details/147425300 长沙红胖子Qt&#xff08;长沙创微智科&#xff09;博文大全&#xff1a;开发技术集合&#xff08;包含Qt实用技术、树莓派、三维、Open…

前端开发核心知识详解:Vue2、JavaScript 与 CSS

一、Vue2 核心知识点 1. Vue2 的双向绑定原理 Vue2 实现双向绑定主要依赖数据劫持与发布 - 订阅者模式。 利用Object.defineProperty方法对数据对象的属性进行劫持&#xff0c;为每个属性定义getter和setter。getter用于收集依赖&#xff0c;当视图中使用到该属性时&#xf…

JDK安装超详细步骤

&#x1f525;【JDK安装超详细步骤】 文章目录 &#x1f525;【JDK安装超详细步骤】1. 卸载系统自带的旧版JDK2. 安装JDK113. 验证安装是否成功4. 常见问题4.1 执行java -version提示命令未找到&#xff1f; 1. 卸载系统自带的旧版JDK 查询已安装的OpenJDK包。 rpm -qa | gre…

39.剖析无处不在的数据结构

数据结构是计算机中组织和存储数据的特定方式&#xff0c;它的目的是方便且高效地对数据进行访问和修改。数据结构表述了数据之间的关系&#xff0c;以及操作数据的一系列方法。数据又是程序的基本单元&#xff0c;因此无论是哪种语言、哪种领域&#xff0c;都离不开数据结构&a…

在离线 Ubuntu 环境下部署双 Neo4j 实例(Prod Dev)

在许多开发和生产场景中&#xff0c;我们可能需要在同一台服务器上运行多个独立的 Neo4j 数据库实例&#xff0c;例如一个用于生产环境 (Prod)&#xff0c;一个用于开发测试环境 (Dev)。本文将详细介绍如何在 离线 的 Ubuntu 服务器上&#xff0c;使用 tar.gz 包部署两个 Neo4j…

第十五届蓝桥杯 2024 C/C++组 下一次相遇

目录 题目&#xff1a; 题目描述&#xff1a; 题目链接&#xff1a; 思路&#xff1a; 自己的思路详解&#xff1a; 更好的思路详解&#xff1a; 代码&#xff1a; 自己的思路代码详解&#xff1a; 更好的思路代码详解&#xff1a; 题目&#xff1a; 题目描述&#xf…

【2】CICD持续集成-k8s集群中安装Jenkins

一、背景&#xff1a; Jenkins是一款开源 CI&CD 系统&#xff0c;用于自动化各种任务&#xff0c;包括构建、测试和部署。 Jenkins官方提供了镜像&#xff1a;https://hub.docker.com/r/jenkins/jenkins 使用Deployment来部署这个镜像&#xff0c;会暴露两个端口&#xff…

IDEA 创建Maven 工程(图文)

设置Maven 仓库 打开IDEA 开发工具&#xff0c;我的版本是2024.3.1&#xff08;每个版本的位置不一样&#xff09;。在【Customize】选项中&#xff0c;可以直接设置【语言】&#xff0c;在最下面选择【All setting】。 进入到熟悉的配置界面&#xff0c;选择配置的【setting…

通过C# 将Excel表格转换为图片(JPG/ PNG)

Excel 表格可能会因为不同设备、不同软件版本或字体缺失等问题&#xff0c;导致格式错乱或数据显示异常。转换为图片后&#xff0c;能确保数据的排版、格式和外观始终保持一致&#xff0c;无论在何种设备或平台上查看&#xff0c;都能呈现出固定的样式&#xff0c;避免了因环境…

国产紫光同创FPGA实现SDI视频编解码+图像缩放,基于HSSTHP高速接口,提供2套工程源码和技术支持

目录 1、前言工程概述免责声明 2、相关方案推荐我已有的所有工程源码总目录----方便你快速找到自己喜欢的项目本博已有的 SDI 编解码方案本方案在Xilinx--Artix7系列FPGA上的应用本方案在Xilinx--Kintex系列FPGA上的应用本方案在Xilinx--Zynq系列FPGA上的应用本方案在Xilinx--U…

自动驾驶安全模型研究

自动驾驶安全模型研究 自动驾驶安全模型研究 自动驾驶安全模型研究1.自动驾驶安全模型概述2. 自动驾驶安全模型应用3. 自动驾驶安全模型介绍3.1 Last Point to Steer3.2 Safety Zone3.3 RSS (Responsibility-Sensitive Safety)3.4 SFF (Safety Force Field)3.5 FSM (Fuzzy Safe…

【项目】基于MCP+Tabelstore架构实现知识库答疑系统

基于MCPTabelstore架构实现知识库答疑系统 整体流程设计&#xff08;一&#xff09;Agent 架构&#xff08;二&#xff09;知识库存储&#xff08;1&#xff09;向量数据库Tablestore&#xff08;2&#xff09;MCP Server &#xff08;三&#xff09;知识库构建&#xff08;1&a…

当OCR遇上“幻觉”:如何让AI更靠谱地“看懂”文字?

在数字化的世界里&#xff0c;OCR&#xff08;光学字符识别&#xff09;技术就像给机器装上了“电子眼”。但当这项技术遇上大语言模型&#xff0c;一个意想不到的问题出现了——AI竟然会像人类一样产生“幻觉”。想象一下&#xff0c;当你拿着模糊的财务报表扫描件时&#xff…

Docker用model.config部署及更新多个模型

步骤&#xff1a; 1、本地打包模型 2、编写model.config文件 3、使用 Docker 启动一个 TensorFlow Serving 容器 4、本地打包后的模型修改后&#xff0c;修改本地model.config&#xff0c;再同步更新容器的model.config 1、本地打包模型&#xff08;本地路径&#xff09; 2、…

Linux kernel signal原理(下)- aarch64架构sigreturn流程

一、前言 在上篇中写到了linux中signal的处理流程&#xff0c;在do_signal信号处理的流程最后&#xff0c;会通过sigreturn再次回到线程现场&#xff0c;上篇文章中介绍了在X86_64架构下的实现&#xff0c;本篇中介绍下在aarch64架构下的实现原理。 二、sigaction系统调用 #i…