【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记录进度 |
| 转码失败 | 状态标记 | 人工介入恢复 |
四、时序图解析
五、性能优化策略
-
分片并行上传:
- 支持多分片并发上传
- 分片大小动态调整(2MB-10MB)
-
内存管理:
byte[] b = new byte[1024 * 10]; // 10KB缓冲区 -
存储优化:
- 临时文件自动清理
- 视频文件HLS自适应码率
-
Redis优化:
redisUtils.setex(key, value, 1小时); // 临时数据自动过期 -
异步队列:
- 转码任务进入线程池
- 失败任务重试机制
六、安全防护措施
-
校验机制:
- MD5+大小双校验防碰撞
- 文件后缀白名单校验
-
防篡改保护:
if (!FileStatusEnums.TRANSFER.getStatus().equals(fileInfo.getStatus())) {return; // 状态校验 } -
临时文件清理:
finally {FileUtils.deleteDirectory(tempFileFolder); }
文件秒传处理模块深度解析
一、核心机制图解
二、代码模块拆解
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("空间不足");
}
双重保障机制:
- Redis实时校验:毫秒级响应
- 数据库事务保障:最终一致性
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(...) {// 整个操作在事务中执行
}
原子性保证:出现任何异常时,引用记录创建和空间更新操作同时回滚
四、时序图解析
五、代码
/*** 上传文件(含秒传处理)* 事务注解确保数据一致性:当空间不足时回滚数据库操作*/@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);
}
工作流程:
read(b)从当前文件指针读取数据- 返回实际读取字节数(len),-1表示EOF
write(b,0,len)写入目标文件- 指针自动后移,下次读取继续
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);}
});
此代码实现了:
- 事务边界控制:确保转码操作在数据库事务提交后触发
- 异步执行保障:通过Spring代理调用实现真正的异步
- 数据可见性:保证转码任务读取到已持久化的文件记录
二、异步转码的必要性
1. 防止事务未提交导致数据不可见
| 场景 | 同步转码 | 异步转码(当前方案) |
|---|---|---|
| 事务提交前 | 可能读取到未提交的临时数据 | 不会触发转码任务 |
| 事务提交后 | 正常执行但阻塞主线程 | 通过事务回调保证数据可见性 |
2. 性能优化对比
// 同步方式(伪代码)
@Transactional
public void uploadFile() {saveToDB(); // 耗时1mstranscodeFile(); // 耗时30s → 接口响应延迟30s+
}// 异步方式(当前实现)
@Transactional
public void uploadFile() {saveToDB(); // 耗时1msregisterAsyncTask(); // 耗时0.5ms → 接口响应延迟≈1.5ms
}
三、具体技术实现分析
1. 事务同步器工作原理
2. 关键组件说明
| 组件 | 作用 |
|---|---|
| TransactionSynchronization | Spring事务同步器接口,提供事务生命周期钩子 |
| 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. 准备切片目录
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_time | 30 | 每段30秒 |
-segment_list | x.m3u8 | 生成索引文件 |
%4d.ts | 切片命名格式(0001.ts) |
输出结构:
/data/video/
├── playlist.m3u8 # HLS主索引文件
├── file_0001.ts # 第一段切片
├── file_0002.ts # 第二段切片
└── ... # 其他切片
4. 清理临时文件
new File(tsPath).delete(); // 删除中间文件index.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();
}
相关文章:
【EasyPan】文件上传、文件秒传、文件转码、文件合并、异步转码、视频切割分析
【EasyPan】项目常见问题解答(自用&持续更新中…)汇总版 文件上传方法解析 一、方法总览 Transactional(rollbackFor Exception.class) public UploadResultDto uploadFile(...)核心能力: 秒传验证:通过MD5文件大小实现文…...
Ubuntu数据连接访问崩溃问题
目录 一、分析问题 1、崩溃问题本地调试gdb调试: 二、解决问题 1. 停止 MySQL 服务 2. 卸载 MySQL 相关包 3. 删除 MySQL 数据目录 4. 清理依赖和缓存 5.重新安装mysql数据库 6.创建程序需要的数据库 三、验证 1、动态库更新了 2、头文件更新了 3、重新…...
Oracle DBA 高效运维指南:高频实用 SQL 大全
大家好,这里是 DBA学习之路,专注于提升数据库运维效率。 目录 前言Top SQL表空间使用率RMAN 备份DataGuard等待事件行级锁在线日志切换用户信息ASM 磁盘组DBLink数据文件收缩AWR 写在最后 前言 作为一名 Oracle DBA,在日常数据库运维工作中&…...
【xlog日志文件】怎么删除里面包含某些字符串的行(使用excel)
将log日志,复制到单独一行 B列(可能一行很长,所以将整合后的放在A列) 使用公式可以筛选出 包含某些字符串的行 为true,将这些行直接删除 IF(COUNT(FIND("MediaMuxterThreadRussia",B2,1))>0,"包含",&quo…...
Spark-Streaming简介和核心编程
Spark-Streaming简介 概述:用于流式数据处理,支持Kafka、Flume等多种数据输入源,可使用Spark原语运算,结果能保存到HDFS、数据库等。它以DStream(离散化流)为抽象表示,是RDD在实时场景的封装&am…...
Docker 快速入门教程
1. Docker 基本概念 镜像(Image): 只读模板,包含创建容器的指令 容器(Container): 镜像的运行实例 Dockerfile: 用于构建镜像的文本文件 仓库(Repository): 存放镜像的地方(如Docker Hub) 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文件的第三方库,可以对Excel文件中的数据进行写入和读取。 weather.pyimport reimport requests#定义函数 def get_html():urlhttps://www.weather.com.cn/weather1d/101210101.shtml #爬虫打开浏览器上的网页resprequests.…...
成熟软件项目解决方案:360°全景影像显控软件系统
若该文为原创文章,转载请注明原文出处 本文章博客地址:https://hpzwl.blog.csdn.net/article/details/147425300 长沙红胖子Qt(长沙创微智科)博文大全:开发技术集合(包含Qt实用技术、树莓派、三维、Open…...
前端开发核心知识详解:Vue2、JavaScript 与 CSS
一、Vue2 核心知识点 1. Vue2 的双向绑定原理 Vue2 实现双向绑定主要依赖数据劫持与发布 - 订阅者模式。 利用Object.defineProperty方法对数据对象的属性进行劫持,为每个属性定义getter和setter。getter用于收集依赖,当视图中使用到该属性时…...
JDK安装超详细步骤
🔥【JDK安装超详细步骤】 文章目录 🔥【JDK安装超详细步骤】1. 卸载系统自带的旧版JDK2. 安装JDK113. 验证安装是否成功4. 常见问题4.1 执行java -version提示命令未找到? 1. 卸载系统自带的旧版JDK 查询已安装的OpenJDK包。 rpm -qa | gre…...
PHP中的ReflectionClass讲解【详细版】
快餐: ReflectionClass精简版 在PHP中,ReflectionClass是一个功能强大的反射类,它就像是一个类的“X光透视镜”,能让我们在程序运行时深入了解类的内部结构和各种细节。 一、反射类的基本概念和重要性 反射是指在程序运行期间获…...
JAVA:Web安全防御
目录 一、Web安全基础与常见威胁 OWASP Top 10核心漏洞解析 • SQL注入(SQLi)、跨站脚本(XSS)、跨站请求伪造(CSRF) • 不安全的反序列化、敏感数据泄露 Java后端常见攻击场景 • 通过HttpServletRequest…...
39.剖析无处不在的数据结构
数据结构是计算机中组织和存储数据的特定方式,它的目的是方便且高效地对数据进行访问和修改。数据结构表述了数据之间的关系,以及操作数据的一系列方法。数据又是程序的基本单元,因此无论是哪种语言、哪种领域,都离不开数据结构&a…...
在离线 Ubuntu 环境下部署双 Neo4j 实例(Prod Dev)
在许多开发和生产场景中,我们可能需要在同一台服务器上运行多个独立的 Neo4j 数据库实例,例如一个用于生产环境 (Prod),一个用于开发测试环境 (Dev)。本文将详细介绍如何在 离线 的 Ubuntu 服务器上,使用 tar.gz 包部署两个 Neo4j…...
【Spring】单例模式的创建方式(Bean解析)
在Java中,单例模式(Singleton Pattern)确保一个类只有一个实例,并提供全局访问点。以下是实现单例的五种常见方式:懒汉式、饿汉式、双重检查锁、静态内部类和枚举,包括代码示例和优缺点分析。 1. 懒汉式&am…...
关于hadoop和yarn的问题
1.hadoop的三大结构及各自的作用? HDFS(Hadoop Distributed File System):分布式文件系统,负责海量数据的存储,具有高容错性和高吞吐量。 MapReduce:分布式计算框架,用于并行处理大…...
【飞渡科技数字孪生虚拟环境部署与集成教程 - CloudMaster实战指南】
飞渡科技数字孪生虚拟环境部署与集成教程 - CloudMaster实战指南 前言 本教程详细记录了飞渡科技的数字孪生平台CloudMaster的配置过程,以及如何将三维数字孪生场景集成到前端项目中。数字孪生技术能够在虚拟环境中精确复现物理实体的数据、特性和行为,…...
计算机软考中级 知识点记忆——排序算法 冒泡排序-插入排序- 归并排序等 各种排序算法知识点整理
一、📌 分类与比较 排序算法 最优时间复杂度 平均时间复杂度 最坏时间复杂度 空间复杂度 稳定性 应用场景与特点 算法策略 冒泡排序 O(n) O(n) O(n) O(1) 稳定 简单易实现,适用于小规模数据排序。 交换排序策略 插入排序 O(n) O(n) O…...
第十五届蓝桥杯 2024 C/C++组 下一次相遇
目录 题目: 题目描述: 题目链接: 思路: 自己的思路详解: 更好的思路详解: 代码: 自己的思路代码详解: 更好的思路代码详解: 题目: 题目描述…...
【2】CICD持续集成-k8s集群中安装Jenkins
一、背景: Jenkins是一款开源 CI&CD 系统,用于自动化各种任务,包括构建、测试和部署。 Jenkins官方提供了镜像:https://hub.docker.com/r/jenkins/jenkins 使用Deployment来部署这个镜像,会暴露两个端口ÿ…...
监控+日志=DevOps 运维的“千里眼”与“顺风耳”
监控+日志=DevOps 运维的“千里眼”与“顺风耳” 在 DevOps 体系中,监控和日志管理是不可或缺的运维基石。有人说,开发只管把代码写好,运维才是真正的“操盘手”,让系统稳定运行、不宕机、不崩溃。而要做到这一点,精准的监控与日志管理 是关键。 试想一下:如果没有监控…...
安卓的Launcher 在哪个环节进行启动
安卓Launcher在系统启动过程中的关键环节启动,具体如下: 内核启动:安卓设备开机后,首先由引导加载程序启动Linux内核。内核负责初始化硬件设备、建立内存管理机制、启动系统进程等基础工作,为整个系统的运行提供底层支…...
IDEA 创建Maven 工程(图文)
设置Maven 仓库 打开IDEA 开发工具,我的版本是2024.3.1(每个版本的位置不一样)。在【Customize】选项中,可以直接设置【语言】,在最下面选择【All setting】。 进入到熟悉的配置界面,选择配置的【setting…...
映射(Mapping)和地址(Address)
Addresses (地址) 以太坊区块链由 _ account _ (账户)组成,你可以把它想象成银行账户。一个帐户的余额是 以太 (在以太坊区块链上使用的币种),你可以和其他帐户之间支付和接受以太币,就像你的银…...
通过C# 将Excel表格转换为图片(JPG/ PNG)
Excel 表格可能会因为不同设备、不同软件版本或字体缺失等问题,导致格式错乱或数据显示异常。转换为图片后,能确保数据的排版、格式和外观始终保持一致,无论在何种设备或平台上查看,都能呈现出固定的样式,避免了因环境…...
国产紫光同创FPGA实现SDI视频编解码+图像缩放,基于HSSTHP高速接口,提供2套工程源码和技术支持
目录 1、前言工程概述免责声明 2、相关方案推荐我已有的所有工程源码总目录----方便你快速找到自己喜欢的项目本博已有的 SDI 编解码方案本方案在Xilinx--Artix7系列FPGA上的应用本方案在Xilinx--Kintex系列FPGA上的应用本方案在Xilinx--Zynq系列FPGA上的应用本方案在Xilinx--U…...
day46—双指针-两数之和-输入有序数组(LeetCode-167)
题目描述 给你一个下标从 1 开始的整数数组 numbers ,该数组已按 非递减顺序排列 ,请你从数组中找出满足相加之和等于目标数 target 的两个数。如果设这两个数分别是 numbers[index1] 和 numbers[index2] ,则 1 < index1 < index2 &l…...
自动驾驶安全模型研究
自动驾驶安全模型研究 自动驾驶安全模型研究 自动驾驶安全模型研究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架构实现知识库答疑系统 整体流程设计(一)Agent 架构(二)知识库存储(1)向量数据库Tablestore(2)MCP Server (三)知识库构建(1&a…...
