Java 大文件压缩与上传优化

 问题背景:

        在业务开发中有部分文件占内存特别大(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);
        }
    }

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值