libilibi项目优化(2)视频文件分块上传
整个文件分片上传过程通过预上传生成唯一的上传 ID 和文件信息,分片上传将每个分片存储到指定路径并更新上传进度,最后通过获取已上传分块信息接口返回前端已上传的分片信息。整个过程利用 Redis 存储文件信息,确保上传过程的高效和可靠。优化后的代码在功能上较为完善,支持断点续传、完整性校验和资源清理等功能,但在处理大规模数据、并发上传和异常情况时仍存在一些不足。在实际应用中,可以根据具体需求和场景进
第一版
文件分片上传过程总结
整个文件分片上传过程分为三个主要步骤:预上传、分片上传和获取已上传分块信息。以下是每个步骤的详细描述:
1. 预上传(preUploadVideo
)
- 功能:生成唯一的上传 ID,并将文件信息存储到 Redis 中,为后续的分片上传做准备。
- 流程:
- 从前端接收文件名(
fileName
)和分片总数(chunks
)。 - 从 Redis 中获取当前用户的用户信息(
TokenUserInfoDto
)。 - 调用
redisComponent.savePreVideoFileInfo
方法:- 生成唯一的上传 ID(
uploadId
)。 - 创建
UploadingFileDto
对象,存储文件的基本信息(文件名、分片总数、初始分片索引等)。 - 根据当前日期和用户 ID 生成存储路径,并确保路径存在。
- 将文件信息存储到 Redis 中,并设置过期时间。
- 生成唯一的上传 ID(
- 返回生成的上传 ID 给前端,用于后续的分片上传。
- 从前端接收文件名(
//预上传文件
@RequestMapping("/preUploadVideo")
public ResponseVO preUploadVideo(@NotEmpty String fileName, @NotNull Integer chunks) {
//从redis中获取用户信息
TokenUserInfoDto tokenUserInfoDto = getTokenUserInfoDto();
//将预上传的文件信息存入redis,并放回相对应的上传id
String uploadId = redisComponent.savePreVideoFileInfo(tokenUserInfoDto.getUserId(), fileName, chunks);
//将id放回给前端,为之后的正式上传做准备
return getSuccessResponseVO(uploadId);
}
2. 分片上传(uploadVideo
)
-
功能:接收分片文件,将其存储到指定路径,并更新 Redis 中的上传进度。
-
流程:
- 从前端接收分片文件(
chunkFile
)、分片索引(chunkIndex
)和上传 ID(uploadId
)。 - 从 Redis 中获取当前用户的用户信息(
TokenUserInfoDto
)。 - 根据上传 ID 获取对应的文件信息(
UploadingFileDto
):- 如果文件信息不存在,抛出异常提示前端重新上传。
- 检查文件大小是否超过系统设置的最大文件大小限制:
- 如果超过限制,抛出异常。
- 构造存储路径,并将分片文件存储到对应路径:
- 路径格式为:
项目根目录/文件夹/临时文件夹/日期/用户ID上传ID/分片索引
。
- 路径格式为:
- 更新 Redis 中的文件信息:
- 增加已上传的分片索引。
- 更新已上传的文件大小。
- 返回成功响应给前端。
//上传文件 @RequestMapping("/uploadVideo") public ResponseVO uploadVideo(@NotNull MultipartFile chunkFile, @NotNull Integer chunkIndex, @NotEmpty String uploadId) throws IOException { //获得当前用户信息 TokenUserInfoDto tokenUserInfoDto = getTokenUserInfoDto(); //获得对应的上传文件的实体 UploadingFileDto fileDto = redisComponent.getUploadingVideoFile(tokenUserInfoDto.getUserId(), uploadId); if (fileDto == null) { throw new BusinessException("文件不存在请重新上传"); } SysSettingDto sysSettingDto = redisComponent.getSysSettingDto(); if (fileDto.getFileSize() > sysSettingDto.getVideoSize() * Constants.MB_SIZE) { throw new BusinessException("文件超过最大文件限制"); } //获得路径 String folder = appConfig.getProjectFolder() + Constants.FILE_FOLDER + Constants.FILE_FOLDER_TEMP + fileDto.getFilePath(); //创建文件 File targetFile = new File(folder + "/" + chunkIndex); chunkFile.transferTo(targetFile); //记录文件上传的分片数 fileDto.setChunkIndex(chunkIndex); //设置当前已上传文件的大小 fileDto.setFileSize(fileDto.getFileSize() + chunkFile.getSize()); //更新文件实体信息 redisComponent.updateVideoFileInfo(tokenUserInfoDto.getUserId(), fileDto); return getSuccessResponseVO(null); }
- 从前端接收分片文件(
3. 获取已上传分块信息(getUploadedChunks
)
-
功能:返回已上传的分片信息,用于前端判断哪些分片已经上传成功。
-
流程:
- 从前端接收上传 ID(
uploadId
)。 - 从 Redis 中获取当前用户的用户信息(
TokenUserInfoDto
)。 - 根据上传 ID 获取对应的文件信息(
UploadingFileDto
):- 如果文件信息不存在,抛出异常提示前端重新上传。
- 返回文件信息给前端,包括已上传的分片索引和文件大小。
// 获取已上传分块信息 @RequestMapping("/getUploadedChunks") public ResponseVO getUploadedChunks(@NotEmpty String uploadId) { TokenUserInfoDto tokenUserInfoDto = getTokenUserInfoDto(); UploadingFileDto fileDto = redisComponent.getUploadingVideoFile(tokenUserInfoDto.getUserId(), uploadId); if (fileDto == null) { throw new BusinessException("文件不存在请重新上传"); } return getSuccessResponseVO(fileDto); }
- 从前端接收上传 ID(
Redis 相关操作
-
savePreVideoFileInfo
方法:- 生成唯一的上传 ID。
- 创建
UploadingFileDto
对象,存储文件的基本信息。 - 根据当前日期和用户 ID 生成存储路径,并确保路径存在。
- 将文件信息存储到 Redis 中,并设置过期时间。
-
updateVideoFileInfo
方法:- 更新 Redis 中的文件信息,包括已上传的分片数和文件大小。
-
getUploadingVideoFile
方法:- 根据用户 ID 和上传 ID 从 Redis 中获取文件信息。
public String savePreVideoFileInfo(String userId, String fileName, Integer chunks) {
//生成上传id
String uploadId = StringTools.getRandomString(Constants.LENGTH_15);
//生成将要上传的文件对应的实体
UploadingFileDto fileDto = new UploadingFileDto();
//设置分片大小
fileDto.setChunks(chunks);
//设置文件名
fileDto.setFileName(fileName);
//设置上传id
fileDto.setUploadId(uploadId);
//设置初始分片索引
fileDto.setChunkIndex(0);
//根据天数新建目录,根据用户id和上传id生成文件路径
String day = DateUtil.format(new Date(), DateTimePatternEnum.YYYYMMDD.getPattern());
String filePath = day + "/" + userId + uploadId;
String folder = appConfig.getProjectFolder()
+ Constants.FILE_FOLDER
+ Constants.FILE_FOLDER_TEMP
+ filePath;
File folderFile = new File(folder);
if (!folderFile.exists()) {
folderFile.mkdirs();
}
//设置对应文件真实路径
fileDto.setFilePath(filePath);
//设置预上传的预留时间
redisUtils.setex(Constants.REDIS_KEY_UPLOADING_FILE + userId + uploadId, fileDto, Constants.REDIS_KEY_EXPIRES_DAY);
return uploadId;
}
public void updateVideoFileInfo(String userId, UploadingFileDto fileDto) {
redisUtils.setex(Constants.REDIS_KEY_UPLOADING_FILE + userId + fileDto.getUploadId(), fileDto, Constants.REDIS_KEY_EXPIRES_DAY);
}
public UploadingFileDto getUploadingVideoFile(String userId, String uploadId) {
//通过userId和uploadId获得当前上传文件的实体
return (UploadingFileDto) redisUtils.get(Constants.REDIS_KEY_UPLOADING_FILE + userId + uploadId);
}
UploadingFileDto
类
- 用于存储上传文件的相关信息,包括:
- 上传 ID(
uploadId
) - 文件名(
fileName
) - 已上传的分片索引(
chunkIndex
) - 总分片数(
chunks
) - 文件大小(
fileSize
) - 文件路径(
filePath
)
- 上传 ID(
public class UploadingFileDto implements Serializable {
private String uploadId;
private String fileName;
private Integer chunkIndex;
private Integer chunks;
private Long fileSize = 0L;
private String filePath;
}
总结
整个文件分片上传过程通过预上传生成唯一的上传 ID 和文件信息,分片上传将每个分片存储到指定路径并更新上传进度,最后通过获取已上传分块信息接口返回前端已上传的分片信息。整个过程利用 Redis 存储文件信息,确保上传过程的高效和可靠。
第二版
使用Redis中的Set结构验证每个分块是否缺失
以下是使用Redis Set集合优化后的代码示例:
1. 预上传文件(preUploadVideo
方法)
@RequestMapping("/preUploadVideo")
public ResponseVO preUploadVideo(@NotEmpty String fileName, @NotNull Integer chunks) {
// 从redis中获取用户信息
TokenUserInfoDto tokenUserInfoDto = getTokenUserInfoDto();
// 生成上传ID
String uploadId = redisComponent.savePreVideoFileInfo(tokenUserInfoDto.getUserId(), fileName, chunks);
// 返回上传ID给前端
return getSuccessResponseVO(uploadId);
}
// 在RedisComponent中
public String savePreVideoFileInfo(String userId, String fileName, Integer chunks) {
// 生成唯一的上传ID
String uploadId = StringTools.getRandomString(Constants.LENGTH_15);
// 创建UploadingFileDto对象,存储文件基本信息
UploadingFileDto fileDto = new UploadingFileDto();
fileDto.setUploadId(uploadId);
fileDto.setFileName(fileName);
fileDto.setChunks(chunks);
fileDto.setChunkIndex(0);
fileDto.setFileSize(0L);
// 设置存储路径
String day = DateUtil.format(new Date(), DateTimePatternEnum.YYYYMMDD.getPattern());
String filePath = day + "/" + userId + uploadId;
fileDto.setFilePath(filePath);
// 创建存储目录
String folderPath = appConfig.getProjectFolder()
+ Constants.FILE_FOLDER
+ Constants.FILE_FOLDER_TEMP
+ filePath;
File folder = new File(folderPath);
if (!folder.exists()) {
folder.mkdirs();
}
// 将文件信息存储到Redis
redisUtils.setex(Constants.REDIS_KEY_UPLOADING_FILE + userId + uploadId, fileDto, Constants.REDIS_KEY_EXPIRES_DAY);
return uploadId;
}
2. 分片上传(uploadVideo
方法)
@RequestMapping("/uploadVideo")
public ResponseVO uploadVideo(@NotNull MultipartFile chunkFile, @NotNull Integer chunkIndex, @NotEmpty String uploadId) throws IOException {
// 获取当前用户信息
TokenUserInfoDto tokenUserInfoDto = getTokenUserInfoDto();
String userId = tokenUserInfoDto.getUserId();
// 获取上传文件信息
UploadingFileDto fileDto = redisComponent.getUploadingVideoFile(userId, uploadId);
if (fileDto == null) {
throw new BusinessException("文件不存在请重新上传");
}
// 检查文件大小是否超过限制
SysSettingDto sysSettingDto = redisComponent.getSysSettingDto();
if (fileDto.getFileSize() + chunkFile.getSize() > sysSettingDto.getVideoSize() * Constants.MB_SIZE) {
throw new BusinessException("文件超过最大文件限制");
}
// 构造存储路径并保存分片文件
String folderPath = appConfig.getProjectFolder()
+ Constants.FILE_FOLDER
+ Constants.FILE_FOLDER_TEMP
+ fileDto.getFilePath();
File targetFile = new File(folderPath + "/" + chunkIndex);
chunkFile.transferTo(targetFile);
// 更新文件信息
fileDto.setChunkIndex(chunkIndex);
fileDto.setFileSize(fileDto.getFileSize() + chunkFile.getSize());
redisComponent.updateVideoFileInfo(userId, fileDto);
// 将分片索引加入Redis Set集合
redisUtils.sadd(Constants.REDIS_KEY_UPLOADED_CHUNKS + uploadId, chunkIndex);
return getSuccessResponseVO(null);
}
3. 合并文件(mergeVideo
方法)
@RequestMapping("/mergeVideo")
public ResponseVO mergeVideo(@NotEmpty String uploadId) {
TokenUserInfoDto tokenUserInfoDto = getTokenUserInfoDto();
String userId = tokenUserInfoDto.getUserId();
// 获取上传文件信息
UploadingFileDto fileDto = redisComponent.getUploadingVideoFile(userId, uploadId);
if (fileDto == null) {
throw new BusinessException("文件不存在请重新上传");
}
// 获取已上传的分片索引
Set<Integer> uploadedChunks = redisUtils.smembers(Constants.REDIS_KEY_UPLOADED_CHUNKS + uploadId);
// 检查分片是否完整
if (uploadedChunks.size() != fileDto.getChunks()) {
// 找出缺失的分片索引
Set<Integer> missingChunks = new HashSet<>();
for (int i = 0; i < fileDto.getChunks(); i++) {
if (!uploadedChunks.contains(i)) {
missingChunks.add(i);
}
}
throw new BusinessException("存在未上传的分片:" + missingChunks);
}
// 检查分片索引的连续性
List<Integer> sortedChunks = new ArrayList<>(uploadedChunks);
Collections.sort(sortedChunks);
for (int i = 0; i < sortedChunks.size(); i++) {
if (sortedChunks.get(i) != i) {
throw new BusinessException("分片索引不连续,存在缺失的分片");
}
}
// 构造存储路径
String tempFolderPath = appConfig.getProjectFolder()
+ Constants.FILE_FOLDER
+ Constants.FILE_FOLDER_TEMP
+ fileDto.getFilePath();
String finalFilePath = appConfig.getProjectFolder()
+ Constants.FILE_FOLDER
+ Constants.FILE_FOLDER_VIDEO
+ fileDto.getFilePath();
// 合并分片文件
File finalFile = new File(finalFilePath);
try (FileOutputStream out = new FileOutputStream(finalFile)) {
for (int i = 0; i < fileDto.getChunks(); i++) {
File chunkFile = new File(tempFolderPath + "/" + i);
if (chunkFile.exists()) {
try (FileInputStream in = new FileInputStream(chunkFile)) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
}
}
}
} catch (IOException e) {
throw new BusinessException("文件合并失败", e);
}
// 检查合并后的文件大小是否与预上传时的总文件大小一致
long mergedFileSize = finalFile.length();
if (mergedFileSize != fileDto.getFileSize()) {
throw new BusinessException("合并后的文件大小不匹配,存在缺失的分片");
}
// 清理临时文件和Redis中的记录
File tempFolder = new File(tempFolderPath);
if (tempFolder.exists()) {
deleteDirectory(tempFolder);
}
redisUtils.del(Constants.REDIS_KEY_UPLOADING_FILE + userId + uploadId);
redisUtils.del(Constants.REDIS_KEY_UPLOADED_CHUNKS + uploadId);
return getSuccessResponseVO(finalFilePath);
}
4. 获取已上传分块信息(getUploadedChunks
方法)
@RequestMapping("/getUploadedChunks")
public ResponseVO getUploadedChunks(@NotEmpty String uploadId) {
Set<Integer> uploadedChunks = redisUtils.smembers(Constants.REDIS_KEY_UPLOADED_CHUNKS + uploadId);
return getSuccessResponseVO(uploadedChunks);
}
5. Redis 工具类方法
// 添加元素到Set集合
public void sadd(String key, Integer value) {
redisTemplate.opsForSet().add(key, value);
}
// 获取Set集合中的所有元素
public Set<Integer> smembers(String key) {
return redisTemplate.opsForSet().members(key);
}
// 删除键值对
public void del(String key) {
redisTemplate.delete(key);
}
通过以上代码,利用Redis的Set集合来记录已上传的分片索引,可以在合并文件时快速检查分片的完整性和连续性,并支持断点续传功能。
优化后的代码的优点和不足之处
优点
-
高效记录与检查已上传分片:
- 使用Redis的Set数据结构来记录已上传的分片索引,可以快速进行插入、删除和查找操作,时间复杂度接近O(1),提高了效率。
-
支持断点续传:
- 前端可以获取已上传的分片信息,只上传未完成的分片,提升了用户体验,特别是在网络不稳定或上传中断的情况下。
-
完整性校验:
- 在合并文件时,通过比较Redis Set集合的大小和预期的分片总数,以及检查分片索引的连续性,确保文件的完整性。
-
文件大小校验:
- 合并文件后,检查合并后的文件大小是否与预上传时的总文件大小一致,进一步确保文件的完整性。
-
资源清理:
- 在文件合并成功后,清理临时存储的分片文件和Redis中的相关记录,避免资源浪费。
-
扩展性:
- 使用Redis进行数据存储和管理,便于扩展和维护,可以轻松地与其他微服务或分布式系统集成。
不足之处
-
Redis性能问题:
- 当分片数量非常大时,Redis的Set数据结构可能会占用较多内存,影响性能。虽然Redis本身是内存数据库,性能较高,但大规模数据操作仍可能导致延迟增加。
-
数据一致性问题:
- 如果在上传过程中服务器宕机或出现其他异常情况,可能会导致Redis中的数据与实际上传的文件不一致。需要额外的机制来确保数据的一致性。
-
并发上传问题:
- 如果多个用户同时上传大量文件,可能会导致Redis和服务器的存储压力增大,需要考虑并发控制和资源限制。
-
错误处理不够完善:
- 当前代码在某些异常情况下的处理不够完善,例如网络中断、存储路径错误等,需要进一步增强错误处理和恢复机制。
-
哈希校验缺失:
- 虽然代码中没有实现,但为了进一步确保文件的完整性,可以考虑在分片上传时使用哈希校验,确保每个分片的内容未被篡改。
-
文件合并的效率问题:
- 当分片数量非常多时,合并文件的过程可能会比较耗时,特别是在磁盘I/O性能较低的情况下。
总结
优化后的代码在功能上较为完善,支持断点续传、完整性校验和资源清理等功能,但在处理大规模数据、并发上传和异常情况时仍存在一些不足。在实际应用中,可以根据具体需求和场景进一步优化和改进。
更多推荐
所有评论(0)