美文网首页Java 杂谈Spring-Boot大数据
360度全方位解决「缓存」

360度全方位解决「缓存」

作者: 热衷技术的Java程序员 | 来源:发表于2020-10-20 16:10 被阅读0次
    360度全方位解决「缓存」

    使用好缓存就仿佛拥有了无比锋利的宝剑,对于性能的提升是立竿见影的。但是如果使用的不好就会严重的影响系统的运行,那么今天我们看看缓存到底是什么以及缓存的应用模式

    1 缓存的本质

    网上有各种对缓存的定义和解释,我认为缓存是为了节约对原始资源重复获取的开销

    什么是资源?

    首先如果这个资源是经常变化的,你觉得缓存了有什么意义。显然不可行,所以,我们对资源的操作需要时是幂等切安全的。举个例子,比如银行转账,改变了目标状态,自然就不能缓存。

    其次,缓存的数据是"重复"获取的。意味着将经常使用的数据缓存下来,当下一次访问的时候直接返回即可,这样节约了通过原始流程访问数据的时间。比如数据库中的连接池,线程池,就是为了避免频繁的创建链接,销毁链接而出。

    如何衡量这个缓存的效果?

    在数据获取的过程中,通过缓存获得数据的次数,除以总的次数所得到的结果为缓冲命中率

    加了一层缓存就一定更快?

    非也。首先这个快是相对而言的,因为在不同的系统中对于同一对象可能扮演的角色不同。比如CPU的多级高速缓存,即内存的缓存。而内存对于CPU存储而言又比较慢,但是对于磁盘却快了很多,所以它可以用作磁盘的缓存介质

    能够举一个常见的例子来阐述缓存的应用?

    如果看了我之前写的面试文章,应该对这个面试题非常的熟悉了。"当敲入URL会经历什么?",在那篇文章中,写了一万多字来阐述这个问题,意味着如果面试官问你这个问题,你可以讲什么且可以从一个点出发将的比较深。回到这里,如果我让你以缓存的角度来阐述这个问题,你将如何去回答?

    首先在地址栏输入域名,会查询浏览器内部"域名-IP"缓存,如果之前通过这个浏览器访问过这个域名,那么就有一张映射表。

    如果没有,则会查询操作系统是否存在这个缓存,如果是mac电脑则/etc/hosts就可以定义域名到IP的关系。

    如果还是没有则会查询域名服务器DNS得到对应的IP和可缓存的时间。如果需要使用命令来测试,这里可以同dig命令查看这个详细过程。

    当请求到达了服务端,通常会有反向代理,在反向代理中会存在缓存配置,对于一个MVC架构的应用而言,MVC的各个层都有可以应用缓存模式

    Controller层有拦截过滤器,可以通过配置缓存来过滤我们需要的请求,这样过滤器直接返回缓存结果而不执行后面的逻辑

    对于Model层,在DAO上的Service暴露API应用缓存也是非常常见的操作

    对于View层,页面标签居多,不会每次都去渲染,而是通过缓存标签的方式节省渲染操作

    这一切操作完事后就会返回给浏览器,浏览器会加载页面中大量的资源,这都是通过读取浏览器的缓存来避免HTTP请求的开销。

    看吧,我们在不断冲浪的同时,下面有着这么精彩的故事,也说明了缓存的重要性了吧

    3 缓存应用模式

    通过不同的特性和优缺点,将缓存分了几种模式,我们看看这几种模式的区别以及优缺点

    Cache-Aside

    数据库的异常情形

    • 如果数据库读取异常直接返回失败,不会存在数据不一致的情况
    • 如果数据读取成功了,但是缓存的写入失败,那么下次访问统一数据的时候继续尝试写入,也没有数据不一致的情况

    所以这两种情况是比较安全的

    数据更新的策略,到底是先更新数据库还是先让缓存失效

    假设我们让缓存显示小,意味着在数据库更新成功之前,如果另外的请求访问缓存,此时缓存已经失效,那么就会按照原来的访问策略,从数据库中使用这个已经旧的数据去更新缓存的数据,这样就导致过期的数据会长期存放于缓存中,最终导致数据不一致。

    为了加深大家对这个概念的理解,画了下面这幅图

    360度全方位解决「缓存」

    第二需要注意的是:数据库更新以后,需要让缓存失效而不是更新缓存为最新的值

    为什么呢?如果两个几乎同时发出的请求分别更新数据库中的值A和B,如果结果是B的更新晚于A,那么数据中的最终值为B。但是,如果数据库更新后去更新缓存,而不是让缓存失效,那么缓存中的数据就有可能是A,而不是B。因为数据库虽然是"更新为A"在"更新为B"之前发生,但如果不做特殊的跨存储系统的事务控制,缓存的更新顺序就未必遵从"A先于B"这个规则,就会导致缓存中的数据是一个长期错误的值A。

    同样画个图,让大家理解更深刻

    360度全方位解决「缓存」

    4 缓存常出现的问题

    上面大多数时候都在谈论缓存带来的好处,现在该说说其弊端了

    • 缓存穿透

    当在同一时间有大量的数据访问,经过了缓存屏障但是并没有起到应有的保护作用。举个例子,需要在数据库对一个key进行查询,如果数据库中没有这个数据,那么缓存也没有这个数据,每次请求都会去数据库查,缓存再次根本没有作用

    怎么解决这个问题勒?

    我们可以在缓存中对这个key存放一个空结果,毕竟没有结果也是结果嘛

    还有一种比较严重的缓存穿透后果。在一般的缓存策略下,通常需要先发生一次缓存命中失败,然后从数据库得到结果,再填到内存缓存中。但是,如果这个数据库的查询太慢,此时大量的数据接踵而至,全部穿透缓存落在数据库上, 此时对于最初的请求所引发的缓存可能还没来得及混村,数据库就直接挂掉了,这种问题有那么解决方案呢

    还有其他的解决方案?

    使用布隆过滤器。主要用来判断一个元素是否在一个集合中,这个算法由一个二进制数据和一个hash算法组成,其基本的思路是这样的:

    我们把集合中的每一个值按照提供的 Hash 算法算出对应的 Hash 值,然后将 Hash 值对数组长度取模后得到需要计入数组的索引值,并且将数组这个位置的值从 0 改成 1。在判断一个元素是否存在于这个集合中时,你只需要将这个元素按照相同的算法计算出索引值,如果这个位置的值为 1 就认为这个元素在集合中,否则则认为不在集合中。 画个图看看就更清楚了

    360度全方位解决「缓存」

    ABC组成一个集合,元素D计算出来对应的数值为1,所以D也存在于集合。但是F在数组值为0,所以不在集合中了。

    如何通过布隆过滤器解决缓存穿透?

    我们以存储用户为例子,初始化一个大数组,长度为10亿的数据,选择一个Hash算法,计算用户的ID的Hash值并映射到这个大数组中,映射的位置为1,其他设置为0

    一旦存在新注册的用户,当我们需要查询一个用户的信息的时候,首先查询的ID在布隆过滤器中是否存在,不存在则直接返回空,不继续查询数据库和缓存,这样不就大大的减少异常查询带来的缓存穿透

    360度全方位解决「缓存」

    过滤器具有非常高的性能,无论是写入还是读取时间复杂度都是O(1)。空间上也是有着不错的又是,假设20亿的数据需要2000000000/8/1024/1024 = 238M空间,而如果使用数组来存储,每个用户ID占4个字节,存储20亿 需要2000000000 * 4 / 1024 / 1024 =7600M 的空间,是布隆过滤器的 32 倍

    布隆过滤器有哪些缺陷?

    • 判断元素是否在集合中有一定的错误几率

    这个问题是hash算法的问题,Hash算法难免有一定的碰撞几率,所谓碰撞,即不同的输入值得到相同的Hash结果

    • 不支持删除元素

    举个例子,假设两个元素A和B都是集合中的元素,具有相同的Hash值,会映射到数组相同的位置。此时如果删除A,数组中对应的位置从1变为-,那么在判断B的时候发现B不在集合中了,得到了错误的结论

    如何解决这个问题

    让数组中不在只有0和1两个数值,直接存储一个计数。假设AB同时命中数组的索引,此时这个值为2,如果A删除,将这个值改为1。当然这样就会增加空间的消耗,所以使用过滤器根据业务需求而定。

    流量控制

    限制对于同一数据的访问,必须等到前一个完成以后才能进行下一个,即如果缓存失效而引发的数据库查询正在进行,那么请他的请求只好乖乖的等着,这种方法通用性还可以,但是这个等待机制会比较影响用户的体验

    缓存预热

    在大量请求到来之前先预热一波,简单高效,局限性在于需要提前知道那些数据可能引发缓存穿透的问题

    • 缓存雪崩

    在一定时间段出现大量的请求访问失效,直接造成后方的系统压力过大,引起系统过载,宕机,这就是缓存雪崩。

    通常采取限流和预热两种方式进行避免。限流可以保障请求大量落到数据库的时候,系统只接纳能够承载的数量。对于预热则主动先王内存中加载一定的热点数据,请求到来的时候缓存不是空的,这样具有一定的保护能力。

    缓存数据存在过期时间,如果缓存加载时间比较集中,那么此时大量的缓存同时国企,对这些数据的请求将全部落在后面的数据库从而导致系统崩溃,如何避免?

    这里主要避免缓存集中写入时间,如果无法避免则使用一个范围的随机数均匀分散过期时间,从而打散系统造成的压力

    • LRU致命缺陷

    LRU即Least Recently Used,最少最近使用算法,原理是维护一个限定最大容量的队列,队列头部总是放置最近访问的元素,超过容量限制的时候从队尾淘汰元素。

    通过一个图看看LRU工作原理

    360度全方位解决「缓存」

    这看起来是 个不错的方案,需要从缓存淘汰数据的时候总是从尾部淘汰一个不常用的,但是如果有用户有意无意的访问一些错误信息,此时就会破坏整个LRU队列中最近访问数据的真实性。

    如果一不小心咱们缓存中存放不少冷门数据,热门活动开始后,大量的请求到来,全部穿透缓存导致数据库压力倍增,网站响应时间瞬间到告警线上。

    怎么解决这个问题?

    这里基于LRU的改进方案很多,简单说下LRU-K,即主缓存队列排的是"第K次访问的元素",也就是如果访问的次数小于k,则将其放在另一个"低级"的队列中维护,这样保证在一定的访问下限才能被送到LRU主队列中

    相关文章

      网友评论

        本文标题:360度全方位解决「缓存」

        本文链接:https://www.haomeiwen.com/subject/gppdmktx.html