缓存是所有工程师都必须知道的非常有用的软件组件。它是一个横向组件,适用于所有技术领域和架构层,如操作系统、数据平台、后端、前端和其他组件。在本文中,我们将描述什么是缓存,并针对前端和客户端解释具体用例。
缓存可以以基本方式定义为数据消费者和数据生产者之间的中间存储器,用于存储和提供将被相同/不同消费者多次访问的数据。除了提高性能外,它在用户可用性方面对数据消费者来说是一个透明层。 通常,数据生产者提供的数据的可重用性是利用缓存优势的关键。性能是使用内存数据库等缓存系统来提供具有低延迟、高吞吐量和并发性的高性能解决方案的另一个原因。
例如,每天有多少人查询天气,他们会重复查询多少次?假设纽约有 1,000 人查询天气,其中 50% 的人每天重复相同的查询两次。在这种情况下,如果我们可以将第一个查询存储在尽可能靠近用户设备的位置,我们将获得两个好处:增加用户体验,因为数据提供得更快,并减少对数据生产者/服务器端的查询次数。输出是更好的用户体验和支持更多并发用户使用该平台的解决方案。
在高层次上,我们可以以互补的方式应用两种缓存策略:
Client/Consumer Side:缓存的数据存储在消费者或用户端,当我们谈论 Web 解决方案时,通常在浏览器的内存中(也称为私有缓存)。
服务器/生产者端:缓存的数据存储在数据生产者架构的组件中。
与任何其他解决方案一样,缓存具有一系列优势,我们将对其进行总结:
靠近客户端的组件更具可扩展性和更便宜,因为三个主要原因:
缓存的最大挑战是数据一致性和数据新鲜度, 这意味着数据如何在整个组织内同步和更新。根据用例,我们会有或多或少的要求限制,因为它与缓存图像相比与库存或销售行为有很大不同。
谈到客户端缓存,我们可以有不同类型的缓存,我们将在本文中稍微分析一下:
它在浏览器中缓存对任何资源(css、html、图像、视频等)的 HTTP 请求,并从前端管理所有与存储、过期、验证、获取等相关的内容。应用程序的观点几乎是透明的,因为它以常规方式发出请求并且浏览 器执行所有“魔术”。
控制缓存的方法是使用 HTTP Headers,在服务器端,它向 HTTP 响应添加特定于缓存的标头,例如:“Expires: Tue, 30 Jul 2023 05:30:22 GMT”,然后是浏览器知道这个资源可以被缓存,下次客户端(应用程序)请求同一个资源时,如果请求时间在过期日期之前,请求将不会完成,浏览器将返回该资源的本地副本。
它允许您设置响应的伪装方式,因为相同的 URL 可以生成不同的响应(并且它们的缓存应该以不同的方式处理)。例如,在返回一些数据的 API 端点中,我们可以使用请求标头来Content-type指定我们是否需要 JSON 或 CSV 等格式的响应。因此,缓存应根据请求标头与响应一起存储。为此,服务器应该设置响应标头Vary: Accept-Language,让浏览器知道缓存取决于该值。有很多不同的标头来控制缓存流和行为,但深入研究它不是本文的目标。它可能会在另一篇文章中解决。
正如我们之前提到的,这种缓存类型需要服务器设置资源过期、验证等。所以这不是一种纯粹的前端缓存方法或类型,但它是缓存前端应用程序使用的资源的最简单方法之一,它是我们将在下面提到的另一种方式的补充。
与这种缓存类型相关,由于它是中间缓存,我们甚至可以将其委托给客户端和服务器之间的“一块”;例如,CDN、反向代理(例如 Varnish)等。
它与 HTTP 缓存方法非常相似,但在这种情况下,我们控制哪些请求被存储或从缓存中提取。我们必须管理缓存过期(这并不容易,因为这些缓存被认为“永远存在”)。即使这些 API 在窗口上下文中可用,也非常适合它们在 worker 上下文中的使用。
该缓存非常适合用于离线应用程序。在第一次请求时,我们可以获取并缓存它需要的所有资源(图像、CSS、JS 等),从而允许应用程序离线工作。它在移动应用程序中非常有用,例如,除了天气数据之外,我们的 GPS 系统还可以使用地图。这使我们即使没有连接到服务器也能获得远足路线的所有信息。
它如何在窗口上下文中工作的一个示例:
const url = ‘
https://catfact.ninja/breeds’caches.open('v1').then((cache) => {
cache.match((url).then((res) => {
if (res) {
console.log('it is in cache')
console.log(res.json())
} else {
console.log('it is NOT in cache')
fetch(url) .then(res => {
cache.put('test', res.clone())
})
}
})
})
在某些情况下,我们 需要更多地控制缓存数据和失效(不仅仅是过期)。缓存失效不仅仅是检查max-age缓存条目。
想象一下我们上面提到的天气应用程序。该应用程序允许用户更新天气以反映某个地方的真实天气。该应用程序需要针对每个城市执行请求并将温度值从华氏度转换为摄氏度(这是一个简单的示例:在其他用例中计算成本可能更高)。
为了避免向服务器做请求(即使它被缓存),我们可以一次做所有的请求,把所有的数据放在一个我们方便的数据结构中,并存储在,例如在浏览器的IndexedDB中,在LocalStorage、SessionStorage 甚至在内存中(不推荐)。下次我们要显示数据时,我们可以从缓存中获取它,而不仅仅是资源数据(甚至是我们所做的计算),节省网络和计算时间。
我们可以通过在API后面加上发布时间来控制缓存的过期,也可以控制缓存的失效。现在想象一下,用户在其浏览器中添加了一只新猫。我们可以让缓存失效,下次再做请求和计算,或者更进一步,用新数据更新我们的本地缓存。或者,另一个用户可以更改该值,服务器将发送一个事件以将更改通知给所有客户端。例如,使用WebSockets,我们的前端应用程序可以听到这些事件并使缓存无效或只更新缓存。
这种缓存需要我们这边的工作来检查缓存并处理可能使其失效或更新的事件等,但非常适合六边形架构,其中使用端口适配器(存储库)从 API 使用数据可以听到域事件以对更改做出反应并使某些缓存无效或更新。
这不是缓存通用解决方案。我们需要考虑它是否适合我们的用例,因为它需要在前端应用程序端工作以使缓存无效或发出和处理数据更改事件。在大多数情况下,HTTP 缓存就足够了。
拥有缓存解决方案和良好的策略应该是任何软件架构中必须的, 但我们的解决方案将是不完整的,并且可能没有优化。 缓存是我们最好的朋友,主要是在高性能场景中。看起来技术失效缓存过程是挑战,但 最大的挑战是了解业务场景和用例,以确定在数据新鲜度和一致性方面的要求,使我们能够设计和选择最佳策略。