Redis作为缓存的使用以及使用注意事项

Redis使用的场景:

redis作为缓存使用时,我们会部署在数据库的前端,业务应用在访问数据时,会先查询 Redis 中是否保存了相应的数据。如果存在则命中缓存,读取redis中的数据,性能就会很快。如果redis中没有相应的数据,就从数据库中读取数据,性能就会变慢。同时需要将数据库中的数据回写入redis。这就是redis的应用场景。
说完了redis应用场景,接下来来介绍redis作为缓存的两种类型:
我们除了从 Redis 缓存中查询、读取数据以外,应用程序还可能会对数据进行修改,这时,我们既可以在缓存中修改,也可以在后端数据库中进行修改,我们该怎么选择呢?这时就涉及到了redis缓存的两种类型:只读缓存和读写缓存。只读缓存能加速读请求,而读写缓存可以同时加速读写请求。而且,读写缓存又有两种数据写回策略,可以让我们根据业务需求,在保证性能和保证数据可靠性之间进行选择。

redis缓存的类型:

只读缓存:

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

读写缓存:

对于读写缓存来说,除了读请求会发送到缓存进行处理,所有的写请求也会发送到缓存(注意此时没有发送给数据库),在缓存中直接对数据进行增删改操作。在使用读写缓存时,最新的数据是在 Redis 中,而 Redis 是内存数据库,一旦出现掉电或宕机,内存中的数据就会丢失。所以需要及时将数据更新到数据库当中。
所以根据业务应用对数据可靠性缓存性能的不同要求,我们会有同步直写异步写回两种策略。其中,同步直写策略优先保证数据可靠性,而异步写回策略优先提供快速响应

同步直写:

同步直写是指:写请求发给缓存的同时,也会发给后端数据库进行处理,等到缓存和数据库都写完数据,才给客户端返回。这样,即使缓存宕机或发生故障,最新的数据仍然保存在数据库中,这就提供了数据可靠性保证。
缺点:同步直写会降低缓存的访问性能。这是因为缓存中处理写请求的速度是很快的,而数据库处理写请求的速度较慢。当缓存中进行写处理完后,还要等待数据库处理完后,才能够将结果返回给客户端,这就增加了缓存的响应延迟。

异步写回:

异步写回策略,则是优先考虑了响应延迟。此时,所有写请求都先在缓存中处理。等到这些修改的数据要被从缓存中淘汰出来时,缓存将它们写回后端数据库。这样一来,处理这些数据的操作是在缓存中进行的,很快就能完成。只不过,如果发生了掉电,而它们还没有被写回数据库,就会有丢失的风险了。

只读缓存与读写缓存的选择:

如果需要对写请求进行加速,我们选择读写缓存;如果写请求很少,或者是只需要提升读请求的响应速度的话,就可以选择只读缓存。
Redis只读缓存和使用直写策略的读写缓存,这两种缓存都会把数据同步写到后端数据库中,它们的区别在于:

1、使用只读缓存时,是先把修改写到后端数据库中,再把缓存中的数据删除。当下次访问这个数据时,会以后端数据库中的值为准,重新加载到缓存中。这样做的优点是,数据库和缓存可以保证完全一致,并且缓存中永远保留的是经常访问的热点数据。缺点是每次修改操作都会把缓存中的数据删除,之后访问时都会先触发一次缓存缺失,然后从后端数据库加载数据到缓存中,这个过程访问延迟会变大。

2、使用读写缓存时,是同时修改数据库和缓存中的值。这样做的优点是,被修改后的数据永远在缓存中存在,下次访问时,能够直接命中缓存,不用再从后端数据库中查询,这个过程拥有比较好的性能,比较适合先修改又立即访问的业务场景。但缺点是在高并发场景下,如果存在多个操作同时修改同一个值的情况,可能会导致缓存和数据库的不一致。

3、当使用只读缓存时,如果修改数据库失败了,那么缓存中的数据也不会被删除,此时数据库和缓存中的数据依旧保持一致。而使用读写缓存时,如果是先修改缓存,后修改数据库,如果缓存修改成功,而数据库修改失败了,那么此时数据库和缓存数据就不一致了。如果先修改数据库,再修改缓存,也会产生上面所说的并发场景下的不一致。

缓存写满后的键淘汰策略:

Redis4.0版本后有8种淘汰策略:
noeviction:不进行数据淘汰。

在这里插入图片描述

LRU:

最近最久未使用,类比操作系统的页面置换算法,LRU 会把所有的数据组织成一个链表,链表的头和尾分别表示 MRU 端和 LRU 端,分别代表最近最常使用的数据和最近最不常用的数据。
举个例子:
如果有一个新数据 被写入缓存,但此时已经没有缓存空间了,也就是链表没有空余位置了,那么,LRU 算法做两件事,把数据放到MRU端,然后把lru尾端数据删除。

redis如何实现lru算法:

Redis 默认会记录每个数据的最近一次访问的时间戳(由键值对数据结构RedisObject 中的 lru 字段记录)。然后,Redis 在决定淘汰的数据时。
第一次会随机选出 N 个数据,把它们作为一个候选集合。接下来,Redis 会比较这 N 个数据的 lru 字段,把 lru 字段值最小的数据从缓存中淘汰出去。

配置参数 maxmemory-samples,这个参数就是 Redis 选出的数据个数 N。

当需要再次淘汰数据时,Redis 需要挑选数据放入第一次淘汰时创建的候选集合。这儿的挑选标准是:能进入候选集合的数据的 lru 字段值必须小于候选集合中最小的 lru 值。当有新数据进入候选数据集后,如果候选数据集中的数据个数达到了 maxmemory-samples,Redis 就把候选数据集中 lru 字段值最小的数据淘汰出去。
此时介绍完淘汰策略后,那么我们就要进行真正的淘汰。即删除key,如上文所述,如果采用异步写回,那么此时被淘汰的数据选定后,如果这个数据是干净数据,那么我们就直接删除;如果这个数据是脏数据,我们需要把它写回数据库。
但是需要注意的是:
Redis 来说,它决定了被淘汰的数据后,会把它们删除。即使淘汰的数据是脏数据,Redis 也不会把它们写回数据库。所以,我们在使用 Redis 缓存时,如果数据被修改了,需要在数据修改时就将它写回数据库。否则,这个脏数据被淘汰时,会被 Redis 删除,而数据库里也没有最新的数据了。所以redis在实际上没有异步直写,只有同步直写。也就是修改数据时,redis删除旧数据,业务层更新数据库。

volatile-ttl:

volatile-ttl 针对的是设置了过期时间的数据,把这些数据中剩余存活时间最短的筛选出来并淘汰掉


Redis在用作缓存时,使用只读缓存或读写缓存的哪种模式?

1、只读缓存模式:每次修改直接写入后端数据库,如果Redis缓存不命中,则什么都不用操作,如果Redis缓存命中,则删除缓存中的数据,待下次读取时从后端数据库中加载最新值到缓存中。

2、读写缓存模式+同步直写策略:由于Redis在淘汰数据时,直接在内部删除键值对,外部无法介入处理脏数据写回数据库,所以使用Redis作读写缓存时,只能采用同步直写策略,修改缓存的同时也要写入到后端数据库中,从而保证修改操作不被丢失。但这种方案在并发场景下会导致数据库和缓存的不一致,需要在特定业务场景下或者配合分布式锁使用。

当一个系统引入缓存时,需要面临最大的问题就是,如何保证缓存和后端数据库的一致性问题,最常见的3个解决方案分别是Cache Aside、Read/Write Throught和Write Back缓存更新策略。

1、Cache Aside策略:只读缓存模式。读操作命中缓存直接返回,否则从后端数据库加载到缓存再返回。写操作直接更新数据库,然后删除缓存。这种策略的优点是一切以后端数据库为准,可以保证缓存和数据库的一致性。缺点是写操作会让缓存失效,再次读取时需要从数据库中加载。这种策略是我们在开发软件时最常用的,在使用Memcached或Redis时一般都采用这种方案。

2、Read/Write Throught策略:应用层读写只需要操作缓存,不需要关心后端数据库。应用层在操作缓存时,缓存层会自动从数据库中加载或写回到数据库中,这种策略的优点是,对于应用层的使用非常友好,只需要操作缓存即可,缺点是需要缓存层支持和后端数据库的联动。

