了解更多关于安全、合理使用缓存的知识,关于缓存可靠性和数据一致性问题
发布时间:2023-04-03 12:35:49 所属栏目:教程 来源:
导读:在上一篇文档《聊一聊作为高并发系统基石之一的缓存,会用很简单,用好才是技术活》中,我们对缓存的庞大体系进行了一个初步的探讨,浮光掠影般的介绍了本地缓存、集中缓存、多级缓存的不同形式,也走马观花似的初识
|
在上一篇文档《聊一聊作为高并发系统基石之一的缓存,会用很简单,用好才是技术活》中,我们对缓存的庞大体系进行了一个初步的探讨,浮光掠影般的介绍了本地缓存、集中缓存、多级缓存的不同形式,也走马观花似的初识了缓存设计的关键原则与需要关注的典型问题。 作为《深入理解缓存原理与实战设计》系列专栏的第二篇内容,从本篇开始,我们将聚焦缓存体系中的具体场景,分别进行深入的阐述与探讨。本篇我们就一起具体地聊一聊缓存使用中需要关注的典型问题与可靠性防护措施。 在分布式系统盛行的今天,尤其是在一些用户体量比较大的互联网业务系统里面,缓存充当着抗压屏障的作用。当前互联网系统可以扛住动辄数万甚至数十万的并发请求量,缓存机制功不可没。而一旦缓存出现问题,对系统的影响往往也是致命的。所以在缓存的使用时必须要考虑完备的兜底灾难应对策略。 热点数据与淘汰策略 大部分服务端使用的抗压型缓存,为了保证缓存执行速度,普遍都是将数据存储在内存中。而受限于硬件与成本约束,内存的容量不太可能像磁盘一样近乎无限的去随意扩容使用。对于实际数据量极其庞大且无法将其全部存储于缓存中的时候,我们需要保证存储在缓存中的有限部分数据要尽可能的命中更多的请求,即要求缓存中存储的都是热点数据。 缓存实现的时候,必须要有一种机制,能够保证内存中的数据不会无限制增加 —— 也即数据淘汰机制。数据淘汰机制,是一个成熟的缓存体系所必备的基础能力。这里有个概念需要厘清,即数据淘汰策略与数据过期是两个不同的概念。 数据过期,是缓存系统的一个正常逻辑,是符合业务预期的一种数据删除机制。即设定在有效期限内的储存数据,该数据在过期后将其从储存区移除。 数据淘汰,是缓存系统的一种“有损自保”的降级策略,是业务预期之外的一种数据删除手段。指的是所存储的数据没达到过期时间,但缓存空间满了,对于新的数据想要加入缓存中时,缓存模块需要执行的一种应对策略。 进一步地,当决定采用先从容器中扔掉一些已有内容的时候,又会面临一个新的抉择,应该扔掉哪些内容?实践中常用的也有几种方案: 按需排序,保留常用。即基于LRU策略,将最久没有被使用过的数据给剔除掉。 提前过期,淘汰出局。对于一些设置了过期时间的记录,将其按照过期时间点进行排序,将最近即将过期的数据剔除(类似让其提前过期)。 其它策略。自行实现缓存时,除了上述集中常见策略,也可以根据业务的场景构建业务自定义的淘汰策略。比如根据创建日期、根据最后修改日期、根据优先级、根据访问次数等等。 此外,在一些大型系统里面,尤其是一些分布式微服务化的系统中,很多情况下都会有多个独立的缓存服务,而最终持久化数据则集中存储。如果某个独立缓存真的出现了缓存雪崩,业务层面应该如何将受损范围控制在仅自身模块、避免殃及数据库以及下游公共服务模块,进而避免业务出现系统性瘫痪呢?这个就需要结合服务治理中的一些手段来综合防范了,比如服务降级、服务熔断、以及接口限流等策略。 将查询到的数据添加到缓存中,这样就可以使得后面的请求继续命中缓存。 但是这种常规操作存在个“漏洞”,因为大部分缓存容量有限制,且很多场景会基于LRU策略进行内存中热点数据的淘汰,假如有个恶意程序(比如爬虫)一直在刷历史数据,容易将内存中的热点数据变为历史数据,导致真正的用户请求被打到数据库层。因而又出现了一些业务场景,会使用类似上面所举的例子的策略,缓存指定时间段内的数据(比如最近1年),且数据不存在时从DB获取内容之后也不会回写到缓存中。针对这种场景,在缓存的设计时,需要考虑到对这种冷数据的加热机制进行一些额外处理,如设定一个门槛,如果指定时间段内对一个冷数据的访问次数达到阈值,则将冷数据加热,添加到热点数据缓存中,并设定一个独立的过期时间,来解决此类问题。 缓存穿透:合理的防身自保手段 我们的系统对外开放并运行的时候,面对的环境险象环生。你不知道请求是来自一个正常用户还是某些别有用心的盗窃者、亦或是个纯粹的破坏者。 还是上面的论坛的例子: 用户在互动论坛上点击帖子并查看内容的时候,界面调用查询帖子详情接口时会传入帖子ID,然后后端基于帖子ID先去缓存中查询,如果缓存中存在则直接返回数据,否则会尝试从MysqL中查询数据并返回。 有些人盯上了论坛的内容,便搞了个爬虫程序,模拟帖子ID的生成规则,调用查询详情接口并传入自己生成的ID去遍历挖取系统内的帖子数据,这样导致很多传入的ID是无效的、系统内并不存在对应ID的帖子数据。 所以,上面大量无效的ID请求到系统内,因为无法命中缓存而被转到MysqL中查询,而MysqL中其实也无法查询到对应的数据(因为这些ID是恶意生成的、压根不存在)。大量此类请求频繁的传入,就会导致请求一直依赖MysqL进行处理,极易冲垮下游模块。这个便是经典的缓存穿透问题(缓存穿透与缓存击穿非常相似,区别点在于缓存穿透的实际请求数据在数据库中也没有,而缓存击穿是仅仅在缓存中没命中,但是在数据库中其实是存在对应数据的)。 缓存穿透的情况往往出现在一些外部干扰或者攻击情景中,比如外部爬虫、比如黑客攻击等等。为了解决缓存穿透的问题,可以考虑基于一些类似白名单的机制(比如基于布隆过滤器的策略,后面系列文章中会详细探讨),当然,有条件的情况下,也可以构建一些反爬策略,比如添加请求签名校验机制、比如添加IP访问限制策略等等。 缓存的数据一致性 缓存作为持久化存储(如数据库)的辅助存在,毕竟属于两套系统。理想情况下是缓存数据与数据库中数据完全一致,但是业务最常使用的旁路缓存架构下,在一些分布式或者高并发的场景中,可能会出现缓存不一致的情况。 在单线程场景下,如果更新缓存和更新数据库操作都是成功的,则可以保证数据库与缓存数据是一致的。但是在多线程场景下,由于由于更新缓存和更新数据库是两个操作,不具备原子性,就有可能出现多个并发请求交叉的情况,进而导致缓存和数据库中的记录不一致的情况。比如下面这个场景: 这种情况下,有很多的人会选择结合数据库的事务来一起控制。因为数据库有事务控制,而Redis等缓存没有事务性,所以会在一个DB事务中封装多个操作,比如先执行数据库操作,执行成功之后再进行缓存更新操作。这样如果缓存更新失败,则直接将当前数据库的事务回滚,企图用这种方式来保证缓存数据与DB数据的一致。 乍看似乎没毛病,但是细想一下,其实是有前提条件的。我们知道数据库事务的隔离级别有几种不同的类型,需要保证使用的事务隔离级别为Serializable或者Repeatable Read级别,以此来保证并发更新的场景下不会出现数据不一致问题,但这也降低了并发效率,提高数据库的cpu负载(隔离级别与并发性能存在一定的关联关系,见下图所示)。 所以对于一些读多写少、写操作并发竞争不是特别激烈且对一致性要求不是特别高的情况下,可以采用事务(高隔离级别) + 先更新数据库再更新缓存的方式来达到数据一致的诉求。 这种也会出现前面说的先操作成功,后操作失败的问题。 我们先看下先删除缓存再更新数据库的操作策略。如果先删除缓存成功,然后更新数据库失败,这种情况下,再次读取的时候,会从DB里面将旧数据重新加载回缓存中,数据是可以保持一致的。 (编辑:汽车网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |
推荐文章
站长推荐
