为什么需要缓存?
在任何一个前端项目中,访问服务器获取数据都是很常见的事情,如果相同的数据被重复请求了不止一次,那么多余的请求必然会浪费网络带宽,以及延迟浏览器渲染所要处理的内容,从而影响用户的使用体验。如果用户使用的是按量计费的方式访问网络,多余的请求还会隐形的增加用户的网络流量资费。因此考虑使用缓存技术对已经获取的资源进行重用,是一种提升网站性能与用户体验的有效策略。
缓存的原理
缓存的原理是在首次请求后保存一份请求资源的响应副本,当用户再次发起相同请求后,如果判断缓存命中则拦截请求,将之前存储的响应副本返回给用户,从而避免了重新向服务器发起资源请求。
缓存类别
缓存的目标是减少从原始数据源获取数据的次数,从而加快处理速度并减少延迟。
缓存可以在不同体系架构级别上实现,包括Http缓存、内存缓存、磁盘缓存、数据库缓存和CDN缓存。
可以用不同的技术缓存数据,每种技术都有其优缺点。比如内存缓存将数据存储在计算机主内存中,与磁盘缓存相比,可以实现更快的访问速度。另一方面,磁盘缓存将数据存储在硬盘上,速度比内存慢,但相对访问远程数据源的速度还是要快得多。使用数据库缓存,将频繁访问的数据存储在数据库中,从而减少从外部存储检索数据的需要。 最后,CDN缓存将数据存储在分布式服务网络中,从而减少访问远程数据的时延。
缓存系统关键性能指标
为了提高缓存系统的效率和性能,非常重要的一点是需要监控各种指标,从而根据指标做出有关缓存系统的重要业务决策。
需要考虑的参数有:
- 缓存命中率: 该指标衡量请求项在缓存中被找到次数的百分比。较高的缓存命中率意味着缓存可以提供更多数据,从而减少访问外部存储并提高性能。
- 时延: 时延是指访问数据所需时间。在缓存系统中,较低的时延意味着数据服务速度更快,从而提高整体性能。
- 吞吐量: 吞吐量度量在给定时间范围内可以处理的数据量。高吞吐量的缓存系统可以处理更多请求,提供更多数据,从而提高整体性能。
- 缓存大小: 缓存大小是为缓存分配的内存或存储的容量。缓存大小会影响缓存命中率,较大的缓存可以提高命中率,但也会增加缓存解决方案的成本和复杂性。
- 缓存未命中率: 此指标度量请求项在缓存中找不到并且需要从外部存储中获取的次数百分比。高缓存未命中率意味着需要从外部存储获取更多数据,从而对性能造成影响。
如果一直监控这些性能指标,就可以据此优化缓存系统,以获得更高的吞吐量和更低的时延。
HTTP缓存
HTTP缓存应该算是前端开发中最常接触的缓存之一,它又可以细分为强制缓存和协商缓存,二者最大的区别在于判断缓存命中时,浏览器是否需要向服务器端进行询问以协商缓存的相关信息,进而判断是否需要就响应内容进行重新请求,下面让我们来看看HTTP缓存的具体机制及缓存的决策策略。
强制缓存
对于强制缓存而言,如果浏览器判断所请求的目标资源有效命中,则直接从强制缓存中返回请求响应,无须与服务器进行任何通信。
其中与强制缓存相关的两个字段是expires和cache-control,expires是在HTTP1.0协议中声明的用来控制缓存失效日期时间戳的字段,它由服务器端指定后通过响应头告知浏览器,浏览器在接收到带有该字段的响应体后进行缓存。
若之后浏览器再次发起相同的资源请求,便会对比expires与本地当前的时间戳,如果当前请求的本地时间戳小于expires的值,则说明浏览器缓存的响应还未过期,可以直接使用而无须向服务器端再次发起请求。只有当本地时间戳大于expires值,发生缓存过期时,才允许重新向服务器发起请求。
从上述强制缓存的是否过期的判断机制中不难看出,这个方式存在一个很大的漏洞,即对本地时间戳过分依赖,如果客户端本地的时间与服务器端的时间不同步,或者对客户端的时间进行主动修改,那么对于缓存过期的判断可能就无法和预期相符。
为了解决expires判断的局限性,从HTTP1.1协议开始新增了cache-control字段来对expires的功能进行拓展和完善。从上述代码中可见cache-control设置了maxage=31536000的属性值来控制响应资源的有效期,它是一个以秒为单位的时间长度,表示该资源在被请求到后的31536000秒内有效,如此便可避免服务器端和客户端时间戳不同步而造成的问题。
注意:如果Cache-Control的max-age和expires同时存在,则以max-age为准。
Cache-Control的其他参数
- no-cache
设置no-cache并非不适用缓存,而是表示强制进行协商缓存,即对于每次发起的请求都不会再去判断强制缓存是否过期,而是直接与服务器写撒谎给你来验证缓存的有效性,若缓存未过期,则会使用本地缓存。
- no-store
设置no-store则表示禁止使用任何缓存,客户端的每次请求都需要服务器端给予全新的响应。no-cache与no-store是两个互斥的属性值,不能同时设置。
- public
若资源响应头中的cache-control字段设置了public属性值,则表示响应资源既可以被浏览器缓存,又可以被代理服务器缓存。
- private
private则限制了响应资源只能被浏览器缓存,如果没有显示指定则默认值是private。
- max-age
表示服务器端告知客户端浏览器响应资源的过期时长。
- s-maxage
对于大型架构的项目通常会涉及使用各种代理服务器的情况,这就需要考虑缓存在代理服务器上的有效性问题,这边是s-maxage存在的意义,它表示缓存在代理服务器中的过期时长,且仅当设置了public属性值时才是有效的。
由此可见,cache-control能够作为expires的完全替代方案,并且拥有其所不具备的一些缓存控制特性,在项目实践中使用它就足够了,目前expires还存在的唯一理由就是向下兼容。
协商缓存
顾名思义,协商缓存就是在使用本地缓存之前,需要向服务器发起一次GET请求,与之协商当前浏览器保存的本地缓存是否已经过期。通常是采用所请求资源的最近一次的修改时间戳来判断的。
- 实例
假设客户端需要向服务器请求一个manifest.js的JS文件,为了让该资源被再次请求时能够通过协商缓存的机制使用本地缓存,那么首次返回该图片资源的响应头中应包含一个名为last-modified的字段,该字段的属性值为该JS文件最近一次修改的时间戳。
当我们刷新网页时,由于该JS文件使用的是协商缓存,客户端浏览器无法确定本地缓存是否过期,所以需要向服务器发送一次GET请求,进行缓存有效性的协商,此次GET请求的请求头中需要包含一个ifmodified-since字段,其值正是上次响应头中last-modified的字段值。
当服务器收到该请求后便会对比请求资源当前的修改时间戳与if-modified-since字段的值,如果二者相同则说明缓存未过期,可继续使用本地缓存,否则服务器重新返回全新的文件资源。
基于Last-Modified的协商缓存(服务器端代码)
const data = fs.readFileSync('./imgs/CSS.png');
const { mtime } = fs.statSync('./imgs/CSS.png');
const ifModifiedSince = req.headers['if-modified-since'];
if (ifModifiedSince === mtime.toUTCString()) {
res.statusCode = 304;
res.end();
return
}
res.setHeader('last-modified',mtime.toUTCString())
res.setHeader('Cache-Control','no-cache');
res.end(data);
复制代码
CDN缓存
CDN全称是内容分发网络,它是构建在现有网络基础上的虚拟智能网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、调度及内容分发等功能模块,使用户在请求所需访问的内容时能够就近获取,以此来降低网络拥塞,提高资源对用户的响应速度。
不使用CDN的通信流程
- 向传统的DNS服务器请求域名解析。
- DNS服务器返回域名对应的服务器IP。
- 根据服务器IP请求服务器内容。
- 服务器返回响应资源。
使用CDN的通信流程
- 客户端向传统的DNS服务器请求域名解析。
- 传统的DNS服务器将域名解析权交给了CNAME指向的专用DNS服务器,所以对用户输入域名的解析最终是在CDN专用的DNS服务器上完成的。
- CDN专用的DNS服务器将CDN负载均衡器的IP发给客户端。
- 浏览器会重新向CDN负载均衡器发起请求,经过对用户IP地址的距离、所请求资源内容的位置等的综合计算,返回给用户确定的缓存服务器IP地址。
- 浏览器最后对缓存服务器进行请求资源。
静态资源适合使用CDN
静态资源指的是不需要网站业务服务器参与计算即可得到的资源,包括第三方库的JavaScript脚本文件、样式表文件以及图片等,这些文件的特点是访问频率高、承载流量大、但更新频次低,且不与业务有太多耦合。
如果是动态资源文件,比如依赖服务器端渲染得到的HTML页面,它需要借助服务器端的数据进行计算才能得到,所以这样的资源不适合存放在CDN缓存服务器上。
读密集型应用缓存
读密集型应用(如Wordpress/静态图像网站)需要设计缓存系统,以支持更多的读缓存。
下面是一些有用的方法:
- Cache-aside
- Cache-through
- Refresh-ahead
- Cache-Aside
Cache-aside方法是最常用的缓存策略之一。
方法
- 每当应用发送请求,首先检查缓存中是否有请求的数据。
- 如果有,返回缓存的数据。
- 否则,应用程序从数据库查询数据,并在返回途中更新缓存,然后返回数据。
优缺点
- 每次缓存未命中都会导致三次访问,可能会造成明显的时延。
- 如果有人更新数据库而不写入缓存,可能会读到过期数据。(因此,Cache-aside通常与其他策略一起使用)。
- Read-through
在read-through方法中,缓存对数据库进行读取/查询操作,然后更新自己并将请求数据返回给最终用户。
方法
- 应用程序每次都从缓存中查询数据。
- 如果数据不在缓存中,则缓存查询数据库并更新自己。
- 缓存将数据返回给最终用户。
优缺点
- 简化应用程序代码,read-through策略确保将数据获取逻辑转移到缓存中,从而简化了应用程序代码。
- 更好的读取可伸缩性。当某个key在Cache-aside中过期时,并发请求可能会触发多次数据库查询相同的数据。在Read-through中,缓存确保只向数据库发送一个查询。
- Refresh-ahead
Refresh-ahead策略是在过期之前刷新缓存数据,该方法适用于热数据,即预计在不久的将来会被请求的数据。
方法
- 假设缓存数据的过期时间为60秒,刷新提前系数为0.5。
- 如果缓存数据在60秒后被访问,将从缓存存储执行同步读取以刷新其值。
- 如果缓存数据在30秒后被访问,比如第35秒,缓存将直接返回数据,并异步刷新数据。
优缺点
因此,refresh-ahead缓存本质上是在下一次可能的缓存访问之前以配置的间隔刷新缓存。在这种读流量非常高的系统中,几毫秒内可能会发生几千个读操作。
写密集型应用缓存
任何写密集型应用程序都需要缓存策略,例如:
- Write-Through
Write-Through策略将缓存作为其主数据存储,即首先在缓存中更新数据,然后才在数据库中更新数据。
下面是应用想要写入数据或更新值时发生的情况:
- 应用程序将数据直接写入缓存。
- 缓存更新主数据库中的数据。当写操作完成时,缓存和数据库都具有相同的值,并且始终保持一致。
优缺点
当与read-through配合使用时,在网络调用中非常有效,数据首先被读/写到缓存中,使得几乎不会发生缓存无效的情况。由于所有数据都是新的和经常访问的数据,而且所有对数据库的写入都是通过缓存完成,使得数据库和缓存几乎始终保持一致。
- Write-back
Write-back方法与write-through非常相似,只是数据库写调用是异步的。
优缺点
-
Write-back缓存可以提高写性能,非常适合涉及大量写操作的工作负载。当与read-through结合使用时,也非常适合混合工作负载,可以确保最近更新访问的数据始终在缓存中可用。
-
降低网络成本: 如果使用批处理调用,还可以减少对数据库的总体写操作,从而减少负载并降低成本,特别是当数据库按请求数量收费时(例如DynamoDB)。
-
缓存使用效率低下: 不经常被请求的数据也会被写入缓存。这点可以通过TTL进行优化。
-
Write-around
在Write-around方法中,首先将数据更新到数据库,然后数据库对缓存进行异步调用以更新key。
方法
- 当收到写请求时,应用程序更新数据库中的记录。
- 数据库异步更新/删除缓存中的键。
优缺点
Write-around可以与read-through结合使用,在数据只写入一次,读取频率较低或从不读取的情况下提供良好的性能(例如实时日志或聊天室)。同样,这种模式也可以与cache-aside结合使用。
2.缓存穿透问题
在高并发的场景中,缓存穿透是一个经常都会遇到的问题。
什么是缓存穿透
大量的请求在缓存中没有查询到指定的数据,因此需要从数据库中进行查询,造成缓存穿透。
会造成什么后果
大量的请求短时间内涌入到database中进行查询会增加database的压力,最终导致database无法承载客户单请求的压力,出现宕机卡死等现象。
常用的解决方案
1.空值缓存
在某些特定的业务场景中,对于数据的查询可能会是空的,没有实际的存在,并且这类数据信息在短时间进行多次的反复查询也不会有变化,那么整个过程中,多次的请求数据库操作会显得有些多余。不妨可以将这些空值(没有查询结果的数据)对应的key存储在缓存中,那么第二次查找的时候就不需要再次请求到database那么麻烦,只需要通过内存查询即可。这样的做法能够大大减少对于database的访问压力。
2.布隆过滤器
通常对于database里面的数据的key值可以预先存储在布隆过滤器里面去,然后先在布隆过滤器里面进行过滤,如果发现布隆过滤器中没有的话,就再去redis里面进行查询,如果redis中也没有数据的话,再去database查询。这样可以避免不存在的数据信息也去往存储库中进行查询情况。
3.缓存雪崩场景
什么是缓存雪崩
当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力。
如何避免缓存雪崩问题
1.使用加锁队列来应付这种问题。当有多个请求涌入的时候,当缓存失效的时候加入一把分布式锁,只允许抢锁成功的请求去库里面读取数据然后将其存入缓存中,再释放锁,让后续的读请求从缓存中取数据。但是这种做法有一定的弊端,过多的读请求线程堵塞,将机器内存占满,依然没有能够从根本上解决问题。
2.在并发场景发生前,先手动触发请求,将缓存都存储起来,以减少后期请求对database的第一次查询的压力。数据过期时间设置尽量分散开来,不要让数据出现同一时间段出现缓存过期的情况。
3.从缓存可用性的角度来思考,避免缓存出现单点故障的问题,可以结合使用 主从+哨兵的模式来搭建缓存架构,但是这种模式搭建的缓存架构有个弊端,就是无法进行缓存分片,存储缓存的数据量有限制,因此可以升级为Redis Cluster架构来进行优化处理。(需要结合企业实际的经济实力,毕竟Redis Cluster的搭建需要更多的机器)
4.Ehcache本地缓存 + Hystrix限流&降级,避免MySQL被打死。 使用 Ehcache本地缓存的目的也是考虑在 Redis Cluster 完全不可用的时候,Ehcache本地缓存还能够支撑一阵。 使用 Hystrix进行限流 & 降级 ,比如一秒来了5000个请求,我们可以设置假设只能有一秒 2000个请求能通过这个组件,那么其他剩余的 3000 请求就会走限流逻辑。 然后去调用我们自己开发的降级组件(降级),比如设置的一些默认值呀之类的。以此来保护最后的 MySQL 不会被大量的请求给打死。
部分内容来源:juejin.cn/post/684490… developer.aliyun.com/article/805… zhuanlan.zhihu.com/p/654005976…
面试常见问题
问题1:强缓存涉及到哪些请求头?
答:涉及到expires和cache-control两个字段,expires是HTTP1.0协议中的,cache-control是HTTP/1.1协议的。
问题2:为什么现在不用expries用cache control?
答:因为基于expires的强制缓存对本地时间戳过于依赖,如果客户端本地的时间与服务器端的时间不同步,那么对缓存过期的判断可能就会出错。cache-control通过maxage=xxx秒的形式来控制响应资源的有效期,如此可以避免服务器端和客户端时间戳不同步的问题。
问题3:强缓存public private no-store no-catch区别?(Cache-Control有哪些属性?分别表示什么意思?)
public:表示响应资源既可以被客户端缓存也可以被代理服务器缓存。 private:表示响应资源只能被浏览器缓存,如果没有显式指定则默认是private no-store:表示禁止使用任何缓存,每次请求都需要服务器给与全新的响应。 no-cache:表示使用协商缓存。每次请求不再去判断强制缓存是否过期,而是直接向服务器发送请求来验证缓存的有效性。 max-age:表示服务器端告知客户端浏览器响应资源的过期时长。 s-maxage:表示缓存在代理服务器中的过期时长,且仅当设置了public属性值时才是有效的。
问题4:协商缓存的校验是在客户端还是服务器端?协商缓存怎么验证是否命中?
答:服务器端,服务器端会对比文件最后的修改时间和客户端请求携带的时间是否一致,一致则判断命中缓存。协商缓存存在两种形式,一种是基于last-modified,客户端第一次请求目标资源的时候,服务器返回的响应标头中包含last-modified和该资源的最后一次修改的时间戳,以及cache-control:no-cache,当客户端再次请求该资源的时候,会携带一个ifmodifiedsince字段,如果这个字段对应的时间和目标资源的时间戳进行对比,没有变化则返回304状态码。另一种是基于Etag的协商缓存,手下服务端将要返回给客户端的数据通过etag模块进行哈希计算生成一个字符串,这个字符串类似于文件指纹,检测客户端的请求标头中的ifNoneMatch字段的值和第一步计算的值是否一致,一致则返回304,不一致则返回最新的数据以及etag标头和Cache-Control:no-cache。
问题5:协商缓存出于什么原因有Last-Modified,Etag?
答:之所以有last-modified还有etag,是因为这二者均有自己的不足,last-modified是根据请求资源的最后修改时间戳来进行判断的,有可能只是对文件名进行了编辑,但是文件内容并未修改,这样时间戳也会更新,从而导致协商缓存判断失效,请求了已经存在的完整资源,这对网络带宽是一种浪费,也有可能是文件修改的速度是毫秒级别的,但是last-modified的单位是秒,可能无法识别出资源的修改。etag并非last-modified的完全替代方案,只能是一种补充方案,etag存在的问题是,服务器需要对文件资源进行etag计算,需要付出额外的计算开销,如果资源的尺寸比较大,生成Etag的过程可能会影响服务器的性能,所以这也就是为什么协商缓存既有last-modified又有etag的原因了。
问题6:协商缓存和强缓存的区别?
相同点
都是从客户端缓存中读取资源。
不同点
- 如果浏览器命中的是强缓存,则不需要给服务器发请求,而协商缓存最终由服务器来决定是否使用缓存,即客户端与服务器之间存在一次通信。
- 在chrome中命中缓存,返回的状态码是200,而如果是协商缓存,返回的是状态码304。
问题7:expires 和 cache-control 哪个优先级高? 不缓存怎么设置?
答:expires是HTTP/1.0的产物,Cache-Control则是HTTP/1.1的产物,二者如果同时存在的话,Cache-Control优先级比Expires高。不缓存则是通过Cache-Control:no-store设置。
问题8:LastModified 对应有个请求头是什么?
last-modified-since.
问题9:缓存的优先级顺序?
答:Cache-Control > Expires > Etag > Last-Modified。