0
点赞
收藏
分享

微信扫一扫

redis夺命连环问10--说说Redis是怎么做旁路缓存的?

巧乐兹_d41f 2022-02-05 阅读 139
redis

目录

相关前置知识文章


前置篇redis夺命连环问4–Redis内存满了怎么办?怎么优化?


说说Redis是怎么做旁路缓存的?


先谈缓存大概怎么做

Redis 缓存时,有三个操作:
应用读取数据时,需要先读取 Redis;
发生缓存缺失时,需要从数据库读取数据;
发生缓存缺失时,还需要更新缓存。


再谈旁路缓存两种模式

只读缓存
当 Redis 用作只读缓存时,应用要读取数据的话,会先调用 Redis GET 接口,查询数据是否存在。而所有的数据写请求,会直接发往后端的数据库,在数据库中增删改。对于删改的数据来说,如果 Redis 已经缓存了相应的数据,应用需要把这些缓存的数据删除,Redis 中就没有这些数据了。
当应用再次读取这些数据时,会发生缓存缺失,应用会把这些数据从数据库中读出来,并写到缓存中。这样一来,这些数据后续再被读取时,就可以直接从缓存中获取了,能起到加速访问的效果。
在这里插入图片描述

读写缓存
对于读写缓存来说,除了读请求会发送到缓存进行处理(直接在缓存中查询数据是否存在),所有的写请求也会发送到缓存,在缓存中直接对数据进行增删改操作。
根据业务应用对数据可靠性和缓存性能的不同要求,我们会有同步直写和异步写回两种策略。
在这里插入图片描述
同步直写模式侧重于保证数据可靠性,而异步写回模式则侧重于提供低延迟访问,我们要根据实际的业务场景需求来进行选择。


那怎么解决缓存和数据库的数据不一致问题?

啥是数据一致性?
“一致性”包含了两种情况:
缓存中有数据,那么,缓存的数据值需要和数据库中的值相同;
缓存中本身没有数据,那么,数据库中的值必须是最新值。

对于读写缓存来说
如果我们采用同步写回策略,那么可以保证缓存和数据库中的数据一致。
在有些场景下,我们对数据一致性的要求可能不是那么高,可以使用异步写回策略。

对于只读缓存来说

  • 如果是新增数据,数据会直接写到数据库中,不用对缓存做任何操作,此时,缓存中本身就没有新增数据,而数据库中是最新值。

  • 如果发生删改操作,应用既要更新数据库,也要在缓存中删除数据。这两个操作如果无法保证原子性,也就是说,要不都完成,要不都没完成,此时,就会出现数据不一致问题了。
    在这里插入图片描述
    如何解决数据不一致问题?
    分情况:

  • 删除失败导致的情况
    重试机制
    具体来说,可以把要删除或者要更新的值暂存到消息队列中(例如使用 Kafka 消息队列)。当应用没有能够成功地删除或更新值时,可以从消息队列中重新读取这些值,然后再次进行删除或更新。
    如果能够成功地删除或更新,我们就要把这些值从消息队列中去除,以免重复操作,如果重试超过的一定次数没成功,就报错。
    在这里插入图片描述

  • 删除没失败高并发导致的情况
    情况一:先删除缓存,再更新数据库。
    在这里插入图片描述
    解决方案——“延迟双删“:在线程 A 更新完数据库值以后,我们可以让它先 sleep 一小段时间,再进行一次缓存删除操作。之所以要加上 sleep 的这段时间,就是为了让线程 B 能够先从数据库读取数据,再把缺失的数据写入缓存,然后,线程 A 再进行删除。
    情况二:先更新数据库值,再删除缓存值。
    在这里插入图片描述
    除非redis挂了,否则这种场景还是很少的。


建议是,优先使用先更新数据库再删除缓存的方法,原因主要有两个:
1.先删除缓存值再更新数据库,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力;
2.如果业务应用中读取数据库和写缓存的时间不好估算,那么,延迟双删中的等待时间就不好设置。

