在当今的互联网应用中,缓存作为一种提高系统性能的关键技术,扮演着至关重要的角色。无论是日常浏览网页、使用 APP,还是企业级应用的后台处理,缓存的存在无处不在。那么,什么是缓存?我们应该如何有效地利用缓存来优化系统性能呢?
什么是缓存?
从本质上来看,缓存是将数据暂时存储在比原始数据源更快的存储介质中,以便快速访问。其主要目的是减少数据访问的延迟,提高系统的性能,从而提升用户体验。
从一个 Web 请求的链路来看,主要有以下几种类型的缓存:
-
浏览器缓存:浏览器本地的缓存,包括内存缓存和磁盘缓存。可以通过 HTTP 响应头控制缓存策略,如 Cache-Control、Expires 等。
-
DNS 缓存:本地机器或 DNS 服务器上对域名解析结果的缓存,可以加快后续对同一域名的访问速度。
-
CDN 缓存:CDN 的边缘节点上的缓存,可以让用户从距离最近的节点获取资源,减轻服务器压力,提高响应速度。
-
Web 服务器缓存:如 Nginx、Apache 等 Web 服务器自带的缓存功能,或者专门的缓存服务器如 Varnish 等。对请求进行缓存,可减轻后端服务器的负载。
-
应用层缓存:在应用程序中自己实现的缓存,如使用 Redis、Memcached 等内存型数据库对数据进行缓存,或者在代码中实现对特定数据的缓存。
-
数据库缓存:数据库本身的查询缓存,如 MySQL 的 Query Cache。会对 SELECT 语句及其结果进行缓存。
-
操作系统缓存:操作系统级别的文件系统缓存,如 Linux 的 Page Cache。可以加快对磁盘上同一文件的重复读取速度。
这些缓存按照其位置和类型,可以分为客户端缓存、网络缓存和服务端缓存。合理地利用各种缓存,可以显著提升 Web 应用的性能和用户体验。同时也要注意缓存的更新策略,以免数据不一致问题的出现。
1 客户端缓存
客户端缓存是指存储在客户端本地的缓存数据,主要是浏览器缓存。浏览器缓存是最常见的客户端缓存形式,它可以显著减少网络传输,加快页面加载速度,提升用户体验。
浏览器缓存主要包括以下两种类型:
- 内存缓存:
-
内存缓存是指存储在内存中的缓存数据,读取速度非常快。
-
但是内存缓存的生命周期较短,会在浏览器关闭时被清除。
-
内存缓存一般用于存储当前页面中已经下载的资源,如页面文档、图片、脚本、样式表等。
- 磁盘缓存:
-
磁盘缓存是指存储在磁盘上的缓存数据,读取速度相对内存缓存慢一些,但是容量更大。
-
磁盘缓存的生命周期更长,可以在浏览器关闭后继续保留。
-
磁盘缓存主要用于存储那些可能在将来的请求中重复使用的资源,如静态图片、脚本、样式表等。
浏览器缓存的工作原理涉及到两个重要的概念:强缓存和协商缓存。
- 强缓存:
-
当资源在缓存有效期内时,浏览器会直接从缓存中读取,不会向服务器发送请求。
-
强缓存由 HTTP 响应头中的 Cache-Control 或 Expires 字段控制。都是表示资源的缓存有效时间。
-
Expires 是 HTTP 1.0 的规范,值是一个 GMT 格式的时间点字符串。缺点是失效时间是一个绝对时间,服务器时间与客户端时间偏差较大时会导致缓存混乱。
-
Cache-Control 是 HTTP 1.1 的规范,一般常用该字段的 max-age 值来进行判断,它是一个相对时间。
-
常见的应用场景有静态资源缓存,如 CSS、JavaScript、图片等,可以设置较长的缓存时间。
- 协商缓存:
-
协商缓存是由服务器来确定缓存资源是否可用,协商缓存会向服务器发送请求,询问资源是否有更新。
-
协商缓存由 HTTP 响应头中的 Last-Modified/If-Modified-Since 或 ETag/If-None-Match 字段控制。
-
服务器根据资源的最后修改时间或内容生成的唯一标识(ETag)来判断资源是否有更新。
-
如果资源没有更新,服务器会返回 304 状态码,告诉浏览器可以直接从缓存中读取。
-
常见的应用场景有动态内容缓存,如新闻、博客文章等,以及 API 响应缓存。
在以上提到的这些缓存控制标签中,优先级:Cache-Control > Expires > ETag > Last-Modified
浏览器在处理缓存时,会优先执行强缓存策略,如果强缓存失效,则执行协商缓存策略。通过合理设置 HTTP 缓存头部,可以有效利用浏览器缓存,减少不必要的网络传输,提升 Web 应用的性能。
这里想多聊一点关于 stale-while-revalidate 和 stale-if-error。它们都是 HTTP Cache-Control 响应头的扩展指令,用于控制浏览器如何处理陈旧的缓存资源,是两个略小众的指令。
1. stale-while-revalidate
语法:Cache-Control: max-age=, stale-while-revalidate=
stale-while-revalidate 指令用于指定在缓存过期后,允许客户端在异步 revalidate 的同时,继续使用陈旧的缓存资源的最长时间。
工作原理:
-
在 max-age 时间内,浏览器直接从缓存中获取资源,不会发送请求。
-
当缓存过期后,在 stale-while-revalidate 指定的时间内,浏览器会发送请求到服务器进行revalidate,但同时会立即返回陈旧的缓存资源给客户端使用。
-
如果 revalidate 成功(即服务器返回304 Not Modified),则缓存更新,并且下次请求会直接从缓存中获取。
-
如果 revalidate 失败(即服务器返回 200 或其他状态码),则缓存更新为新的响应内容,并且下次请求会直接从缓存中获取新内容。
使用场景:
-
适用于更新不太频繁,但又希望用户总是能看到最新内容的资源,如 CSS、JavaScript 等。
-
提高了用户体验,避免因等待revalidate而延迟页面呈现。
示例:Cache-Control: max-age=600, stale-while-revalidate=30
说明:资源可以在 600 秒内直接从缓存中获取,在接下来的30秒内,虽然缓存已过期,但浏览器仍可以显示陈旧的缓存资源,同时在后台进行异步revalidate。
2. stale-if-error
语法:Cache-Control: max-age=, stale-if-error=
stale-if-error 指令用于指定在发生错误(如网络错误、服务器错误等)时,允许客户端使用陈旧的缓存资源的最长时间。
工作原理:
-
在 max-age 时间内,浏览器直接从缓存中获取资源,不会发送请求。
-
当缓存过期后,浏览器会发送请求到服务器获取最新资源。
-
如果请求过程中发生错误,并且在 stale-if-error 指定的时间内,浏览器会返回陈旧的缓存资源给客户端使用。
-
如果请求成功,则缓存更新,并且下次请求会直接从缓存中获取。
使用场景:
-
适用于更新频率较高,但又希望在请求出错时能显示旧内容,而不是错误页面的资源,如用户生成的内容、新闻文章等。
-
提高了用户体验,避免因请求错误而显示错误页面。
示例:Cache-Control: max-age=600, stale-if-error=1200
说明:资源可以在 600 秒内直接从缓存中获取,在接下来的 1200 秒内,如果请求发生错误,浏览器仍可以显示陈旧的缓存资源。
这两个指令可以单独使用,也可以组合使用,以实现更灵活的缓存控制策略。它们的目的都是在一定条件下允许使用过期的缓存,以提高性能和用户体验,同时还能保证一定的数据更新。
需要注意的是,这两个指令都是 HTTP Cache-Control 响应头的扩展,并非所有浏览器都支持。目前,Chrome、Firefox 等主流浏览器已经实现了对这两个指令的支持。在实际应用中,还需要进行充分的测试和评估,以确保缓存策略的有效性和可靠性。
并且尽管浏览器已经支持了,但是服务器也需要正确地设置响应头才能生效。
2 服务器缓存
服务器缓存是指将数据临时存储在服务器的内存或磁盘上,以便后续的请求可以直接从缓存中获取数据,而不必每次都重新生成或计算数据。服务器缓存的目的是提高服务器的响应速度,减少数据处理的开销,从而提升整个系统的性能和吞吐量。
服务器缓存可以分为多个层次和类型,包括:
1.Web 服务器缓存:在 Web 服务器上配置的缓存机制,如:
-
Nginx 的 FastCGI 缓存、Proxy 缓存等。
-
Apache 的 mod_cache 模块。
- 应用层缓存:在应用程序中实现的缓存机制,通常使用内存缓存技术,如:
-
对象缓存:将频繁访问的数据对象缓存在内存中,如 Java 的 Guava Cache 等。
-
查询缓存:将数据库查询的结果缓存起来,下次相同的查询可以直接从缓存中获取,如 Hibernate Second Level Cache。
-
页面缓存(Page Caching):将动态生成的网页内容缓存起来,下次请求可以直接返回缓存的页面。
-
数据库缓存:在数据库系统中实现的缓存机制,如 MySQL Query Cache
-
分布式缓存:将数据缓存在独立的分布式缓存服务器上,供多个应用服务器共享使用,如常用的 Redis。
这些服务器缓存的类型可以独立使用,也可以组合使用,形成多级缓存架构。不同类型的服务器缓存在系统架构中的位置不同,缓存的数据类型和粒度也不同,但它们的共同目标都是提高服务器的性能,加快数据访问速度。
在实际应用中,需要根据具体的业务场景和性能瓶颈,选择适当的服务器缓存策略。这包括合理设计缓存的粒度、缓存的过期策略、缓存的更新机制、缓存的容量规划等。同时也要注意缓存的一致性问题,确保缓存的数据与原始数据源保持同步,避免出现脏读或数据不一致的问题。
服务器缓存是提高系统性能的重要手段之一。合理利用服务器缓存,可以显著减少数据处理的开销,提高服务器的响应速度,从而提升整个系统的吞吐量和并发处理能力。但同时也要权衡缓存的成本和收益,避免过度使用缓存而带来的内存开销和维护成本。
3 缓存实现策略或模式
从应用程序角度来看,缓存的实现模式主要涉及如何在应用程序中使用和集成缓存,以提高数据访问的效率和性能。
常用的缓存模式有以下几种:
- 旁路缓存(Cache Aside):
-
应用程序先查询缓存,如果缓存中有数据,则直接返回。
-
如果缓存中没有数据,则从数据源(如数据库)查询,并将查询结果存入缓存,然后返回。
-
更新数据时,先更新数据源,然后再删除缓存,保证数据一致性。
-
优点:简单易懂,应用程序可以控制缓存的生命周期。
-
缺点:可能出现缓存和数据源不一致的情况,需要合理设计缓存的过期策略和并发策略。
- 读写穿透(Read/Write Through):
-
应用程序只与缓存交互,不直接访问数据源。
-
缓存服务负责与数据源交互,并将数据在缓存和数据源之间同步。
-
读取数据时,缓存服务先查询缓存,如果缓存中没有数据,则从数据源查询,并将结果存入缓存,然后返回。
-
更新数据时,缓存服务先更新缓存,然后再更新数据源,保证数据一致性。
-
优点:应用程序无需关心缓存和数据源的同步,缓存服务保证了数据一致性。
-
缺点:缓存服务需要实现与数据源的交互,增加了复杂性;写操作的性能可能较低,因为需要同时更新缓存和数据源。
- 异步写入(Write Behind):
-
应用程序只与缓存交互,不直接访问数据源。应用程序更新缓存,缓存服务在后台异步地将数据更新到数据源。
-
写入数据时,只更新缓存,并将更新操作加入队列。
-
异步线程或进程从队列中取出更新操作,并批量写入数据源。
-
优点:写入操作的性能很高,因为只需要更新缓存;数据源的写入可以批量进行,提高效率。
-
缺点:缓存和数据源之间可能存在一定的延迟,需要合理设计队列的大小和刷新策略;如果缓存服务崩溃,可能导致数据丢失,因此需要着重考虑缓存服务的可靠性
- 预刷新(Refresh Ahead):
-
定期或在特定条件下,异步地从数据源加载数据到缓存中。
-
可以通过定时任务、事件触发或者智能预测等方式来触发预刷新操作。
-
优点:避免了缓存 miss 导致的性能下降,提高了读取操作的响应速度。
-
缺点:需要额外的计算资源和存储空间来执行预刷新操作;如果预刷新的数据无法准确预测,可能会浪费资源。
在业务场景中我们往往不局限于只使用某一种策略,可能会是使用以上多种模式,,根据不同的数据特点和访问模式,采用不同的策略。例如,对于读多写少的数据,可以使用「旁路缓存」或「读写穿透」策略;对于写多读少的数据,可以使用「异步写入」策略。
计算机领域有个名言警句:
There are only two hard problems in Computer Science: cache invalidation, and naming things.(计算机领域只有有两大难题,「让缓存失效」和「给东西命名」)
接下来我们聊一下缓存过期策略。
4 缓存过期策略
缓存过期策略是指确定缓存数据何时失效并从缓存中移除的规则。合理的缓存过期策略可以帮助控制缓存的数据鲜度,并优化缓存的空间利用率。以下是一些常见的缓存过期策略:
- TTL 策略
-
定义:TTL(Time To Live)策略为每个缓存条目设置一个固定的生存时间。当数据存入缓存时,指定一个过期时间。到达过期时间后,缓存条目将被自动移除,即使它在这段时间内没有被访问过。
-
优点:TTL 策略实现简单,易于配置,适用于对数据新鲜度有严格要求的场景。
-
缺点:容易导致缓存抖动,即频繁的缓存失效和重新加载,可能增加系统负载。
-
场景:TTL策略适用于那些数据变化频繁且需要确保数据新鲜度的场景。例如,实时新闻数据、股票价格、天气预报等。
- LRU 策略
-
定义:LRU(Least Recently Used)策略根据使用频率来决定缓存条目的去留。当缓存空间不足时,会移除最近最少使用的条目,以腾出空间存储新的数据。
-
优点:LRU 策略能有效利用缓存空间,适用于访问模式有局部性的场景。
-
缺点:实现较复杂,可能会增加缓存管理的开销,特别是在高并发环境下。
-
场景:LRU 策略广泛应用于需要频繁访问的大型数据集,例如 Web 服务器的页面缓存、数据库查询缓存等。
- LFU 策略
-
定义:LFU(Least Frequently Used)策略根据访问频率来决定缓存条目的去留。当缓存空间不足时,会移除访问频率最低的条目,以腾出空间存储新的数据。
-
优点:LFU 策略适用于访问频率有明显差异的场景,能有效缓存高频访问的数据。
-
缺点:实现复杂度较高,频繁更新访问计数可能会增加系统负载。
-
场景:LFU 策略适用于用户访问行为具有明显模式的应用,如推荐系统、热点新闻或视频的缓存。
- FIFO 策略
-
定义:FIFO(First In, First Out)策略按照条目加入缓存的顺序来决定去留。最早加入缓存的条目最先被移除,不考虑条目的使用频率或时间。
-
优点:FIFO 策略实现简单,适用于数据访问模式较为均匀的场景。
-
缺点:可能会导致热门数据被过早移除,不适合需要缓存热点数据的场景。
-
场景:FIFO 策略适用于缓存数据生命周期较短且频繁更新的场景,例如某些实时数据流的缓冲。
- ARC 策略
-
定义:ARC(Adaptive Replacement Cache)策略结合了 LRU 和 LFU 的优点,通过动态调整缓存策略来适应不同的访问模式。ARC 维护两个 LRU 列表,一个用于最近访问过的数据,另一个用于以前访问过的数据,并根据缓存命中情况在这两个列表之间调整权重。
-
优点:ARC策略能够自适应调整缓存替换策略,既考虑了最近使用的频率,又考虑了访问频率,从而提高缓存命中率。
-
缺点:实现复杂,需要维护多个列表和动态调整算法,可能增加缓存管理的开销。
-
场景:ARC 策略适用于访问模式多变且无法预知的场景,如混合型工作负载的缓存管理。它在需要高效利用缓存空间且保持高命中率的系统中表现尤为出色,例如数据库管理系统、操作系统的页面缓存等。
- SLRU 策略
-
定义:SLRU(Segmented Least Recently Used)是一种缓存替换算法,它是LRU(Least Recently Used)算法的一个变体。SLRU(Segmented LRU)策略将缓存分为两个段:一个是保护段(probation segment),另一个是优选段(protected segment)。新加入的条目首先进入保护段,如果条目在保护段中被再次访问,则移动到优选段。优选段中的条目如果再次被访问,则保持在优选段,否则会被移除。
-
优点:SLRU 策略通过分段管理缓存条目,既能保留最近访问的数据,也能保护多次访问的数据,提高缓存命中率。
-
缺点:实现复杂度较高,需要维护多个段和管理策略,可能增加系统开销。
-
场景:SLRU 策略适用于需要平衡最近访问和频繁访问需求的场景,例如Web浏览器的缓存管理、文件系统的缓存管理等。
5 一些注意事项
在应用开发中使用缓存虽然可以显著提升系统性能和用户体验,但如果不当使用,也可能导致一些问题和陷阱。
-
缓存与数据源的一致性: 缓存数据和原始数据源之间的不一致是常见的问题之一。当数据被更新时,如果缓存没有同步更新,就会出现旧数据被重复使用的情况。
-
缓存穿透:缓存穿透指查询不存在的数据时,请求直接穿过缓存访问数据库,如果这种请求非常频繁,将严重影响数据库的性能。
-
缓存雪崩:缓存雪崩是指在缓存层面发生大规模的缓存失效,导致所有的请求都去打数据库,可能会因此使数据库压力过大而崩溃。
-
缓存预热:系统启动后缓存是空的,直接面对大流量可能会导致短时间内数据库请求量激增。
-
脏读问题:在分布式环境中,如果多个节点同时对缓存进行读写操作,可能会读到过期或不一致的数据。
6 小结
缓存不是解决性能问题的银弹,而是一种在适当的场景下能够显著提升系统响应速度和处理能力的工具。在实际应用中,缓存的引入需要仔细考虑其适用性、一致性问题、资源管理和安全性等多方面因素。
缓存最适合用于读操作远多于写操作的数据,以及那些数据更新不频繁、但需要快速访问的场景。然而,对于高度动态的数据,缓存可能不仅无法提供预期的性能提升,反而因为频繁的缓存更新和失效处理增加了额外的复杂性和开销。
在使用缓存的过程中,数据一致性是引入缓存时必须面对的一个挑战。无论是在单体应用还是分布式系统中,如何保证缓存中的数据与数据库中的数据保持一致,是设计缓存策略时必须仔细考虑的问题。不恰当的缓存策略可能导致数据过时或错误,影响业务的正确性。
并且,缓存的管理和维护也是一项不可忽视的任务。正确的缓存大小、适宜的过期策略、有效的内存管理等都是确保缓存系统高效运作的关键。缓存过大可能会消耗过多的内存资源,影响系统的稳定性;缓存过小则可能无法发挥缓存的性能优势。
缓存是一种强大的优化工具,但它并不适合所有情况。只有在正确的场景下,配合合适的策略和周到的管理,才能发挥出缓存的最大效能,帮助提升应用的性能和用户体验。