背景:压缩文件夹
@Override
public BaseResponse<GetFolderZipResult> getFolderZipById(String taskId, String fileId, HttpServletRequest request) {
// 校验文件夹是否存在
long start1 = System.currentTimeMillis();
File folder = this.getById(fileId);
if (folder == null || folder.getIsFolder() != 1) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "文件夹不存在或操作资源不是文件夹");
}
String userId = folder.getUserId();
// 创建临时目录和文件
String tempDirPath = ZIP_TEMP_DIR + "/" + userId + "/" + java.util.UUID.randomUUID() + "/";
String zipFileName = "folder_" + System.currentTimeMillis() + ".zip";
java.io.File tempZipFile = new java.io.File(tempDirPath, zipFileName);
// 绝对路径
String absolutePath = tempZipFile.getAbsolutePath();
// 压缩文件的地址
LambdaUpdateWrapper<Downloadtask> updateWrapper = new LambdaUpdateWrapper<Downloadtask>()
.eq(Downloadtask::getTaskId, taskId).set(Downloadtask::getStoragePath, absolutePath);
if (!downloadtaskService.update(updateWrapper)) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "本地路径存储失败");
}
try {
// 确保目录存在
FileUtil.mkdir(tempDirPath);
// 创建ZIP输出流
try (ZipOutputStream zipOutputStream = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tempZipFile)))) {
// 获取所有子文件
List<File> fileList = getAllChildren(fileId, userId);
Map<String, List<File>> childrenByParent = fileList.stream()
.collect(Collectors.groupingBy(File::getParentId));
Map<String, File> fileMap = fileList.stream()
.collect(Collectors.toMap(File::getFileId, Function.identity()));
// 添加根目录
String rootDirName = folder.getName() + "/";
zipOutputStream.putNextEntry(new ZipEntry(rootDirName));
zipOutputStream.closeEntry();
// 递归添加文件到ZIP
processChildrenForZipping(fileId, rootDirName, childrenByParent, fileMap, zipOutputStream);
}
long end1 = System.currentTimeMillis();
System.out.println("执行本地压缩消耗的时间:" + (end1 - start1));
// 上传到COS
String cosStoragePath = FilePathUtil.generateStoragePath(
FileConstant.BIZ_TYPE_ZIP,
userId,
folder.getName() + ".zip"
);
cosManager.putObject(cosStoragePath, tempZipFile);
long end2 = System.currentTimeMillis();
System.out.println("执行上传过程消耗的时间:" + (end2 - end1));
String url = FilePathUtil.generateAccessUrl(cosStoragePath);
GetFolderZipResult result = new GetFolderZipResult();
result.setUrl(url);
result.setStoragePath(absolutePath);
result.setCompleted(true);
result.setTotalSize(tempZipFile.length());
return ResultUtils.success(result);
} catch (IOException e) {
log.error("ZIP文件创建失败:", e);
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "压缩文件创建失败");
} catch (CosClientException e) {
log.error("COS上传失败:", e);
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "文件上传失败");
}
}
这是原本的代码,发现执行耗时非常的长,明明只是压缩20M左右的文件夹,但是执行总耗时却要47秒(差不多一分钟)。
主包肯定是不能允许这种情况存在的。
这显然很不合理,那么为什么会这么耗时呢?主要是因为我的系统的做法是将文件存储到第三方对象存储中,然后如果用户进行文件夹下载的话就需要去不断的从cos获取文件并写入到压缩文件对象中,然后最后进行返回,可见整个流程肯定是非常耗时的。
我一开始排查耗时的原因的时候首先我怀疑的是因为使用的是java原生的方式进行压缩,导致非常的耗时,后面就采用 zstd 进行压缩,但是我发现好像影响不是很大,哪怕我直接把 level调整为1,耗时也只比之前快了不到一秒的时间。
那这不是等于优化了一个寂寞?
那么很明显肯定还有其他的地方执行耗时比较高
long startDownload = System.currentTimeMillis();
COSObject object = cosClientConfig.cosClient().getObject(cosClientConfig.getBucket(), parseStoragePath(file.getStoragePath()));
long downloadTime = System.currentTimeMillis() - startDownload;
System.out.println(downloadTime);
522
118
118
105
86
68
81
82
72
82
79
……
经过我的排查发现,这个从cos获取文件的代码貌似就是罪魁祸首,然后我输出执行耗时发现,虽然每个消耗的时间都不多,但是当文件一多起来的时候就需要消耗很多的时间,主要原因还是因为是串行执行
那么到这里要修改的方式就很简单了,那么就是改成并行执行
@Override
public BaseResponse<GetFolderZipResult> getFolderZipById(String taskId, String fileId, HttpServletRequest request) {
// 校验文件夹是否存在
File folder = this.getById(fileId);
if (folder == null || folder.getIsFolder() != 1) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "文件夹不存在或操作资源不是文件夹");
}
String userId = folder.getUserId();
// 创建临时目录和文件
String tempDirPath = ZIP_TEMP_DIR + "/" + userId + "/" + java.util.UUID.randomUUID() + "/";
String zipFileName = "folder_" + System.currentTimeMillis() + ".tar.zst";
java.io.File tempZipFile = new java.io.File(tempDirPath, zipFileName);
// 绝对路径
String absolutePath = tempZipFile.getAbsolutePath();
// 压缩文件的地址
LambdaUpdateWrapper<Downloadtask> updateWrapper = new LambdaUpdateWrapper<Downloadtask>()
.eq(Downloadtask::getTaskId, taskId).set(Downloadtask::getStoragePath, absolutePath);
if (!downloadtaskService.update(updateWrapper)) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "本地路径存储失败");
}
try {
// 确保目录存在
FileUtil.mkdir(tempDirPath);
// 1. 准备待下载文件列表
List<FileDownloadInfo> downloadInfos = prepareFilesForZip(fileId, userId, folder.getName());
// 2. 并行下载文件
Map<String, byte[]> downloadedFiles = new ConcurrentHashMap<>();
ExecutorService executor = Executors.newFixedThreadPool(10); // 调整线程池大小
List<CompletableFuture<Void>> futures = new ArrayList<>();
for (FileDownloadInfo downloadInfo : downloadInfos) {
if (!downloadInfo.isFolder()) {
futures.add(CompletableFuture.runAsync(() -> {
try {
COSObject object = cosClientConfig.cosClient().getObject(cosClientConfig.getBucket(), parseStoragePath(downloadInfo.getStoragePath()));
try (InputStream cosStream = object.getObjectContent();
ByteArrayOutputStream bos = new ByteArrayOutputStream()) { // 或下载到临时文件
IOUtils.copy(cosStream, bos);
downloadedFiles.put(downloadInfo.getRelativePath(), bos.toByteArray()); // 或存储临时文件路径
}
} catch (CosClientException | IOException e) {
log.error("COS 文件下载失败:{}", downloadInfo.getStoragePath(), e);
// 可以选择抛出异常或记录错误,根据业务需求处理
}
}, executor));
}
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
executor.shutdown();
long startCompress = System.currentTimeMillis();
// 3. 写入 ZIP 归档
try (OutputStream fOut = new FileOutputStream(tempZipFile);
ZstdCompressorOutputStream zstdOut = new ZstdCompressorOutputStream(fOut, 1);
TarArchiveOutputStream tarOut = new TarArchiveOutputStream(zstdOut)) {
tarOut.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX);
writeToZipArchive(downloadInfos, downloadedFiles, tarOut);
}
long endCompress = System.currentTimeMillis();
System.out.println("执行本地压缩消耗的时间:" + (endCompress - startCompress));
// 4. 上传到 COS (与原逻辑相同)
String cosStoragePath = FilePathUtil.generateStoragePath(
FileConstant.BIZ_TYPE_ZIP,
userId,
folder.getName() + ".tar.zst"
);
cosManager.putObject(cosStoragePath, tempZipFile);
long endUpload = System.currentTimeMillis();
System.out.println("执行上传过程消耗的时间:" + (endUpload - endCompress));
String url = FilePathUtil.generateAccessUrl(cosStoragePath);
GetFolderZipResult result = new GetFolderZipResult();
result.setUrl(url);
result.setStoragePath(absolutePath);
result.setCompleted(true);
result.setTotalSize(tempZipFile.length());
System.out.println("压缩文件的大小:" + tempZipFile.length());
return ResultUtils.success(result);
} catch (IOException e) {
log.error("ZIP 文件创建失败:", e);
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "压缩文件创建失败");
} catch (CosClientException e) {
log.error("COS 上传失败:", e);
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "文件上传失败");
}
}
private List<FileDownloadInfo> prepareFilesForZip(String parentId, String userId, String rootFolderName) {
List<File> allFiles = getAllChildren(parentId, userId);
List<FileDownloadInfo> downloadInfos = new ArrayList<>();
Map<String, File> fileMap = allFiles.stream().collect(Collectors.toMap(File::getFileId, Function.identity()));
// 添加根目录
downloadInfos.add(new FileDownloadInfo(getById(parentId), rootFolderName + "/"));
// 递归构建相对路径
buildDownloadInfo(parentId, rootFolderName + "/", fileMap, downloadInfos);
return downloadInfos;
}
private void buildDownloadInfo(String parentId, String parentPath, Map<String, File> fileMap, List<FileDownloadInfo> downloadInfos) {
List<File> children = getAllChildrenByParentId(parentId); // 假设有这个方法
for (File child : children) {
String relativePath = parentPath + child.getName() + (child.getIsFolder() == 1 ? "/" : "");
downloadInfos.add(new FileDownloadInfo(child, relativePath));
if (child.getIsFolder() == 1) {
buildDownloadInfo(child.getFileId(), relativePath, fileMap, downloadInfos);
}
}
}
private void writeToZipArchive(List<FileDownloadInfo> downloadInfos, Map<String, byte[]> downloadedFiles, TarArchiveOutputStream tarOut) throws IOException {
for (FileDownloadInfo fileInfo : downloadInfos) {
if (fileInfo.isFolder()) {
addDirectoryToTar(fileInfo.getRelativePath(), fileInfo.getUpdateTime(), tarOut);
} else {
TarArchiveEntry entry = new TarArchiveEntry(fileInfo.getRelativePath());
entry.setSize(fileInfo.getSize() != null ? fileInfo.getSize() : 0);
entry.setModTime(fileInfo.getUpdateTime().getTime());
entry.setMode(0644);
tarOut.putArchiveEntry(entry);
byte[] content = downloadedFiles.get(fileInfo.getRelativePath());
if (content != null) {
IOUtils.copy(new ByteArrayInputStream(content), tarOut);
} else {
log.warn("文件内容未找到:{}", fileInfo.getRelativePath());
// 可以选择抛出异常或记录警告
}
tarOut.closeArchiveEntry();
}
}
}
private List<File> getAllChildrenByParentId(String parentId) {
// 实现根据 parentId 查询子文件和文件夹的逻辑
LambdaQueryWrapper<File> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(File::getParentId, parentId)
.orderByAsc(File::getIsFolder); // 文件夹在前
// 可以直接在 getAllChildren 方法中处理层级关系,返回扁平列表,然后在 prepareFilesForZip 中构建层级信息
return this.list(queryWrapper); // 替换为实际的数据库查询
}
private void addDirectoryToTar(String entryPath, Date modifiedTime, TarArchiveOutputStream tarOut) throws IOException {
TarArchiveEntry dirEntry = new TarArchiveEntry(entryPath);
dirEntry.setSize(0); // 文件夹大小强制为0
dirEntry.setMode(0755);
if (modifiedTime != null) {
dirEntry.setModTime(modifiedTime.getTime());
}
tarOut.putArchiveEntry(dirEntry);
tarOut.closeArchiveEntry();
}
使用多线程之后,那个速度可谓是非常的快啊
和之前的简直就是一个天一个地
只能说是非一般的感觉好吧
我这里设置的线程池大小是10,其实我也试过20,会更加的快一点,但是其实也没必要了,10够用了,没必要再去浪费资源,大家可以根据自己的系统去进行设计。
最后如果各位大佬还有什么优化的思路的话,欢迎大家进行指教,我一定会好好向各位大佬学习的。
谢谢这位靓仔靓女的观看。