在 WebApp 中创建和下载巨大 ZIP(来自多个 BLOB)的最佳实践
Posted
技术标签:
【中文标题】在 WebApp 中创建和下载巨大 ZIP(来自多个 BLOB)的最佳实践【英文标题】:Best Practices to Create and Download a huge ZIP (from several BLOBs) in a WebApp 【发布时间】:2013-05-11 05:24:26 【问题描述】:我需要从我的 Web 应用程序执行大量文件下载。
这显然是一个长期运行的动作(它将被使用每年一次[-per-customer]),所以时间不是问题(除非它遇到一些超时,但我可以通过创建某种形式的 keepalive 心跳来处理它)。我知道如何创建一个隐藏的 iframe 并将其与content-disposition: attachment
一起使用以尝试下载文件而不是在浏览器中打开它,以及如何实例化客户端-服务器通信以绘制进度表;
下载的实际大小(和文件数量)未知,但为简单起见,我们可以将其虚拟视为 1GB,由 100 个文件组成,每个文件 10MB。
由于这应该是一键式操作,我的第一个想法是将所有文件分组,同时从数据库中读取它们,在动态生成的 ZIP 中,然后要求用户保存 ZIP。
问题是:从 WebApp 中的多个小字节数组创建大型存档的最佳实践是什么,已知的缺点和陷阱是什么?
可以随机拆分成:
应该将每个字节数组转换为物理临时文件,还是可以将它们添加到内存中的 ZIP 中? 如果是,我知道我必须处理可能的名称相等(它们可以在数据库的不同记录中具有相同的名称,但不能在同一个文件系统或 ZIP 中):是否有任何其他可能的问题想到了什么(假设文件系统总是有足够的物理空间)? 因为我不能依靠有足够的 RAM 来在内存中执行整个操作,所以我猜应该创建 ZIP 并将其提供给文件系统,然后再发送给用户;有什么不同的方法(例如使用 websocket),比如询问用户将文件保存在哪里,然后开始从服务器到客户端的持续数据流(Sci- Fi 我猜)? 如果您想到任何其他相关的已知问题或最佳实践,我们将不胜感激。【问题讨论】:
一年一次?也许 HTTP 不是做到这一点的最佳方式。根据您将其发送给谁,我会考虑使用 rsync 之类的东西。 我完全同意你的看法。不幸的是,这必须是该 web 应用程序的一个功能(包含敏感数据,我们甚至没有读取生产数据库的权限......希望客户永远不会遇到任何问题) 我们无法讨论:/ 您是否在为您的 webapp 使用某个框架?Struts2 + Spring
... 你知道一些特定于框架的东西吗?我省略了它(即使在标签中),因为我认为它不相关
在我去年参与的一个项目中,我们有一些非常相似的东西:用户上传了一个经过处理的文件,最后向用户呈现了一份报告。但有时这需要几个小时,因此无法将结果放入会话中。我们在动作类中有一个对工作线程的静态引用。因此,当用户访问该操作时,我们检查了工作线程的状态,并根据提供的状态 a) 启动选项 b) 进度信息 c) 下载链接。我们使用涡轮机,但我认为这也适用于支柱
【参考方案1】:
通过将每个 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 给了我直接流式传输的想法。
【讨论】:
嗨,我的用例几乎和你一样,只是字节流是通过 http 连接来自远程主机。我发现这会消耗系统而不是 JVM 的大量内存使用。你有同样的问题吗? 不,但我已经评论了你的问题 已经被浏览器管理了...如果有错误,浏览器会告诉你无法完成下载 嗯,这取决于我猜的浏览器实现。我记得当时还在 Web 应用程序中创建了一个进度条,它在会话中轮询由生成 ZIP 的线程提供的值。我想有很多选择,例如队列。 是的,它可能需要来自客户端的自定义监控。但是,仅供参考,使用失败操作终止 servlet 输出流会导致浏览器(chrome/ff/edge)显示下载失败。这不会在 tomcat/undertow 中发生,除了与提交相关的 tomcat 版本 9.0.53 > tomcat.apache.org/tomcat-9.0-doc/changelog.html > github.com/apache/tomcat/commit/…【参考方案2】:对于无法立即放入内存的大型内容,流将内容从数据库传输到响应。
这种事情其实很简单。您不需要 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 服务器上
【讨论】:
来自数据库的流式传输就像一个魅力!重复条目处理程序已完成,唯一剩下的就是双击控件/模式行为和我的特定框架中的错误处理程序(因为我使用了结果为 NONE 的 Struts2 Action 和手动响应编写而不是 Servlet)。仍然感谢您的想法!【参考方案3】:您可能想同时尝试多个下载。我在这里找到了与此相关的讨论 - Java multithreaded file downloading performance
希望这会有所帮助。
【讨论】:
这无疑是一个引人入胜的论点,但我认为它不适合我现在的情况。感谢同样的@InduDevanath以上是关于在 WebApp 中创建和下载巨大 ZIP(来自多个 BLOB)的最佳实践的主要内容,如果未能解决你的问题,请参考以下文章