问题背景:
在业务开发中有部分文件占内存特别大(5个G以上),并且文件夹嵌套层级关系附件(有上千个文件夹)打包下载的时候服务就会挂掉。
问题分析:
1.大文件压缩问题:
由于在业务中这种内存很大的文件占比很低,项目初期该功能实现的时候就采用了java.util.zip下ZipOutputStream常规的压缩方法,由于是单线程执行,文件量很大的时候服务就会崩掉。
2.大文件压缩完以后上传问题:
优化前项目中采用的是java.io下的BufferedInputStream跟BufferedOutputStream对压缩后的文件进行上传。BufferedInputStream与最原始的InputStresam相比,可以读取更大的字节块并对其进行缓冲可大大加快 IO 速度。但是文件过大缓冲区同样会过大从而占用过多内存,上传耗时会很久。
问题解决思路:
1.大文件压缩过慢问题优化:
采用多线程压缩,加快压缩速度。通过网上翻阅资料发现org.apache.commons中存现成的多线程压缩方法,优化后压缩文件代码如下:
/**
* 压缩文件
*
* @param zipFileName 压缩文件名
* @param zipFiles 需压缩的文件列表
*/
private void zipFiles(String zipFileName, List<ZipFileDTO> zipFiles) {
try {
long zipStartTime = System.currentTimeMillis();
ParallelScatterZipCreator parallelScatterZipCreator = new ParallelScatterZipCreator(threadPoolExecutor("zip"));
FileOutputStream outputStream = new FileOutputStream(zipFileName);
ZipArchiveOutputStream zipArchiveOutputStream = new ZipArchiveOutputStream(outputStream);
zipArchiveOutputStream.setEncoding("UTF-8");
for (ZipFileDTO dto : zipFiles) {
String filePath =dto.getFileName();
File file = new File(filePath);
if (!file.exists()) {
logger.warn("文件不存在,忽略压缩:" + dto.getFileName());
continue;
}
final InputStreamSupplier inputStreamSupplier = () -> {
try {
return new FileInputStream(file);
} catch (FileNotFoundException e) {
logger.error("文件不存在:" + dto.getFileName());
return new NullInputStream(0);
}
};
ZipArchiveEntry zipArchiveEntry = new ZipArchiveEntry(dto.getFileName());
zipArchiveEntry.setMethod(ZipArchiveEntry.DEFLATED);
zipArchiveEntry.setSize(file.length());
zipArchiveEntry.setUnixMode(UnixStat.FILE_FLAG | 436);
parallelScatterZipCreator.addArchiveEntry(zipArchiveEntry, inputStreamSupplier);
}
parallelScatterZipCreator.writeTo(zipArchiveOutputStream);
zipArchiveOutputStream.flush();
zipArchiveOutputStream.close();
long zipEndTime = System.currentTimeMillis();
logger.info("文件压缩完成,耗时:" + (zipEndTime - zipStartTime) / 1000 + "秒");
} catch (Exception e) {
throw new RuntimeException("文件压缩失败:" + e.getMessage(), e);
}
}
private ThreadPoolExecutor threadPoolExecutor(String threadName) {
return new ThreadPoolExecutor(2,
10,
60,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10000),
new ThreadFactory() {
final AtomicInteger atomicInteger = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, threadName + "-" + atomicInteger.getAndAdd(1));
}
},
new ThreadPoolExecutor.AbortPolicy());
}
2.压缩完后上传过慢问题优化:
该项目存储文件使用的是阿里云的OSS存储系统,通过翻阅OSS的api发现oss提供了分片上传功能,直接采用这种方式,也就是将所要上传的文件,按照规定的大小,分成一块一块的数据块,进行分别上传,再由服务端将这些数据块合成一个完整的文件。提高上传效率。代码如下:
/**
* 上传文件
*
* @param ossClient oss客户端
* @param zipPath 压缩包路径
* @param bucketName bucket名称
* @param ossPath oss完整路径
*/
private long uploadZip(OSSClient ossClient, String zipPath, String bucketName , String ossPath) {
try {
logger.info("开始分片上传文件");
long startUploadTime = System.currentTimeMillis();
File zipFile = new File(zipPath);
// 创建InitiateMultipartUploadRequest对象。
InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(bucketName, ossPath);
// 初始化分片。
InitiateMultipartUploadResult upresult = ossClient.initiateMultipartUpload(request);
// 返回uploadId,它是分片上传事件的唯一标识。您可以根据该uploadId发起相关的操作,例如取消分片上传、查询分片上传等。
String uploadId = upresult.getUploadId();
// partETags是PartETag的集合。PartETag由分片的ETag和分片号组成。
List<PartETag> partETags = new ArrayList<PartETag>();
// 每个分片的大小,用于计算文件有多少个分片。单位为字节。
final long partSize = 10 * 1024 * 1024L; //10 MB。
// 填写本地文件的完整路径。
long fileLength = zipFile.length();
int partCount = (int) (fileLength / partSize);
if (fileLength % partSize != 0) {
partCount++;
}
// 遍历分片上传。
for (int i = 0; i < partCount; i++) {
long startPos = i * partSize;
long curPartSize = (i + 1 == partCount) ? (fileLength - startPos) : partSize;
InputStream instream = new FileInputStream(zipFile);
// 跳过已经上传的分片。
instream.skip(startPos);
UploadPartRequest uploadPartRequest = new UploadPartRequest();
uploadPartRequest.setBucketName(bucketName);
uploadPartRequest.setKey(ossPath);
uploadPartRequest.setUploadId(uploadId);
uploadPartRequest.setInputStream(instream);
// 设置分片大小。除了最后一个分片没有大小限制,其他的分片最小为100 KB。
uploadPartRequest.setPartSize(curPartSize);
// 设置分片号。每一个上传的分片都有一个分片号,取值范围是1~10000,如果超出此范围,OSS将返回InvalidArgument错误码。
uploadPartRequest.setPartNumber(i + 1);
// 每个分片不需要按顺序上传,甚至可以在不同客户端上传,OSS会按照分片号排序组成完整的文件。
UploadPartResult uploadPartResult = ossClient.uploadPart(uploadPartRequest);
// 每次上传分片之后,OSS的返回结果包含PartETag。PartETag将被保存在partETags中。
partETags.add(uploadPartResult.getPartETag());
}
// 创建CompleteMultipartUploadRequest对象。
// 在执行完成分片上传操作时,需要提供所有有效的partETags。OSS收到提交的partETags后,会逐一验证每个分片的有效性。当所有的数据分片验证通过后,OSS将把这些分片组合成一个完整的文件。
CompleteMultipartUploadRequest completeMultipartUploadRequest = new CompleteMultipartUploadRequest(bucketName, ossPath, uploadId, partETags);
// 完成分片上传。
ossClient.completeMultipartUpload(completeMultipartUploadRequest);
logger.info("文件上传完成,耗时:" + (System.currentTimeMillis() - startUploadTime) / 1000 + "秒");
return fileLength;
} catch (Exception e) {
throw new RuntimeException("文件上传失败:" + e.getMessage(), e);
}
}