当前位置: 首页 > news >正文

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

【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);            // 新文件IDdbFile.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() {@Overridepublic void afterCommit() {fileInfoService.transferFile(fileInfo.getFileId(), webUserDto);}
});

此代码实现了:

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

二、异步转码的必要性

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

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

2. 性能优化对比

// 同步方式(伪代码)
@Transactional
public void uploadFile() {saveToDB();          // 耗时1mstranscodeFile();      // 耗时30s → 接口响应延迟30s+
}// 异步方式(当前实现)
@Transactional
public void uploadFile() {saveToDB();          // 耗时1msregisterAsyncTask(); // 耗时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.tsString 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.tsnew File(tsPath).delete(); 
}

相关文章:

  • MySQL索引知识点(笔记)
  • 《大模型+Agent 企业应用实践》的大纲
  • 网络基础概念(下)
  • 驱动开发硬核特训 · Day 17:深入掌握中断机制与驱动开发中的应用实战
  • MYSQL的binlog
  • 《棒球规则》全明星比赛规则·棒球1号位
  • 爱普生FC1610BN晶体在健康监测手环的应用
  • 使用Python设置excel单元格的字体(font值)
  • JavaScript 扩展Array类方法实现数组求和
  • 【网络应用程序设计】实验一:本地机上的聊天室
  • 代码随想录训练营38天 || 322. 零钱兑换 279. 完全平方数 139. 单词拆分
  • 从零开始学习MySQL的系统学习大纲
  • HCIP(综合实验2)
  • 每日算法-哈希表(两数之和、)
  • el-table表格既出现横向滚动条,又出现纵向滚动条?
  • YOLOv8非常详细的模型的训练两种方式
  • 文件上传漏洞2
  • <四级英语词汇> 2025.4.22
  • Cursor Free VIP 重置进程错误,轻松恢复使用!
  • 三网通电玩城平台系统结构与源码工程详解(四):子游戏集成与服务器调度机制全解
  • 福建一改造项目1人高处坠亡且事故迟报41天,住建厅约谈相关责任单位
  • 神二十瞄准明日17时17分发射
  • 委托第三方可一次性补缴十多万元的多年社保?广州多人涉嫌被骗后报警
  • 大理杨徐邱上诉案开庭:当事人称曾接受过两次测谎测试
  • 日媒:日本公明党党首将访华,并携带石破茂亲笔信
  • 88岁罗马教皇方济各突然去世,遗嘱内容对外公布