从Azure Blob存储下载大文件时如何避免OutOfMemoryError?
解决方案
核心问题分析
当前代码出现OOM的根源有两个:
- 将整个文件全量加载到内存(
ByteArrayOutputStream转byte[]),大文件直接超出JVM堆内存上限; downloadRange方法参数错误:第二个参数应为结束偏移量,原代码传入文件总大小,导致每次循环重复下载从当前位置到文件末尾的内容,内存占用呈倍数增长。
方案1:直接写入响应流(推荐,彻底解决OOM)
无需新增库,修改代码将文件块直接写入HTTP响应流,避免全量加载到内存:
主方法修改:
public void getFileBytes(@RequestBody ZeusRequestVO<String> request, HttpServletResponse response) { documentService.downloadFileToResponse(request.getPayload(), response); }
下载逻辑修改:
public void downloadFileToResponse(String blobName, HttpServletResponse response) { long chunkSize = 10 * 1024 * 1024; // 10MB分块 CloudBlockBlob blob = connectAndgetCloudBlockBlob(blobName); try { if (blob.exists()) { blob.downloadAttributes(); long fileSize = blob.getProperties().getLength(); // 设置响应头,告知客户端文件信息 response.setContentType(blob.getProperties().getContentType()); response.setHeader("Content-Disposition", "attachment; filename=\"" + blobName + "\""); response.setContentLengthLong(fileSize); try (OutputStream outputStream = response.getOutputStream()) { for (long i = 0; i < fileSize; i += chunkSize) { // 计算当前块的合法结束偏移量,避免超出文件范围 long endOffset = Math.min(i + chunkSize - 1, fileSize - 1); blob.downloadRange(i, endOffset, outputStream); outputStream.flush(); // 实时输出,减少缓冲区堆积 } } } else { response.setStatus(HttpServletResponse.SC_NOT_FOUND); } } catch (StorageException | IOException e) { throw new GenericException(e, BusinesErrorEnum.AZURE_BLOB_STORAGE_EXCEPTION); } }
方案2:修正原下载逻辑的参数错误(仅缓解,仍有OOM风险)
如果业务必须将文件加载到内存返回,先修正downloadRange的参数错误,避免重复下载导致内存翻倍:
public ByteArrayOutputStream downloadFileByChunks(String blobName) { long file_size = 0; long chunkSize = 10 * 1024 * 1024; CloudBlockBlob blob = connectAndgetCloudBlockBlob(blobName); ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { if (blob.exists()) { blob.downloadAttributes(); file_size = blob.getProperties().getLength(); for (long i = 0; i < file_size; i += chunkSize) { // 正确计算结束偏移:当前块最后一个字节的索引 long endOffset = Math.min(i + chunkSize - 1, file_size - 1); blob.downloadRange(i, endOffset, baos); } } } catch (StorageException e) { throw new GenericException(e, BusinesErrorEnum.AZURE_BLOB_STORAGE_EXCEPTION); } return baos; }
注意:此方案仍会将全量文件加载到内存,仅适用于文件大小不超过JVM堆内存的场景。
方案3:临时文件中转(内存不足时的备选)
若无法直接写入响应流,可将文件下载到磁盘临时文件,再通过临时文件输出,避免占用过多内存:
public void getFileBytes(@RequestBody ZeusRequestVO<String> request, HttpServletResponse response) throws IOException { File tempFile = documentService.downloadFileToTemp(request.getPayload()); try { Files.copy(tempFile.toPath(), response.getOutputStream()); } finally { tempFile.delete(); // 清理临时文件 } } public File downloadFileToTemp(String blobName) { long chunkSize = 10 * 1024 * 1024; CloudBlockBlob blob = connectAndgetCloudBlockBlob(blobName); File tempFile = File.createTempFile("azure_blob_", ".tmp"); try (FileOutputStream fos = new FileOutputStream(tempFile)) { if (blob.exists()) { blob.downloadAttributes(); long fileSize = blob.getProperties().getLength(); for (long i = 0; i < fileSize; i += chunkSize) { long endOffset = Math.min(i + chunkSize - 1, fileSize - 1); blob.downloadRange(i, endOffset, fos); fos.flush(); } } } catch (Exception e) { tempFile.delete(); throw new GenericException(e, BusinesErrorEnum.AZURE_BLOB_STORAGE_EXCEPTION); } return tempFile; }
内容的提问来源于stack exchange,提问作者Gonzalo León
相关产品推荐
相关产品推荐

