前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >猿思考系列8——缓存的套路也就这些

猿思考系列8——缓存的套路也就这些

作者头像
山旮旯的胖子
发布2020-07-28 16:53:08
1930
发布2020-07-28 16:53:08
举报
文章被收录于专栏:猿人工厂猿人工厂

在之前的两个章节中,我们主要讨论了数据库的两个比较重要的知识——事务和索引。猿人工厂君也知道,内容对于新手而言,理解起来还是比较很吃力的,文中提到的原理和内容,有兴趣的可以和我一起探讨,猿人工厂君就不一一赘述了。今天我们会开始一个新的话题。

哈哈,缓存这个话题比较热,有太多的文章聊过它了。今天我们也聊聊缓存,不过可能会有些不一样的地方吧。缓存是什么?缓存其实就是计算结果。那缓存有什么作用?减少计算,甚至是不计算,最好还是用别人家的机器计算。一般来讲,好多人会武断的认为,缓存其实就是内存里的那点儿事儿。其实思路开阔一些,缓存又岂止是内存。图上是个段子,但是实际的生活中,类似的事情还是很多的。如果要讲缓存,那么大概分为两类吧。一类是静态的,一类是动态的。

比如某网站左侧的类目信息,很多人认为这种信息应该是动态内容吧,因为涉及类目和对应的链接而且数量比较多。实际上,对于大型站点来说,这类信息万年不变,早就是静态的了直接变成html代码片段,静态化了。那什么是动态的呢?别急,这个后面会讲。

如果要搞懂缓存在互联网方面的应用,最简单的就是访问一个大型站点了。然后顺着访问链路一层一层抽丝剥茧。

第一层缓存往往出现在浏览器端,这种方式其实就是拿别人家的机器计算的方式,客户端嘛,还有这类信息用web服务器处理就好了,IO方面的表现比应用服务器强太多了。你可能经常看见http header中出现一些关键字比如Expires ,Cache-Control,Last-Modified等等,这些就和这个有关了。

Expires服务端返回的过期时间,如果请求时间小于这个时间,那么说明缓存没有过期,则可以直接使用客户端的数据。如果请求时间大于这个时间,说明在请求的时候这个数据已经过期,必须从服务器重新获取数据。

Cache-Control 的值有private、public、no-cache、max-age,no-store,默认为private。含义如下:

private: 客户端可以缓存

public: 客户端和代理服务器可缓存

max-age=?: 缓存的内容将在 ?秒后失效

no-cache: 需要使用对比缓存来验证缓存数据

no-store: 所有内容都不会缓存

需要注意max-age的值哈,是秒级的比如你写30,那么超过30秒,客户就需要重新到服务端获取数据了哈。

Last-Modified又是干什么的呢?Last-Modified是服务端在响应时通知客户端资源被修改的最后时间,比较有用噢,可以把它看做一种标识。客户端第一次请求时,会将数据和标识一起返回,客户端将标识和数据一起存起来。然后再次请求时,客户端将标识发给服务器,服务器根据这个标识判断资源是否发生变化,如果没有变化,发一个304给客户端,客户端直接用就行了,这个种事情,服务端只会返回header不会返回body,是不是轻松多啦?

那具体怎么来用呢?哈哈这个就跟规则有关了,下面就介绍几个。

Last-Modified/ If-Modified-Since 规则:客户端次一次请求时服务器返回Last-Modified,客户端第二次请求时向服务端发起一个If-Modified-Since字段,服务器看到之后对比资源时间是否改动过,如果没有改动返回304,客户端直接用就好了(只有header),如果有改动,返回新的资源(header和body)和状态码200,以及新的Last-Modified,客户端再使用新的就好了。

Etag /If-None-Match 规则:客户端次一次请求时服务器返回Etag(资源的哈希值),客户端第二次请求时向服务端发起一个If-None-Match字段并且传递之前的Etag,服务器看到之后对比资源的哈希值,如果没有改动返回304,客户端直接用就好了(只有header),如果有改动,返回新的资源(header和body)和状态码200,以及新的Etag,客户端再使用新的就好了。

把资源搬到你隔壁的网吧,这个当然是最好的啦。你经常访问站点的时候,通过浏览器工具,你经常看到过xxxcdn.xxx.com这类的域名吧。这就是cdn啦,一般来讲,一些静态的资源,css,js,html等等代码片段都仍在上面了。这些服务器理你家近的,甚至就和你在一个镇上都有可能的。你访问站点的路由就减少了,自然就快了。

相信看过java代码执行套路的同学,已经知道jvm有JIT技术可以字节码直接编译成机器代码。从这个层面上来讲,机器代码就是对字节码的一个缓存。这样类似的场景还有一些的,比如涉及到后端渲染的模板,你会用到类似于(当然你可能没接触过)<esi:include src="/xxxx" />这样的标签,这是一种叫做ESI的技术,专门处理代码片段的。ESI的出现让动态的模板能够得到缓存支持。Web服务器读取到ESI标签,可以根据src的地址去获取资源,从而把内容合并在缓存的html中,一并发给客户端。这样就解决了动态模板不能缓存的问题了。不过动态模板,往往是和应用服务器在一起的噢,这种技术派上用场的时候,一般来说已经早过了cdn了,到你的应用服务器层了,多说一句,搞懂代码在哪儿执行真的很重要噢。

