使用浏览器缓存进行增量更新

7
客户端(一个AngularJS应用程序)从服务器获取相当大的列表。这些列表可能有数百或数千个元素,未经压缩可能会达到几兆字节(某些用户(管理员)获取更多数据)。
我不打算让客户端获取部分结果,因为排序和过滤不应该影响服务器。
压缩效果很好(约为10倍),由于列表不经常更改,“304 NOT MODIFIED”也非常有帮助。但是,缺少另一个重要的优化:
由于列表的典型更改相当小(例如,修改两个元素并添加一个新元素),仅传输更改听起来像是一个好主意。我想知道如何正确地做到这一点。
类似于“GET”“/offer/123/items”的请求应始终返回提供编号123中的所有项目,对吗?可以在此处使用压缩和304,但不能进行增量更新。像“GET”“/offer/123/items?since=1495765733”这样的请求似乎是正确的方式,但是浏览器缓存不会被使用:
- 要么没有任何更改,答案为空(并且缓存它毫无意义) - 要么发生了一些更改,客户端更新其状态,并且永远不再询问自1495765733以来的更改(并且缓存它甚至更没有意义)
显然,当使用“since”查询时,“资源”将不会被缓存(原始查询只使用一次或根本不使用)。
因此,我不能依赖浏览器缓存,我只能使用localStorage或sessionStorage,它们具有一些缺点:
- 它受到几兆字节的限制(浏览器HTTP缓存可能更大且自动处理) - 当达到限制时,我必须实现某些替换策略 - 浏览器缓存存储已经压缩的数据,而我无法获取它们(我必须重新压缩它们) - 对于获取更大列表的用户(管理员),它无法工作,因为即使单个列表可能已超过限制 - 它在注销时被清空(客户的要求)
鉴于存在HTML 5和HTTP 2.0,这非常不令人满意。我错过了什么?
是否可以在增量更新的同时使用浏览器HTTP缓存?

为什么不分两个阶段进行请求呢?首先,使用可选的“since”过滤器请求新的ID,不可缓存。然后,通过将ID作为查询参数包含在内,请求完整的项目列表,包括项目详细信息,以进行完全可缓存的请求。 - Constantin Galbenu
@ConstantinGALBENU 我看不出它有什么帮助。当我包含所有ID时,我会获取整个列表,这是我需要避免的。当我仅包含新ID时,我会得到一个我永远不会再次需要的请求,因为下一次会有不同的新ID。 - maaartinus
你在服务器端使用哪种语言来处理这些请求? - Joe Coder
@JoeCoder Java(嵌入式Jetty Servlet)。 - maaartinus
3个回答

4
我想你可能忽略了一个重要因素:头文件。我认为符合大部分需求的方法是:
- 首先进行常规的GET /offer/123/items请求。 - 然后发送带有“Fetched-At: 1495765733”头文件的后续GET /offer/123/items请求,这表明您的服务器在何时发送了初始请求。
从现在开始,有两种情况:
- 如果没有更改,则可以发送304。 - 如果有变化,请自上次时间戳以来返回新项,并在响应中设置“Cache-Control: no-cache”标头。
这样,您就可以获得增量更新,并且可以缓存最初的大型元素。
然而,仍然存在一个缺点,即缓存仅执行一次,不会缓存更新。你说你的列表不经常更新,所以它可能已经适用于你,但如果你真的想进一步推动这个问题,我可以考虑更多的事情。
当接收到增量更新时,您可以在后台触发另一个没有"Fetched-At"头文件的请求,该请求完全不被应用程序使用,但将用于更新您的HTTP缓存。从性能角度来看,这不会像听起来那么糟糕,因为框架不会使用新数据更新其数据(并潜在地触发重新呈现),唯一值得注意的缺点可能在于网络和内存消耗。在移动设备上可能会有问题,但它似乎并不是一个旨在在这些设备上显示的应用程序。
我不知道你的用例情况,只是随便提一下,你确定分页之类的方法不适用吗?对于普通人来说,处理几百万字节的数据听起来很困难 ;)

1
不错的答案!确实,没有人想看到成千上万行的代码,但客户可以进行筛选和排序,这样人们就能得到他们可以使用的东西。分页也可以起到作用,但客户可以在不生成任何流量的情况下很好地完成它。而且可能更快。 - maaartinus
1
谢谢!我明白了,只要已经考虑过,那就是好事,我只是想确认一下。 - Preview
1
我们是否知道浏览器不会把 Cache-Control: no-cache 视为释放一些内存的时机?如果我被告知不要缓存新版本的内容,我也不会保留旧版本。实际上,sec14.9.1 甚至没有说响应不能被存储。我想我们还需要 no-store。+++ 我会试试它们(我不必支持所有浏览器,所以这是可行的),但这需要一些时间。 - maaartinus
这是一个相当不错的观点,是的,“no-store”可能是更好的选择。我找不到任何关于在接收到不可缓存响应时潜在删除的信息(无论是使用“no-store”还是“no-cache”),对我来说,这两种行为都是有道理的,所以我猜这可能取决于浏览器,就像你说的那样。 - Preview

