BASE64编码字符串解码时堆内存溢出

工作中有一个下载对账的定时任务,通过调用dubbo服务去下载文件。运行一段时间后,发现dubbo超时异常。通过排查发现是序列化超时了。dubbo默认的序列化大小是8MB,而这个文件大约30MB。增大序列化文件大小不可取,无奈只得更改方案:dubbo服务下载对账单文件后,不再返回内容,而是通过启动新线程直接保存到共享存储目录,然后直接返回空内容即可。然后由quartz定时任务去取文件进行后续处理。
但是测试起来又发现了第二个问题,对账单文件是经过压缩,然后通过BASE64编码后进行传输的,文件大约有30多MB。解码时堆内存直接OOM了(大约暴涨了600mb内存,我也不太清楚为什么String.getBytes()会让内存暴涨这么多)。报错代码如下:

byte[] data = Base64.decodeBase64(billStr.getBytes("UTF-8"));

无奈继续修改方案:
1、将字符串进行按4的倍数分割,例如1024 * 1024 * 4(4mb)。
2、将截取的字符串进行BASE64解码,并写入文件。
3、然后将文件保存至共享存储目录。

字符串按照4的倍数分割,是因为BASE64编码是3个字节一编,然后编成4个字节,对应4个字符。因此按照4的倍数截取,不会导致内容错乱。

 /**
     * 保存下载的账单到共享存储
     */
    private void saveBillToDisk(String billStr) {
        try {
            String fileName = "bill";
            File file = new File(configBean.getBillSavedPath() + fileName);
        
            // 文件如果有则删除
            if(file.exists()){
                file.delete();
            }
            
            int size = configBean.getBillCutSize() * RANGE_UNIT; // 每次读取大小
            int beginIndex = 0;
            int endIndex = size;
            int length = billStr.length();

            // 文件小,直接解码保存
            if(length < size){
                byte[] data = Base64.decodeBase64(billStr.getBytes("UTF-8"));
                FileUtils.writeByteArrayToFile(file, data); // 该方法会自动创建文件
            }else{
                // 文件过大,分批保存
                while(endIndex < length){
                    String tmpStr = billStr.substring(beginIndex,endIndex);

                    byte[] data = Base64.decodeBase64(tmpStr.getBytes("UTF-8"));
                    FileUtils.writeByteArrayToFile(file, data,true); // 该方法会自动创建文件

                    beginIndex += size;
                    endIndex += size;
                }

                // 剩下的
                String lastStr = billStr.substring(beginIndex,length);
                byte[] lastData = Base64.decodeBase64(lastStr.getBytes("UTF-8"));
                FileUtils.writeByteArrayToFile(file, lastData,true); // 该方法会自动创建文件
            }

            finishSaveBill(context);// 标记上传完成
        } catch (Exception e) {
            log.error("保存账单到共享存储发生异常", e);
        }
    }

每次解码的文件大小可以根据内存大小自行合理配置,太小的话IO操作过于频繁耗时长,太大的话就跟之前的一样导致堆内存溢出(我这里设置的是12MB):

    // 字符串截取范围单位,这里是1MB。
    private static final int RANGE_UNIT = 1024 * 1024;

上传完成后,通过保存一个空的done文件,表示文件保存完毕:

 /**
     * 标记文件已经上传完
     */
    private void finishSaveBill(String fileName) throws Exception{
        String doneFileName = configBean.getBillSavedPath() + fileName + ".done";
        log.info("done文件保存路径:{}",configBean.getBillSavedPath() + fileName + ".done");

        // 创建一个done文件
        File doneFile = new File(doneFileName);
        if(doneFile.exists()){
            doneFile.delete();
        }

        FileUtils.writeStringToFile(doneFile,"","UTF-8");
    }

这样定时任务就可以通过循环判断done文件来判断账单文件是否保存完毕。

        int queryTimes = 0;
        while(queryTimes < Constants.MAX_QUERY_TIMES){
            if(isUploadDone(fileName)){
                break;
            }

            Thread.sleep(10*1000);//sleep 10s
            queryTimes += 1;
        }

        if(queryTimes >= Constants.MAX_QUERY_TIMES){
            log.warn("轮询次数超过限制,放弃处理。")
            return false;
        }

       // 账单文件保存完毕,进行后续处理
       ...
/**
 * 检查文件是否上传完了
 * */
public boolean isUploadDone(String fileName){
    String filePath = configBean.getBillSavePath() + fileName + ".done";
    File doneFile = new File(filePath);
    
    return doneFile.exists();
}

你可能感兴趣的:(Thinking,in,Java)