内部的事情自然就是内存缓存了,比如JVM。在这个事情上,它其实也是很有优势的。内存换时间,这个道理大家都懂,它还有一个特点就是不需要网络开销,也不需要序列化和反序列化对象,节约cpu的。但是也有一个端——数据量大了,内存受影响,也放不下,一般来说,我们把热点的,数据量不大的数据放在这里。当然,你肯定也听过ehcache之类的,进程级别的,能隔离应用的JVM,减小内存开销都在一个进程里的影响,也是有些许好处的。

代码语言:javascript
复制
代码语言:javascript
复制
代码语言:javascript
复制

数据量大了,可靠性方面是要有要求的。分布式缓存的数据会分布到不同的缓存节点上,每个缓存节点缓存的数据依然是有容量限制的。一般来说为了方便访问这些数据节点,会假如一个代理机制去访问和识别节点。比如增加了新的节点,代理也能识别它,还能把新的缓存数据存放过去。而且为了可靠性保证,很多时候会对每个节点增加master/salve的设计,缓存数据写入Master 节点的时候,会同时同步一份到Slave节点。一旦Master节点失效,可以通过代理直接切换到Slave 节点保证缓存的正常工作。每个缓存节点还可以提供缓存过期的机制,也可以会把数据可靠性要求相对较高的缓存保存到文件上,以免丢失。这方面的代表redis就是一个。

可是数据量大了你还是撑不住,解决不了问题啊。这个时候就需要想办法了,比如通过hash算法/一致性hash算法,将数据分片处理,也可以搞数据范围的限制,将一定数据范围的数据放在某个节点,另一部分放到其他节点上等等。

当然,缓存内部对缓存内容的管理也是有很多办法的,比如FIFO(First In First Out):先进先出算法,最先放入缓存的数据最先被移除。LRU(Least Recently Used):最近最少使用算法,把最久没有使用过的数据移除缓存。LF(Least Frequently Used):最不常用算法,一段时间内使用频率最小的数据被移除缓存。以上这些都是缓存的过期策略,是管理缓存内容的办法。

一般来讲,缓存的失效问题,有以下几个常见的方面:

缓存雪崩:缓存同时失效被清除,缓存未更新,请求无法命中缓存,如果要求强制拿到数据,请求会打到数据库。如果频繁发生,数据库压力也大,应用很快挂掉的。不过对于这类问题,可以考虑对缓存设计不同的过期时间,对缓存更新进行保护,比如只允许一个线程去操作,引入master/salve机制,发生问题及时切换。如果实在需要的话引入熔断(某个节点不能工作,让代理层不要打入流量)/限流机制(保护节点不会打死,但是需要根据流量评估,限制狠了误伤)。

缓存穿透:被别人猜到某个请求后面的缓存key的关系了,恶意伪造,不存在的key,来访问,然后导致应用不停查询数据库。对于这个问题,可以从以下角度去思考。比如既然值是null,那就把null缓存起来,下一次请求来就不走库了,比如根据可能存在的数据范围,建立一个大的bitmap,直接过滤掉,这种搞法还有一个学名叫做“布隆过滤器”。其实还有其他比较有效的,那就是在对于一些非必须保障的场景下,数据只走缓存,取不到就是null,后端建立一个队列,将请求隔离起来再慢慢的去可控的查询。

缓存击穿:在数据请求的时候某一个缓存刚好失效或者正在写入缓存,同时这这个时间点上这个key发生了超高并发请求,成为“热点”数据。这就是缓存击穿,和缓存雪崩的区别在于击穿是只针对某一缓存缓存,雪崩是针对多个缓存。解决这个问题只用保障在同一时刻,一个key只有一个线程在读/写就好了。比如当缓存失效的时候,写一个标识进去,写完后,其他读取的缓存再进行读取就好了。

首先确定哪些内容需要被缓存——一定是查询量比较大,避免每次走数据库或者是其他开销较大的组件(搜索/性能不高的远程接口等等)的场景。要不没有意义的。

同步更新:数据有修改,立刻更新,这种方式最常见,一般访问量不是特别大,业务性能要求不是特别高,比如后台管理时,改个数据什么的。

异步推送:数据有修改,发出消息,或者直接放到内存队列中,另起一个线程,接到消息或者操作队列,更新缓存。

异步拉取:一般来讲,这种场景常常用于解决高并发的场景,和JVM缓存进行配合,单起一个线程,拉去数据(可以是分布式缓存中的数据,也可以是数据库中的数据),放到JVM中。比如,之前提到的那个类目页面片段,如果要动态化,那最好就是这个方式,流量大了redis撑不住,但是内存缓存还撑不住的情况,是不多见的。

以上这些还都是些手段而已,实际的过程中,要充分考虑业务要求的,前台大量业务查询有些时候是可以在缓存层面返回空数据的,坚决避免请求打库的。同时,也要充分考虑静态的威力,至少数据兜底是要考虑的,别人家的机器干嘛不用?

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-04-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 猿人工厂 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
数据库
云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档