3
我会完全放弃请求/响应循环,并转向推送模型。具体来说,使用WebSockets技术。
这是金融交易网站上用于提供实时股票行情表的标准技术。以下是一个生产应用程序,演示了WebSockets的强大功能:

https://www.poloniex.com/exchange#btc_eth

WebSocket应用程序有两种状态:全局和用户。上面的链接将显示三个全局数据表。当您登录时,底部会显示两个附加的用户数据表。
这不是HTTP;您不能只是将其放入Java Servlet中。您需要在服务器上运行一个单独的进程,该进程通过TCP进行通信。好消息是,有成熟的解决方案可供选择。基于Java的解决方案具有非常不错的免费许可选项,包括客户端和服务器API(并与Angular2集成),即Lightstreamer。他们还有一个组织良好的演示页面demo page。还有适配器可用于与您的数据源集成。
您可能会犹豫是否放弃现有的servlet方法,但从长远来看,这将减少头痛,并且可以很好地扩展。即使使用设计良好的仅标头请求,HTTP轮询也无法很好地处理频繁更新的大型列表。
----------编辑----------

由于列表更新不频繁,WebSockets 可能过于复杂。根据对此答案的评论提供的更多详细信息,我建议使用基于 DOM 的 AJAX 更新排序器和过滤器,例如 DataTables,它具有一些内置的缓存选项。为了在会话之间重用客户端数据,在前面链接中的 ajax 请求应该被修改以将当前表格数据保存到 localStorage 中,每次 ajax 请求后,当客户端开始新会话时,使用此数据填充表格。这将允许插件管理过滤、排序、缓存和基于浏览器的持久性。


你绕过了我的问题,不过这是一个好答案。使用servlets和websockets是可能的(我从未尝试过,但应该不难)。我在考虑长轮询作为比websockets更少问题(也更少功能强大)的替代方案。+++感谢您提供的链接。由于只有一个类将剩余代码与HTTP隔离开来,因此我没有放弃servlet的任何问题。 - maaartinus
我在绕开你的问题。就像当我回答关于“如何同步两个列表?”的问题时,建议使用哈希映射表一样。 :) - Joe Coder
当然,有时候回避问题是最好的选择。这非常令人印象深刻,但可能有些过头了(我的列表很大,我不想显示陈旧的数据,但更新很少)。我会阅读它并稍后回来,谢谢。 +++ 另外,它并没有解决缓存问题:因为我把排序和过滤留给了客户端,所以它需要加载或缓存几兆字节的数据。 - maaartinus
它解决了不同的问题,即快速传播许多更改到许多客户端的小列表。我的问题是获取一个大列表,它通常与其先前版本略有不同。这来自于我让客户端执行所有排序和过滤的不寻常决定。Lightstreamer的客户端从未存储兆字节的数据,因为它们只接收要显示给用户的数据。它们的数据变化非常快,因此发送和存储超过可以显示的数据是没有意义的。如果我想使用Lightstreamer,我需要做很多改变;这是可行的,但是价格也是一个问题。 - maaartinus
这个方案可行,但是明天我想在今天的状态上继续构建,但今天没有请求完整列表,只有增量更新。所以我可以使用昨天的状态加上今天的100个更改并发送100个新更改。这看起来不太好,因为过几天,我会得到1000+100+100+100...我会很高兴如果我能告诉浏览器将更改合并到原始列表中,就像我发送了整个列表一样。 - maaartinus
显示剩余4条评论

2

我在考虑类似Aperçu的想法,但使用两个请求。这个想法还不完善,请谅解……

  • 客户端请求GET/offer/123/items,可能带有ETagFetched-At头。

服务器会做出回应:

  • 如果没有任何一个头字段或自上次Fetched-At时间戳以来有太多更改,则回传200和完整列表。
  • 如果自那时起什么都没有改变,则回传304
  • 304并带有特殊的Fetch-More头,告诉客户端需要获取更多数据,否则将违反HTTP工作方式。

最后一种情况违反了HTTP的工作方式,但据我所知,这是让浏览器缓存所有我想要缓存的东西的唯一方法。由于整个通信都是加密的,代理无法因违反规范而对我进行惩罚。

客户端通过请求GET/offer/123/items/errata来响应Fetch-Errata。这样,资源就被分成了两个请求。这种分割很丑陋,但是angular $http拦截器可以隐藏应用程序中的这种丑陋。

第二个请求也可以缓存,并且还可以带有Fetched-At头。详情尚不清楚,但一些强有力的魔法使我相信它可以工作。实际上,错误可能本身就是不准确的,但仍然有用,需要一个新的错误……等等。

在HTTP/1.1中,多个请求可能意味着更多的延迟,但由于节省了带宽,有几个请求仍然是有利可图的。服务器可以决定何时停止。

在HTTP/2中,多个请求可以同时发送。服务器可以有效地处理它们,因为它知道它们是一起的。还有一些更神奇的魔法……

我觉得这个想法很奇怪,但很有趣,期待大家的评论。请随意给我投票,但请留下一份解释。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接