在这里插入图片描述


如何解决缓存雪崩?

缓存雪崩:是指大量的应用请求无法在 Redis 缓存中进行处理,紧接着,应用将大量请求发送到数据库层,导致数据库层的压力激增。

原因
第一个原因是:缓存中有大量数据同时过期,导致大量请求无法得到处理。
在这里插入图片描述

解决

  • 微调过期时间:业务层的确要求有些数据同时失效,可以在用 EXPIRE 命令给每个数据设置过期时间时,给这些数据的过期时间增加一个较小的随机数,既避免了大量数据同时过期,同时也保证了这些数据基本在相近的时间失效,仍然能满足业务需求。
  • 服务降级:
    当业务应用访问的是非核心数据时,暂时停止从缓存中查询这些数据,而是直接返回预定义信息、空值或是错误信息;
    当业务应用访问的是核心数据时,仍然允许查询缓存,如果缓存缺失,也可以继续通过数据库读取,数据库压力没那么大。
    在这里插入图片描述
    第二个原因:Redis 缓存实例发生故障宕机了,无法处理请求
    解决
  • 第一个建议,是在业务系统中实现服务熔断或请求限流机制。(事后诸葛亮)
    在这里插入图片描述

在这里插入图片描述

  • 第二个建议:通过主从节点的方式构建 Redis 缓存高可靠集群。如果 Redis 缓存的主节点故障宕机了,从节点还可以切换成为主节点,继续提供缓存服务,避免了由于缓存实例宕机而导致的缓存雪崩问题。(预防)

如何解决缓存击穿?

缓存击穿:是指针对某个访问非常频繁的热点数据的请求,无法在缓存中进行处理,紧接着访问该数据的大量请求,一下子都发送到了后端数据库,导致了数据库压力激增,会影响数据库处理其他请求。缓存击穿的情况,经常发生在热点数据过期失效时。
在这里插入图片描述
解决
对于访问特别频繁的热点数据,不设置过期时间。


如何解决缓存穿透?

缓存穿透:是指要访问的数据既不在 Redis 缓存中,也不在数据库中,导致请求在访问缓存时,发生缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据。一般是业务层误删操作或恶意攻击导致。
在这里插入图片描述
解决
第一种方案是,缓存空值或缺省值。

第二种方案是,使用布隆过滤器快速判断数据是否存在,避免从数据库中查询数据是否存在,减轻数据库压力。

第三种方案是,在请求入口的前端进行请求检测。把恶意的请求直接过滤掉,不让它们访问后端缓存和数据库。


刚刚你说到了布隆过滤器,能具体说说吗?

布隆过滤器:由一个初值都为 0 的 bit 数组和 N 个哈希函数组成,可以用来快速判断某个数据是否存在。当我们想标记某个数据存在时,布隆过滤器会通过三个操作完成标记:

  • 首先,使用 N 个哈希函数,分别计算这个数据的哈希值,得到 N 个哈希值。
  • 然后,我们把这 N 个哈希值对 bit 数组的长度取模,得到每个哈希值在数组中的对应位置。
  • 最后,我们把对应位置的 bit 位设置为 1,这就完成了在布隆过滤器中标记数据的操作。

过程:当需要查询某个数据时,我们就执行刚刚说的计算过程,先得到这个数据在 bit 数组中对应的 N 个位置。紧接着,我们查看 bit 数组中这 N 个位置上的 bit 值。只要这 N 个 bit 值有一个不为 1,这就表明布隆过滤器没有对该数据做过标记,所以,查询的数据一定没有在数据库中保存。
在这里插入图片描述
当缓存缺失后,应用查询数据库时,可以通过查询布隆过滤器快速判断数据是否存在。如果不存在,就不用再去数据库中查询了。


