15

我需要从我的 Web 应用程序执行大量文件下载。

这显然是一个长期运行的操作(它将每年使用一次[-per-customer]),所以时间不是问题(除非它遇到一些超时,但我可以通过创建某种形式的keepalive心跳)。我知道如何创建一个隐藏的iframe并使用它content-disposition: attachment来尝试下载文件而不是在浏览器中打开它,以及如何实例化客户端-服务器通信以绘制进度表;

下载的实际大小(和文件数量)未知,但为简单起见,我们可以虚拟地将其视为 1GB,由 100 个文件组成,每个文件 10MB。

由于这应该是一键式操作,所以我的第一个想法是将所有文件分组,同时从数据库中读取它们,在动态生成的 ZIP 中,然后要求用户保存 ZIP。

问题是:从 WebApp 中的多个小字节数组创建一个巨大的存档时,最佳实践是什么,已知的缺点和陷阱是什么?

可以随机分为:

  • 应该在物理临时文件中转换每个字节数组,还是可以将它们添加到内存中的 ZIP 中?
  • 如果是,我知道我将不得不处理可能的名称相等(它们可以在数据库的不同记录中具有相同的名称,但不能在同一个文件系统或 ZIP 中):是否还有其他可能的问题介意(假设文件系统总是有足够的物理空间)?
  • 因为我不能依靠有足够的 RAM 来在内存中执行整个操作,所以我猜应该创建 ZIP 并将其提供给文件系统,然后再发送给用户;有什么不同的方法(例如使用websocket),比如询问用户将文件保存在哪里,然后开始从服务器到客户端的持续数据流(我猜是科幻小说)?
  • 任何其他相关的已知问题或您想到的最佳实践将不胜感激。
4

3 回答 3

15

通过将每个 BLOB 从数据库直接流式传输到客户端的文件系统而创建的完全动态 ZIP 文件的启动示例。

使用具有以下性能的大量档案进行测试:

  • 服务器磁盘空间成本:0 兆字节
  • 服务器RAM成本: ~ xx MB。Runtime.getRuntime().freeMemory()内存消耗是不可测试的(或者至少我不知道如何正确地做到这一点),因为我在循环之前、期间和之后多次运行相同的例程(通过使用)得到了不同的、显然是随机的结果)。但是,内存消耗比使用 byte[] 低,这就足够了。


FileStreamDto.java使用InputStream而不是byte[]

public class FileStreamDto implements Serializable {
    @Getter @Setter private String filename;
    @Getter @Setter private InputStream inputStream; 
}


Java Servlet(或 Struts2 Action)

/* Read the amount of data to be streamed from Database to File System,
   summing the size of all Oracle's BLOB, PostgreSQL's ABYTE etc: 
   SELECT sum(length(my_blob_field)) FROM my_table WHERE my_conditions
*/          
Long overallSize = getMyService().precalculateZipSize();

// Tell the browser is a ZIP
response.setContentType("application/zip"); 
// Tell the browser the filename, and that it needs to be downloaded instead of opened
response.addHeader("Content-Disposition", "attachment; filename=\"myArchive.zip\"");        
// Tell the browser the overall size, so it can show a realistic progressbar
response.setHeader("Content-Length", String.valueOf(overallSize));      

ServletOutputStream sos = response.getOutputStream();       
ZipOutputStream zos = new ZipOutputStream(sos);

// Set-up a list of filenames to prevent duplicate entries
HashSet<String> entries = new HashSet<String>();

/* Read all the ID from the interested records in the database, 
   to query them later for the streams: 
   SELECT my_id FROM my_table WHERE my_conditions */           
List<Long> allId = getMyService().loadAllId();

for (Long currentId : allId){
    /* Load the record relative to the current ID:         
       SELECT my_filename, my_blob_field FROM my_table WHERE my_id = :currentId            
       Use resultset.getBinaryStream("my_blob_field") while mapping the BLOB column */
    FileStreamDto fileStream = getMyService().loadFileStream(currentId);

    // Create a zipEntry with a non-duplicate filename, and add it to the ZipOutputStream
    ZipEntry zipEntry = new ZipEntry(getUniqueFileName(entries,fileStream.getFilename()));
    zos.putNextEntry(zipEntry);

    // Use Apache Commons to transfer the InputStream from the DB to the OutputStream
    // on the File System; at this moment, your file is ALREADY being downloaded and growing
    IOUtils.copy(fileStream.getInputStream(), zos);

    zos.flush();
    zos.closeEntry();

    fileStream.getInputStream().close();                    
}

zos.close();
sos.close();    


处理重复条目的辅助方法

private String getUniqueFileName(HashSet<String> entries, String completeFileName){                         
    if (entries.contains(completeFileName)){                                                
        int extPos = completeFileName.lastIndexOf('.');
        String extension = extPos>0 ? completeFileName.substring(extPos) : "";          
        String partialFileName = extension.length()==0 ? completeFileName : completeFileName.substring(0,extPos);
        int x=1;
        while (entries.contains(completeFileName = partialFileName + "(" + x + ")" + extension))
            x++;
    } 
    entries.add(completeFileName);
    return completeFileName;
}



非常感谢@prunge给了我直接流的想法。

于 2013-05-20T16:49:43.370 回答
9

对于无法立即放入内存的大型内容,将内容从数据库流式传输到响应。

这种事情其实很简单。您不需要 AJAX 或 websockets,可以通过用户单击的简单链接流式传输大文件下载。现代浏览器有不错的下载管理器和自己的进度条——为什么要重新发明轮子?

如果为此从头开始编写 servlet,请访问数据库 BLOB,获取其输入流并将内容复制到 HTTP 响应输出流。如果你有 Apache Commons IO 库,你可以使用IOUtils.copy(),否则你可以自己做。

可以使用ZipOutputStream即时创建 ZIP 文件。在响应输出流上创建其中一个(来自 servlet 或框架提供的任何内容),然后从数据库中获取每个 BLOB,putNextEntry()首先使用,然后如前所述流式传输每个 BLOB。

潜在的陷阱/问题:

  • 根据下载大小和网络速度,请求可能需要很长时间才能完成。防火墙等可能会阻碍这一点并提前终止请求。
  • 希望您的用户在请求这些文件时位于一个不错的公司网络上。在远程/躲避/移动连接上情况会更糟(如果在下载 1.9G 或 2.0G 后掉线,用户必须重新开始)。
  • 它会给您的服务器带来一些负担,尤其是压缩巨大的 ZIP 文件。ZipOutputStream如果这是一个问题,则在创建时可能值得关闭/关闭压缩。
  • 超过 2GB(或者是 4GB)的 ZIP 文件可能与某些 ZIP 程序有问题。我认为最新的 Java 7 使用 ZIP64 扩展,所以这个版本的 Java 将正确地编写巨大的 ZIP,但是客户端会有支持大 zip 文件的程序吗?我以前肯定遇到过这些问题,尤其是在旧的 Solaris 服务器上
于 2013-05-17T04:06:17.963 回答
2

可能您想同时尝试多个下载。我在这里找到了与此相关的讨论-Java多线程文件下载性能

希望这可以帮助。

于 2013-05-17T02:54:20.313 回答