栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

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

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

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

目录

相关前置知识文章说说Redis是怎么做旁路缓存的?

先谈缓存大概怎么做再谈旁路缓存两种模式那怎么解决缓存和数据库的数据不一致问题?如何解决缓存雪崩?如何解决缓存击穿?如何解决缓存穿透?刚刚你说到了布隆过滤器,能具体说说吗?如何解决缓存污染?

相关前置知识文章

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


说说Redis是怎么做旁路缓存的?
先谈缓存大概怎么做

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


再谈旁路缓存两种模式

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

读写缓存:
对于读写缓存来说,除了读请求会发送到缓存进行处理(直接在缓存中查询数据是否存在),所有的写请求也会发送到缓存,在缓存中直接对数据进行增删改操作。
根据业务应用对数据可靠性和缓存性能的不同要求,我们会有同步直写和异步写回两种策略。

同步直写模式侧重于保证数据可靠性,而异步写回模式则侧重于提供低延迟访问,我们要根据实际的业务场景需求来进行选择。


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

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

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

对于只读缓存来说

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

如果发生删改操作,应用既要更新数据库,也要在缓存中删除数据。这两个操作如果无法保证原子性,也就是说,要不都完成,要不都没完成,此时,就会出现数据不一致问题了。

如何解决数据不一致问题?
分情况:

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

删除没失败高并发导致的情况
情况一:先删除缓存,再更新数据库。

解决方案:
一:延迟双删:在线程 A 更新完数据库值以后,我们可以让它先 sleep 一小段时间,再进行一次缓存删除操作。之所以要加上 sleep 的这段时间,就是为了让线程 B 能够先从数据库读取数据,再把缺失的数据写入缓存,然后,线程 A 再进行删除。
二:更新与读取操作进行异步串行化:–这个我看不懂,拉倒吧这个

1、异步串行化
我在系统内部维护n个内存队列,更新数据的时候,根据数据的唯一标识,将该操作路由之后,发送到其中一个jvm内部的内存队列中(对同一数据的请求发送到同一个队列)。读取数据的时候,如果发现数据不在缓存中,并且此时队列里有更新库存的操作,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,也将发送到同一个jvm内部的内存队列中。然后每个队列对应一个工作线程,每个工作线程串行地拿到对应的操作,然后一条一条的执行。
这样的话,一个数据变更的操作,先执行删除缓存,然后再去更新数据库,但是还没完成更新的时候,如果此时一个读请求过来,读到了空的缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,排在刚才更新库的操作之后,然后同步等待缓存更新完成,再读库。

2、读操作去重
多个读库更新缓存的请求串在同一个队列中是没意义的,因此可以做过滤,如果发现队列中已经有了该数据的更新缓存的请求了,那么就不用再放进去了,直接等待前面的更新操作请求完成即可,待那个队列对应的工作线程完成了上一个操作(数据库的修改)之后,才会去执行下一个操作(读库更新缓存),此时会从数据库中读取最新的值,然后写入缓存中。
如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回;如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值。(返回旧值不是又导致缓存和数据库不一致了么?那至少可以减少这个情况发生,因为等待超时也不是每次都是,几率很小吧。这里我想的是,如果超时了就直接读旧值,这时候仅仅是读库后返回而不放缓存)

情况二:先更新数据库值,再删除缓存值。

除非redis挂了,否则这种场景还是很少的。
解决:
也可用重试机制(Kafka消息队列),
但是这个方案会有一个缺点就是会对业务代码造成大量的侵入,深深的耦合在一起,所以这时会有一个优化的方案,我们知道对 Mysql 数据库更新操作后再 binlog 日志中我们都能够找到相应的操作,那么我们可以订阅 Mysql 数据库的 binlog 日志对缓存进行操作


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


如何解决缓存雪崩?

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

原因:
第一个原因是:缓存中有大量数据同时过期,导致大量请求无法得到处理。

解决:

微调过期时间:业务层的确要求有些数据同时失效,可以在用 EXPIRE 命令给每个数据设置过期时间时,给这些数据的过期时间增加一个较小的随机数,既避免了大量数据同时过期,同时也保证了这些数据基本在相近的时间失效,仍然能满足业务需求。数据预热:可以通过缓存 reload 机制,预先去更新缓存,再即将发生大并发访问前手动触发加载缓存不同的 key服务降级:
当业务应用访问的是非核心数据时,暂时停止从缓存中查询这些数据,而是直接返回预定义信息、空值或是错误信息;
当业务应用访问的是核心数据时,仍然允许查询缓存,如果缓存缺失,也可以继续通过数据库读取,数据库压力没那么大。

第二个原因:Redis 缓存实例发生故障宕机了,无法处理请求
解决:

第一个建议,是在业务系统中实现服务熔断或请求限流机制。(事后诸葛亮)
加锁排队:在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个 key 只允许一个线程查询数据和写缓存,其他线程等待;

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

其他解决方案:

做二级缓存,或者双缓存策略:Cache1 为原始缓存,Cache2 为拷贝缓存,Cache1 失效时,可以访问 Cache2,Cache1 缓存失效时间设置为短期,Cache2 设置为长期。


如何解决缓存击穿?

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

解决:
对于访问特别频繁的热点数据,不设置过期时间。


如何解决缓存穿透?

缓存穿透:是指要访问的数据既不在 Redis 缓存中,也不在数据库中,导致请求在访问缓存时,发生缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据。一般是业务层误删操作或恶意攻击导致。

解决:
第一种方案是,缓存空值或缺省值。

缓存空对象带来的问题:
1、空值做了缓存,意味着缓存中存了更多的键,需要更多的内存空间,比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。
2、缓存和存储的数据会有一段时间窗口的不一致,可能会对业务有一定影响。例如:过期时间设置为 5分钟,如果此时存储添加了这个数据,那此段时间就会出现缓存和存储数据的不一致,此时可以利用消息系统或者其他方式清除掉缓存层中的空对象。

第二种方案是,布隆过滤器:将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力。

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

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


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

布隆过滤器:由一个初值都为 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 策略,并根据这些数据的访问时限设置它们的过期时间,以免它们留存在缓存中造成污染。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/726192.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号