从问题成因来看,缓存雪崩和击穿主要是因为数据不在缓存中了,而缓存穿透则是因为数据既不在缓存中,也不在数据库中。所以,缓存雪崩或击穿时,一旦数据库中的数据被再次写入到缓存后,应用又可以在缓存中快速访问数据了,数据库的压力也会相应地降低下来,而缓存穿透发生时,Redis 缓存和数据库会同时持续承受请求压力。
在这里插入图片描述
建议:尽量使用预防式方案:
针对缓存雪崩,合理地设置数据过期时间,以及搭建高可靠缓存集群;
针对缓存击穿,在缓存访问非常频繁的热点数据时,不要设置过期时间;
针对缓存穿透,提前在入口前端实现恶意请求检测,或者规范数据库的数据删除操作,避免误删除。


如何解决缓存污染?

缓存污染是啥:数据服务完访问请求后很少被访问了,却还留存在缓存中占用缓存空间。

除了在明确知道数据被再次访问的情况下,volatile-ttl 可以有效避免缓存污染。在其他情况下,volatile-random、allkeys-random、volatile-ttl 这三种策略并不能应对缓存污染问题。

LRU缓存策略:会在每个数据对应的 RedisObject 结构体中设置一个 lru 字段,用来记录数据的访问时间戳。在进行数据淘汰时,LRU 策略会在候选数据集中淘汰掉 lru 字段值最小的数据(也就是访问时间最久的数据)。

也正是因为只看数据的访问时间,使用 LRU 策略在处理扫描式单次查询操作时,无法解决缓存污染。

LFU 缓存策略的优化:LFU 缓存策略是在 LRU 策略基础上,为每个数据增加了一个计数器统计访问次数。筛选淘汰数据时,首先会根据数据的访问次数进行筛选,把访问次数最低的数据淘汰出缓存。如果两个数据的访问次数相同,LFU 策略再比较这两个数据的访问时效性,把距离上一次访问时间更久的数据淘汰出缓存。

LRU 策略更加关注数据的时效性,而 LFU 策略更加关注数据的访问频次。通常情况下,实际应用的负载具有较好的时间局部性,所以 LRU 策略的应用会更加广泛。但是,在扫描式查询的应用场景中,LFU 策略就可以很好地应对缓存污染问题了,建议你优先使用。

实现
为了避免操作链表的开销,Redis 在实现 LRU 策略时使用了两个近似方法:

  • Redis 是用 RedisObject 结构来保存数据的,RedisObject 结构中设置了一个 lru 字段,用来记录数据的访问时间戳;
  • Redis 并没有为所有的数据维护一个全局的链表,而是通过随机采样方式,选取一定数量(例如 10 个)的数据放入候选集合,后续在候选集合中根据 lru 字段值的大小进行筛选。

Redis 在实现 LFU 策略的时候,只是把原来 24bit 大小的 lru 字段,又进一步拆分成了两部分。

  • ldt 值:lru 字段的前 16bit,表示数据的访问时间戳;
  • counter 值:lru 字段的后 8bit,表示数据的访问次数。

当 LFU 策略筛选数据时,Redis 会在候选集合中,根据数据 lru 字段的后 8bit 选择访问次数最少的数据进行淘汰。当访问次数相同时,再根据 lru 字段的前 16bit 值大小,选择访问时间最久远的数据进行淘汰。

使用了非线性递增的计数器方法,即使缓存数据的访问次数成千上万,LFU 策略也可以有效地区分不同的访问次数,从而进行合理的数据筛选。
Redis 在实现 LFU 策略时,还设计了一个 counter 值的衰减机制。LFU 策略在数据不再被访问后,会较快地衰减它们的访问次数,尽早把它们从缓存中淘汰出去,避免缓存污染。

建议:你可以优先使用 volatile-lfu 策略,并根据这些数据的访问时限设置它们的过期时间,以免它们留存在缓存中造成污染。

举报

相关推荐

0 条评论