1. 什么是分片上传?
分片上传是将大文件分割成多个小片段,分别上传到服务器,最后在服务器端将这些片段合并成完整文件的上传方式。这种方式可以实现断点续传、并行上传等功能,提高大文件上传的可靠性和效率。
2. 实现原理
分片上传的基本流程:
-
客户端将文件分割成固定大小的片段
-
每个片段都有一个序号(chunkIndex)
-
服务器接收到片段后存储到临时目录
-
所有片段上传完成后进行合并
3. 代码实现分析
3.1 Controller层实现
java
@RequestMapping("/uploadFile")
public ResponseVO uploadFile(HttpSession session, String fileId, MultipartFile file,
@NotEmpty String fileName, @NotEmpty String filePid,
@NotEmpty String fileMd5, @NotNull Integer chunkIndex,
@NotNull Integer chunks) {
SessionWebUserDto webUserDto = getUserInfoFromSession(session);
UploadResultDto resultDto = fileInfoService.uploadFile(webUserDto, fileId, file,
fileName, filePid, fileMd5,
chunkIndex, chunks);
return getSuccessResponseVO(resultDto);
}
主要参数说明:
-
fileId: 文件唯一标识
-
fileMd5: 文件的MD5值,用于秒传判断
-
filePid: 父文件夹ID
-
fileName: 文件名称
-
chunkIndex:文件分片的索引
-
chunks:文件分片的总数
3.2 Service层实现
java
@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();
// 用户的临时文件暂存路径
File uerTempDirPath = null;
boolean uploadSuccess = true;
try {
if(StrUtil.isEmpty(fileId)){
fileId = RandomUtil.randomString(Constants.LENGTH_10);
}
resultDto.setFileId(fileId);
Date curDate = new Date();
// 获取用户已使用的空间
UserSpaceDto userSpaceDto = redisComponent.getUserSpaceUse(webUserDto.getUserId());
if(chunkIndex == 0) {
// 第一次上传(第一个分片),去数据库中查找有没有这个文件,如果有,则可秒传
// ...妙传逻辑在此处省略,可查看我的另一篇博客
return resultDto;
}
}
// 没找到,进行分片上传
// 判断网盘空间
Long currentTempSize = redisComponent.getFileTempSize(webUserDto.getUserId(), fileId);
if (file.getSize() + currentTempSize + userSpaceDto.getUseSpace() > userSpaceDto.getTotalSpace()) {
// 网盘空间不足
throw new BusinessException(ResponseCodeEnum.CODE_904);
}
// 临时文件暂存目录
String tempDirPath = appConfig.getProjectFolder() + Constants.FILE_FOLDER_TEMP;
String userFileFolderName = webUserDto.getUserId() + fileId;
// 用户的临时文件暂存路径
uerTempDirPath = new File(tempDirPath + userFileFolderName);
if(!uerTempDirPath.exists()){
// 如果不存在,则创建
uerTempDirPath.mkdirs();
}
// 分片文件
File chunkFile = new File(uerTempDirPath.getPath() + "/" +chunkIndex);
file.transferTo(chunkFile);
// 保存临时文件大小
redisComponent.saveFileTempSize(webUserDto.getUserId(), fileId, file.getSize());
if(chunkIndex < chunks - 1){
// 不是最后一个分片
resultDto.setStatus(UploadStatusEnums.UPLOADING.getCode());
return resultDto;
}
// 最后一个分片上传完成,记录数据库,异步合并分片
String month = DateUtil.format(curDate, "yyyyMM");
String fileSuffix = StringTools.getFileSuffix(fileName);
// 真实文件名
String realFileName = userFileFolderName + fileSuffix;
FileTypeEnums fileTypeEnums = FileTypeEnums.getFileTypeBySuffix(fileSuffix);
// 自动重命名
fileName = autoRename(fileName, filePid, webUserDto.getUserId());
// 将文件写入数据库
FileInfo fileInfo = new FileInfo();
fileInfo.setFileId(fileId);
fileInfo.setUserId(webUserDto.getUserId());
fileInfo.setFileMd5(fileMd5);
fileInfo.setFileName(fileName);
fileInfo.setFilePath(month + "/" + realFileName);
fileInfo.setFilePid(filePid);
fileInfo.setCreateTime(curDate);
fileInfo.setLastUpdateTime(curDate);
fileInfo.setFileCategory(fileTypeEnums.getCategory().getCode());
fileInfo.setFileType(fileTypeEnums.getType());
fileInfo.setStatus(FileStatusEnums.TRANSFER.getStatus());
fileInfo.setFolderType(FileFolderTypeEnums.FILE.getType());
fileInfo.setDelFlag(FileDelFlagEnums.USING.getFlag());
this.baseMapper.insert(fileInfo);
// 更新用户使用空间
Long useSpace = redisComponent.getFileTempSize(webUserDto.getUserId(), fileId);
updateUserSpace(webUserDto, useSpace);
resultDto.setStatus(UploadStatusEnums.UPLOAD_FINISH.getCode());
// 异步合并分片
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
fileInfoService.transferFile(fileInfo.getFileId(), webUserDto);
}
});
return resultDto;
}catch (BusinessException e) {
log.error("上传文件失败", e);
throw e;
} catch (IOException e) {
log.error("上传文件失败", e);
uploadSuccess = false;
}finally {
if(uerTempDirPath != null && !uploadSuccess){
FileUtil.del(uerTempDirPath);
}
}
return resultDto;
}
3.3 临时文件大小管理
使用Redis管理临时文件大小:
java
@Component
public class RedisComponent {
/**
* 获取临时文件大小
*/
public Long getFileTempSize(String userId, String fileId) {
Long currentSize = getFileSize(Constants.REDIS_KEY_USER_FILE_TEMP_SIZE +
userId + fileId);
return currentSize;
}
/**
* 保存临时文件大小
*/
public void saveFileTempSize(String userId, String fileId, Long fileSize) {
Long currentSize = getFileTempSize(userId, fileId);
stringRedisTemplate.opsForValue().set(
Constants.REDIS_KEY_USER_FILE_TEMP_SIZE + userId + fileId,
Convert.toStr(currentSize + fileSize),
Constants.REDIS_KEY_EXPIRES_ONE_HOUR,
TimeUnit.SECONDS
);
}
}
4. 核心技术要点
4.1 分片管理
-
使用chunkIndex标识分片序号
-
使用临时目录管理分片文件
4.2 空间管理
-
实时统计临时文件占用空间
-
使用Redis缓存临时文件大小
-
定期清理过期的临时文件
4.3 文件存储
-
按用户ID和文件ID创建临时目录
-
分片文件使用序号命名
-
支持并发上传多个文件