新大陆,Java-压缩竟然还能这么优化!

前言

最近在做数据导出的功能,由于要支持批量导出且导出的文件都巨大3GB起,所以决定在导出最终结果时进行压缩

第一天

java压缩,emmm...首先想到的就是java.util.zip下面的各种api,直接上代码:

/**
*  批量压缩文件 v1.0
*  
* @param fileNames 需要压缩的文件名称列表(包含相对路径) 
* @param zipOutName 压缩后的文件名称
**/
public static void batchZipFiles(List<String> fileNames, String zipOutName) {
        //设置读取数据缓存大小
        byte[] buffer = new byte[4096];
        ZipOutputStream zipOut = null;
        try {
            zipOut = new ZipOutputStream(new FileOutputStream(zipOutName));
            for (String fileName : fileNames) {
                File inputFile = new File(fileName);
                if (inputFile.exists()) {
                    BufferedInputStream bis = new BufferedInputStream(new FileInputStream(inputFile));
                    //将文件写入zip内,即将文件进行打包
                    zipOut.putNextEntry(new ZipEntry(inputFile.getName()));
                    //写入文件的方法,同上
                    int size = 0;
                    //设置读取数据缓存大小
                    while ((size = bis.read(buffer)) >= 0) {
                        zipOut.write(buffer, 0, size);
                    }
                    //关闭输入输出流
                    zipOut.closeEntry();
                    bis.close();
                }
            }
        } catch (Exception e) {
            log.error("batchZipFiles error:sourceFileNames:" + JSONObject.toJSONString(fileNames), e);
        } finally {
            if (null != zipOut) {
                try {
                    zipOut.close();
                } catch (Exception e) {
                    log.error("batchZipFiles error:sourceFileNames:" + JSONObject.toJSONString(fileNames), e);
                }
            }
        }
    }

首先利用BufferedInputStream读取文件内容,ZipOutputStream的putNextEntry方法对每一个文件进行压缩写入。

最后将所有压缩后的文件写入到最终的zipOutName文件中。由于用了BufferedInputStream缓冲输入流,文件的读取和写入都是从缓存区(内存)中也就是代码里面对应的byte数组获取,相比较普通的FileInputStream提升了较大的效率。但是不够!耗时如下:


压缩三个大小为3.5GB的文件

第二天

想到了NIO,传统的IO叫BIO(上面代码)是同步阻塞的,读写都在一个线程中。NIO则是同步非阻塞的,核心是channel(通道),buffer(缓冲区),Selector(选择器)。

其实说的通俗易懂点就是NIO在密集型计算下效率之所以比BIO高的原因是NIO是多路复用,用更少的线程最更多的事情,相对于BIO大大减少了线程切换,竞争带来的资源损耗。不多BB了,上代码:


/**
     *  批量压缩文件 v2.0
     *
     * @param fileNames 需要压缩的文件名称列表(包含相对路径) 
     * @param zipOutName 压缩后的文件名称
     **/
    public static void batchZipFiles(List<String> fileNames, String zipOutName) throws Exception {
        ZipOutputStream zipOutputStream = null;
        WritableByteChannel writableByteChannel = null;
        ByteBuffer buffer = ByteBuffer.allocate(2048);
        try {
            zipOutputStream = new ZipOutputStream(new FileOutputStream(zipOutName));
            writableByteChannel = Channels.newChannel(zipOutputStream);
            for (String sourceFile : fileNames) {
                File source = new File(sourceFile);
                zipOutputStream.putNextEntry(new ZipEntry(source.getName()));
                FileChannel fileChannel = new FileInputStream(sourceFile).getChannel();
                while (fileChannel.read(buffer) != -1) {
                    //更新缓存区位置
                    buffer.flip();
                    while (buffer.hasRemaining()) {
                        writableByteChannel.write(buffer);
                    }
                    buffer.rewind();
                }
                fileChannel.close();
            }

        } catch (Exception e) {
            log.error("batchZipFiles error  fileNames:" + JSONObject.toJSONString(fileNames), e);
        } finally {
            zipOutputStream.close();
            writableByteChannel.close();
            buffer.clear();
        }
    }

还是利用java.nio包下面的api,首先用Channels.newChannel()方法将zipOutputStream输出流创建一个写的通道通道,在读取文件内容的时候直接用FileInputStream.getChannel()

获取当前文件读的通道,然后从读的通道中通过ByteBuffer(缓冲区)读取文件内容写入writableByteChannel写通道中,一定记得反转缓冲区buffer.flip(),否则读取的内容就是文件最后的内容byte=0时的。这种方法相较于上面的速度如下图所示:



压缩三个大小为3.5GB的文件

第三天

继续优化,听说用上内存映射文件的方式更快!那还等什么,让我来try一try!撸代码:

/**
     *  批量压缩文件 v3.0
     *
     * @param fileNames 需要压缩的文件名称列表(包含相对路径) 
     * @param zipOutName 压缩后的文件名称
     **/
