缓存的本质
先聊一下缓存存储的基础。首先,局部性原理,是缓存存储的基础,即在局部的时间,对数据的访问是局部的、集中的(概率上去讲);另一个基础是,能快速提供数据访问的资源总是缺乏的,即如果所有数据都能被快速访问,那缓存即没有意义。
从本质上讲,即对数据计算的中间结果进行缓存,形成数据冗余,同样的输入的情况下,直接返回结果。比如在算法中,对于Add函数,输入相同的情况下,如果此函数没有副作用,那么输出也应该相同,那么,我们就可以建一个Map,存储结果,下次再有同样的数据需要计算,那么就直接查询出结果,而不用计算;再比如,数据库中,如果统计所有订单金额,即可以设计一个中间表,查询的话,直接访问中间表,而不用直接去统计数据,只需要在更新订单后,重新计算数据。
从Web服务的常用架构上来讲,一般请求进来以后,会从Web代理服务,到业务服务器,到数据库进行数据持久化,在业务服务器,可以在内存或者外部访问比较快的中间件中缓存数据。从架构上讲,一般可以对Http请求、SQL请求进行数据缓存,因为这两个是业务处理的边界,业务代理通过访问数据库,查询、修改数据,响应Http的请求。
综合来说,一般的机制如下:
- 一个是透明缓存,即消费侧,不处理命中失效的情况,只是请求缓存,命中,则拿数据,没有命中,则挂起请求,由缓存处理器去请求生产端
- 另一种是非透明的,即消费侧,先去找缓存数据,命中则取数据,不命中,则返回,由
建立一个模型,去判断缓存的意义。
CacheSize: 是缓存容量
QData: 指消息端去直接请求生产端, 产生一个功能的请求的代价
QCache: 指消息端去请求缓存端,产生一个成功/失败请求的平均代价
QCacheData: 指消息端去请求缓存端,请求失败,缓存端命中失败,触发访问生产端,并更新缓存的代价, 一般是QCache+DCache+UCache的代价的总和
UCache:指生产端更新缓存的代价
DCache:指缓存被标记失效的代价
CAP:指命中率
结合Web服务的架构, 我们假定读取访问缓存的代价与访问数据库的代价差了一个数量级,暂定为(访问的数学期望)10MS与100MS。
假定请求数为M,只读业务占有90%,如不使用缓存,所需要的代价为A = M*QData;如使用缓存,则有B = 10%M + 90%M * (1-CAP) * QData + 90%M * CAP * QCache ;故 N=B/A,则为缓存的期望效率。
缓存刷新的机制
缓存的失效有几种,适用于不同的场景。比如标记时间失效、主动标记失效、按算法清理(缓存空间一定)。主动标记失效,可以在业务执行前、也可以在业务执行后,但一般在业务执行后,因为业务层面本身事务性、有失败的情况。
缓存存储的KEY
KEY是查义缓存数据的关键,一般采用各种HASH算法。
几个边界情况
由于现实中,请求不是平均进到服务器的,所以有一个排队的情况,假定一个业务服务器,比如Tomcat业务服务器。我们假定Tomcat有500个线程,则同时最多可以进来500个请求,系统排队分别处理500个请求。同一时间(区间)的500个请求,有几种边界情况。
- 全部打到缓存上,并全部命中
- 全部打到缓存上,命中一部分,另一部分需要请求数据并刷新缓存
- 部分打到缓存上,
** 部分打到缓存上(写请求无法打到缓存上),部分请求触发了数据更新,需要标记缓存数据失效
……
总之,请求进来以后,会触发上面定义的几种操作,命中、没命中、标记失效、刷新缓存,那第统计这些操作所付出的代价,与完全不使用缓存作为对比,则得出缓存的效率与解决方案。
在其它条件不变的情况下,缓存命中率,是影响系统处理请求所花费时间的主要因素。影响命中率的因素主要有缓存容量的大小、数据有效时间、访问集中度。缓存容量大,则可以缓存更多数据,命中率上长;数据有效时间短,频繁刷新缓存,原因可能是频繁修改数据(也有可能是缓存容量过小),则也会导致缓存命中率低;访问集中度,是决定缓存命中率的一个客观因素,访问集中度高,在同样的数据有效时间内,可以提高缓存命中率。
总之随着命中率的下降,系统处理请求期望时间也会变长,从而导致系统的QPS能力下降,(另一个影响是请求处理时间过长,导致请求处理的结果失效,消费端已经放弃接收结果)一旦QPS下降到一定程度,导致消费端请求失败,无法进入请求阶段,消费端频繁重新请求,没有熔断机制,则会导致系统崩溃。
导致请求的期望响应时间过长
会有几种极端的情况
一个是缓存全部失效,所有的请求打到数据库,那么系统能提供的QPS,就是拿掉缓存的系统,(当然,如果需要自动刷新缓存,还有自动刷新缓存的代价),如果打到系统上的QPS量过大,而没有熔断机制,则系统会被击溃。
无论如何,如果要保证系统能持续的提供服务,就要避免系统承载能力退化后,大量积压请求压到系统上,避免进入请求阶段的请求在相应的处理时间内处理完成(没有进入队列的,可以直接丢掉)。而决定一个请求处理时间的,则是对远程资源的访问,如数据库、RPC调用,对竞争资源的争取。
这里引出两个问题,一个是如何观察系统的响应时间,根据系统的响应时间判断系统的承载能力,另一个是系统内如何管理资源,如访问数据库的时间、RPC调用的时间、对竞争资源的争取的时间,避免因为业务处理时间过长,导致请求处理失效(处理了,但没有起有作用)而玩死自己。
无效请求带来的缓存击穿问题
如果缓存只是对正常的业务访问进行缓存,如查询每个用户的个人信息,系统如果只对返回有用户信息的请求进行缓存,而对没有用户信息为空的访问没有进行缓存,如果消息端持续攻击,则会降低缓存的命中率。
这个问题本质上是对空值的处理问题。
并发访问带来的重复刷新数据问题
如果一个业务,有多个请求同时进入到处理池,则有可能多个“业务处理”同时OR先后访问数据库,刷新缓存。可以使用同步锁,但也有开销。
服务异常带来的同时刷新数据问题(缓存雪崩)
由于刷新缓存也需要一定的代价,如果缓存失效过多(同时被标记失效),系统处理能力退化,给数据库访问/RPC访问/竞争资源访问带来问题。就是上面聊到的,如何协调内部资源,保证到业务处理队列的请求不要太多。
在一种典型的Web服务架构下,我曾经一种处理方式是限制业务服务器的Tomcat线程数,而对缓存的访问则使用Nginx+Lua的方式在Nginx端进行处理。本质上来说,是把请求队列放大一些,把处理业务的队列放小一些,从而从容的刷新数据。
缓存刷新对缓存的命中的影响,刷新机制也影响着缓存的命中率。缓存刷新是指从请求进到系统后,到系统可以为其它同类请求提供缓存访问的这个时间段。
访问数据:消息端去请求生产端,产生一个成功的请求
几个指标
几种缓存的方式,一种透明的,即代理。
使用缓存与使用数据结构优化访问速度的区别。
网友评论