3、Write Back策略:读写缓存模式+异步写回策略。写操作只写缓存,比较简单。而读操作如果命中缓存则直接返回,否则需要从数据库中加载到缓存中,在加载之前,如果缓存已满,则先把需要淘汰的缓存数据写回到后端数据库中,再把对应的数据放入到缓存中。这种策略的优点是,写操作飞快(只写缓存),缺点是如果数据还未来得及写入后端数据库,系统发生异常会导致缓存和数据库的不一致。这种策略经常使用在操作系统Page Cache中,或者应对大量写操作的数据库引擎中。
----Kaito

LFU 缓存策略:

U 策略中会从两个维度来筛选并淘汰数据:一是,数据访问的时效性(访问时间离当前时间的远近);二是,数据的被访问次数。

工作原理:

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

实现原理:

Redis 在实现 LFU 策略的时候,只是把原来 24bit 大小的 lru 字段,又进一步拆分成了两部分。
ldt 值:lru 字段的前 16bit,表示数据的访问时间戳;
counter 值:lru 字段的后 8bit,表示数据的访问次数。
当 LFU 策略筛选数据时,Redis 会在候选集合中,根据数据 lru 字段的后 8bit 选择访问次数最少的数据进行淘汰。当访问次数相同时,再根据 lru 字段的前 16bit 值大小,选择访问时间最久远的数据进行淘汰。
LFU 策略实现的计数规则是:每当数据被访问一次时,首先,用计数器当前的值乘以配置项 lfu_log_factor 再加 1,再取其倒数,得到一个 p 值;然后,把这个 p 值和一个取值范围在(0,1)间的随机数 r 值比大小,只有 p 值大于 r 值时,计数器才加 1。这样可以延缓counter增长次数。
在一些场景下,有些数据在短时间内被大量访问后就不会再被访问了。那么再按照访问次数来筛选的话,这些数据会被留存在缓存中,但不会提升缓存命中率。为此,Redis 在实现 LFU 策略时,还设计了一个 counter 值的衰减机制。
LFU 策略使用衰减因子配置项 lfu_decay_time 来控制访问次数的衰减。LFU 策略会计算当前时间和数据最近一次访问时间的差值,并把这个差值换算成以分钟为单位。然后,LFU 策略再把这个差值除以 lfu_decay_time 值,所得的结果就是数据 counter 要衰减的值。
如果业务应用中有短时高频访问的数据的话,建议把 lfu_decay_time 值设置为 1,这样一来,LFU 策略在它们不再被访问后,会较快地衰减它们的访问次数,尽早把它们从缓存中淘汰出去,避免缓存污染。

缓存异常:

数据一致性:缓存中有数据,那么,缓存的数据值需要和数据库中的值相同;缓存中本身没有数据,那么,数据库中的值必须是最新值。不符合这两种情况的,就属于缓存和数据库的数据不一致问题。
而不一致问题的情况则与缓存读写模式相关,接下来我们来分别了解下不同模式下的缓存不一致情况。
同步直写策略:
同时更新缓存和数据库。所以,我们要在业务应用中使用事务机制,来保证缓存和数据库的更新具有原子性,也就是说,两者要不一起更新,要不都不更新,返回错误信息,进行重试。否则,我们就无法实现同步直写。
只读缓存:
如果有数据新增,会直接写入数据库;而有数据删改时,就需要把只读缓存中的数据标记为无效。这样一来,应用后续再访问这些增删改的数据时,因为缓存中没有相应的数据,就会发生缓存缺失。此时,应用再从数据库中把数据读入缓存,这样后续再访问数据时,就能够直接从缓存中读取了。
只读缓存不同操作:
新增数据:数据会直接写到数据库中,不用对缓存做任何操作,此时,缓存中本身就没有新增数据,而数据库中是最新值,这种情况符合我们刚刚所说的一致性的第 2 种情况,所以,此时,缓存和数据库的数据是一致的。
删改数据:
应用既要更新数据库,也要在缓存中删除数据。这两个操作如果无法保证原子性,也就是说,要不都完成,要不都没完成,此时,就会出现数据不一致问题了。
1:先删除缓存,再更新数据库,如果缓存删除成功,但是数据库更新失败,那么,应用再访问数据时,缓存中没有数据,就会发生缓存缺失。然后,应用再访问数据库,但是数据库中的值为旧值,应用就访问到旧值了。
2:先完成了数据库的更新,但是,在删除缓存时失败了,那么,数据库中的值是新值,而缓存中的是旧值,数据就会不一致了。
解决方法:
可以把要删除的缓存值或者是要更新的数据库值暂存到消息队列当中。当应用没有能够成功地删除缓存值或者是更新数据库值时,可以从消息队列中重新读取这些值,然后再次进行删除或更新。如果能够成功地删除或更新,我们就要把这些值从消息队列中去除。
上面这些情况,是异常发生的情况,下面介绍并发情况下的数据不一致性问题:
1 先删除缓存,再更新数据库:
线程 A 删除缓存值后,还没有来得及更新数据库(比如说有网络延迟),线程 B 就开始读取数据了,那么这个时候,线程 B 会发现缓存缺失,就只能去数据库读取。此时线程 B 读取到了旧值,线程 B 是在缓存缺失的情况下读取的数据库,所以,它还会把旧值写入缓存,这可能会导致其他线程从缓存中读到旧值。等到线程 B 从数据库读取完数据、更新了缓存后,线程 A 才开始更新数据库,此时,缓存中的数据是旧值,而数据库中的是最新值,两者就不一致了。
解决方案:
延迟双删:在线程 A 更新完数据库值以后,我们可以让它先 sleep 一小段时间,再进行一次缓存删除操作。即删除线程B写的旧值。
2 先更新数据库值,再删除缓存值。
如果线程 A 删除了数据库中的值,但还没来得及删除缓存值,线程 B 就开始读取数据了,那么此时,线程 B 查询缓存时,发现缓存命中,就会直接从缓存中读取旧值。不过,在这种情况下,如果其他线程并发读缓存的请求不多,那么,就不会有很多请求读取到旧值。而且,线程 A 一般也会很快删除缓存值,这样一来,其他线程再次读取时,就会发生缓存缺失,进而从数据库中读取最新值。所以,这种情况对业务的影响较小。

缓存雪崩:

缓存雪崩是指大量的应用请求无法在 Redis 缓存中进行处理,紧接着,应用将大量请求发送到数据库层,导致数据库层的压力激增。导致缓存雪崩原因可能有:
1:缓存中有大量数据同时过期,导致大量请求无法得到处理。
解决方案:
调整键过期时间,错开时间
采用服务降级,只允许核心数据查询缓存。
2:redis 缓存实例发生故障宕机了,无法处理请求,这就会导致大量请求一下子积压到数据库层,从而发生缓存雪崩。
解决方案:
1:服务熔断:业务应用调用缓存接口时,缓存客户端并不把请求发给 Redis 缓存实例,而是直接返回,缺点是虽然可以保证数据库的正常运行,暂停了整个缓存系统的访问
2:请求限流。这里说的请求限流,在业务系统的请求入口前端控制每秒进入系统的请求数,避免过多的请求被发送到数据库。

缓存击穿

缓存击穿是指,针对某个访问非常频繁的热点数据的请求,无法在缓存中进行处理,紧接着,访问该数据的大量请求,一下子都发送到了后端数据库,导致了数据库压力激增,会影响数据库处理其他请求。缓存击穿的情况,经常发生在热点数据过期失效时。
解决方案:
对于访问特别频繁的热点数据,我们就不设置过期时间了。这样一来,对热点数据的访问请求,都可以在缓存中进行处理,而 Redis 数万级别的高吞吐量可以很好地应对大量的并发请求访问。

缓存穿透

缓存穿透是指要访问的数据既不在 Redis 缓存中,也不在数据库中,导致请求在访问缓存时,发生缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据。此时,应用也无法从数据库中读取数据再写入缓存,来服务后续请求,这样一来,缓存也就成了“摆设”,如果应用持续有大量请求访问数据,就会同时给缓存和数据库带来巨大压力。缓存穿透发生场景:
1:业务层误操作:缓存中的数据和数据库中的数据被误删除了,所以缓存和数据库中都没有数据;
2:恶意攻击:专门访问数据库中没有的数据。
解决方案:
1:一旦发生缓存穿透,我们就可以针对查询的数据,在 Redis 中缓存一个空值或是和业务层协商确定的缺省值。
2:使用布隆过滤器快速判断数据是否存在,避免从数据库中查询数据是否存在,减轻数据库压力。

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值