工作中有一个下载对账的定时任务,通过调用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();
}