public static void batchZipFiles(List<String> fileNames, String zipOutName) {
        ZipOutputStream zipOutputStream = null;
        WritableByteChannel writableByteChannel = null;
        MappedByteBuffer mappedByteBuffer = null;
        try {
            zipOutputStream = new ZipOutputStream(new FileOutputStream(zipOutName));
            writableByteChannel = Channels.newChannel(zipOutputStream);
            for (String sourceFile : fileNames) {
                File source = new File(sourceFile);
                long fileSize = source.length();
                zipOutputStream.putNextEntry(new ZipEntry(source.getName()));
                int count = (int) Math.ceil((double) fileSize / Integer.MAX_VALUE);
                long pre = 0;
                long read = Integer.MAX_VALUE;
                //由于一次映射的文件大小不能超过2GB,所以分次映射          
                for (int i = 0; i < count; i++) {
                    if (fileSize - pre < Integer.MAX_VALUE) {
                        read = fileSize - pre;
                    }
                    mappedByteBuffer = new RandomAccessFile(source, "r").getChannel()
                            .map(FileChannel.MapMode.READ_ONLY, pre, read);
                    writableByteChannel.write(mappedByteBuffer);
                    pre += read;
                }
                //释放资源
                Method m = FileChannelImpl.class.getDeclaredMethod("unmap", MappedByteBuffer.class);
                m.setAccessible(true);
                m.invoke(FileChannelImpl.class, mappedByteBuffer);
                mappedByteBuffer.clear();
            }
        } catch (Exception e) {
            log.error("zipMoreFile error  fileNames:" + JSONObject.toJSONString(fileNames), e);
        } finally {
            try {
                if (null != zipOutputStream) {
                    zipOutputStream.close();
                }
                if (null != writableByteChannel) {
                    writableByteChannel.close();
                }
                if (null != mappedByteBuffer) {
                    mappedByteBuffer.clear();
                }
            } catch (Exception e) {
                log.error("zipMoreFile error  fileNames:" + JSONObject.toJSONString(fileNames), e);
            }
        }
    }

这里有两个坑的地方是:

1.利用MappedByteBuffer.map文件时如果文件太大超过了Integer.MAX时(大约是2GB)就会报错:

所以这里需要分次将要写入的文件映射为内存文件。

2.这里有个bug,就是将文件映射到内存后,在写完就算clear了mappedByteBuffer,也不会释放内存,这时候就需要手动去释放,详细见上代码。

看速度!


压缩三个大小为3.5GB的文件

肯定是我的打开方式有问题,为什么反而是最慢的。。难道是文件太大了吗?我的机器内存太小了?还是我用的有问题,让我思考一下。。希望留言区讨论一下。

第四天

我在想批量压缩文件这么慢是不是因为是串行的,如果改成多线程并行那不是会快了?说干就干,本来想自己写的,后来在google上查资料发现apache-commons有现成的,那果断不重复造轮子,上代码:

/**
     *  批量压缩文件 v4.0
     *
     * @param fileNames 需要压缩的文件名称列表(包含相对路径) 
     * @param zipOutName 压缩后的文件名称
     **/
public static void compressFileList(String zipOutName, List<String> fileNameList) throws IOException, ExecutionException, InterruptedException {
        ThreadFactory factory = new ThreadFactoryBuilder().setNameFormat("compressFileList-pool-").build();
        ExecutorService executor = new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(20), factory);
        ParallelScatterZipCreator parallelScatterZipCreator = new ParallelScatterZipCreator(executor);
        OutputStream outputStream = new FileOutputStream(zipOutName);
        ZipArchiveOutputStream zipArchiveOutputStream = new ZipArchiveOutputStream(outputStream);
        zipArchiveOutputStream.setEncoding("UTF-8");
        for (String fileName : fileNameList) {
            File inFile = new File(fileName);
            final InputStreamSupplier inputStreamSupplier = () -> {
                try {
                    return new FileInputStream(inFile);
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                    return new NullInputStream(0);
                }
            };
            ZipArchiveEntry zipArchiveEntry = new ZipArchiveEntry(inFile.getName());
            zipArchiveEntry.setMethod(ZipArchiveEntry.DEFLATED);
            zipArchiveEntry.setSize(inFile.length());
            zipArchiveEntry.setUnixMode(UnixStat.FILE_FLAG | 436);
            parallelScatterZipCreator.addArchiveEntry(zipArchiveEntry, inputStreamSupplier);
        }
        parallelScatterZipCreator.writeTo(zipArchiveOutputStream);
        zipArchiveOutputStream.close();
        outputStream.close();
        log.info("ParallelCompressUtil->ParallelCompressUtil-> info:{}", JSONObject.toJSONString(parallelScatterZipCreator.getStatisticsMessage()));
    }

先看结果:


压缩三个大小为3.5GB的文件

果然还是并行的快!


©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,898评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,401评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,058评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,539评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,382评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,319评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,706评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,370评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,664评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,715评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,476评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,326评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,730评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,003评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,275评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,683评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,877评论 2 335

推荐阅读更多精彩内容

  • 今天感恩节哎,感谢一直在我身边的亲朋好友。感恩相遇!感恩不离不弃。 中午开了第一次的党会,身份的转变要...
    迷月闪星情阅读 10,534评论 0 11
  • 彩排完,天已黑
    刘凯书法阅读 4,168评论 1 3
  • 没事就多看看书,因为腹有诗书气自华,读书万卷始通神。没事就多出去旅游,别因为没钱而找借口,因为只要你省吃俭用,来...
    向阳之心阅读 4,765评论 3 11
  • 表情是什么,我认为表情就是表现出来的情绪。表情可以传达很多信息。高兴了当然就笑了,难过就哭了。两者是相互影响密不可...
    Persistenc_6aea阅读 123,292评论 2 7