Redis缓存的一些理解
之前简单的学习过Redis,但只是简单的增删改查,最近在看极客时间关于Redis的专栏(《Redis核心技术与实战》),有不少的收获,记录一下
一.工作原理
1.为什么redis适合做缓存?
缓存的两个特征,分别是可以快速访问;缓存写满时,数据需要被淘汰。而 Redis 天然就具有高性能访问和数据淘汰机制,正好符合缓存的这两个特征的要求,所以非常适合用作缓存。
2.redis做缓存的两种模式
只读缓存和读写缓存,
读写缓存提供了同步直写和异步写回这两种模式,
同步直写模式侧重于保证数据可靠性,
而异步写回模式则侧重于提供低延迟访问,
我们要根据实际的业务场景需求来进行选择。
举个例子,在商品大促的场景中,商品的库存信息会一直被修改。如果每次修改都需到数据库中处理,就会拖慢整个应用,此时,我们通常会选择读写缓存的模式。而在短视频 App 的场景中,虽然视频的属性有很多,但是,一般确定后,修改并不频繁,此时,在数据库中进行修改对缓存影响不大,所以只读缓存模式是一个合适的选择。
3.只读缓存和使用直写策略的读写缓存有什么区别吗?
只读缓存是牺牲了一定的性能,优先保证数据库和缓存的一致性,它更适合对于一致性要求比较要高的业务场景。
而如果对于数据库和缓存一致性要求不高,或者不存在并发修改同一个值的情况,那么使用读写缓存就比较合适,它可以保证更好的访问性能。
二.替换策略
图片来自极客专栏《Redis核心技术与实战》
1.主要是注意LRU算法和LFU算法
LRU算法:
LRU算法是把所有的数据放在一个列表里,然后两端分别为MRU端,LRU端,当某个位置的数据被访问或者是添加了某个新数据的时候,会被移动到MRU端,如果在存储空间满了的情况下,新进来的数据会被放在MRU端,LRU端的一个数据会被淘汰.
但是LRU算法存在两个问题:1.用链表存储,会有额外的内存空间开销;2.操作过程中会有数据移动,如果数据量过大,会很费时间,降低redis的缓存性能.
Redis修改了LRU算法:
Redis会记录每个数据最近被访问的时间戳(由键值对数据结构RedisObject里面的lru记录),在Redis淘汰数据的时候,会随机选出N个数据(maxmemory-samples)组成候选集,将lru最小的数据淘汰,(注意是随机,这是一种局部最优解,所以会有过期很久但是没有删除的数据,但是性能提高了)
当第二次要淘汰数据的时候,Redis会再选一批数据进入第一次选出来的那个候选集合(能进入候选集合的必须是lru值比当前候选集合里最小的lru值还小的数据),当数据量达到maxmemory-samples时,Redis会把lru值最小的数据淘汰.
2.不考虑LFU算法,其他算法如何选择?
答案1:优先使用 allkeys-lru 策略。这样,可以充分利用 LRU 这一经典缓存算法的优势,把最近最常访问的数据留在缓存中,提升应用的访问性能。如果你的业务数据中有明显的冷热数据区分,我建议你使用 allkeys-lru 策略。
如果业务应用中的数据访问频率相差不大,没有明显的冷热数据区分,建议使用 allkeys-random 策略,随机选择淘汰的数据就行。
如果你的业务中有置顶的需求,比如置顶新闻、置顶视频,那么,可以使用 volatile-lru 策略,同时不给这些置顶数据设置过期时间。这样一来,这些需要置顶的数据一直不会被删除,而其他数据会在过期时根据 LRU 规则进行筛选。
答案2:先根据是否有始终会被频繁访问的数据(例如置顶消息),来选择淘汰数据的候选集,也就是决定是针对所有数据进行淘汰,还是针对设置了过期时间的数据进行淘汰。候选数据集范围选定后,建议优先使用 LRU 算法,也就是,allkeys-lru 或 volatile-lru 策略。
当然,设置缓存容量的大小也很重要,我的建议是:结合实际应用的数据总量、热数据的体量,以及成本预算,把缓存空间大小设置在总数据量的 15% 到 30% 这个区间就可以。
3.哪些数据要被淘汰?
要看采用哪种淘汰策略.
4.被淘汰的数据怎样处理?
一般来说,如果是干净的数据直接处理,如果是脏数据则写回数据库,但是Redis规定只要是淘汰的数据一定会被删除.
所以一般对缓存的修改,要在修改时写入数据库.
5.怎样判断数据是干净数据还是脏数据?
脏数据是曾经修改过,和数据库的数据不一样.
6.当一个系统引入缓存时,需要面临最大的问题就是,如何保证缓存和后端数据库的一致性问题
最常见的3个解决方案分别是Cache Aside、Read/Write Throught和Write Back缓存更新策略。
1、Cache Aside策略:就是文章所讲的只读缓存模式。读操作命中缓存直接返回,否则从后端数据库加载到缓存再返回。写操作直接更新数据库,然后删除缓存。这种策略的优点是一切以后端数据库为准,可以保证缓存和数据库的一致性。缺点是写操作会让缓存失效,再次读取时需要从数据库中加载。这种策略是我们在开发软件时最常用的,在使用Memcached或Redis时一般都采用这种方案。
2、Read/Write Throught策略:应用层读写只需要操作缓存,不需要关心后端数据库。应用层在操作缓存时,缓存层会自动从数据库中加载或写回到数据库中,这种策略的优点是,对于应用层的使用非常友好,只需要操作缓存即可,缺点是需要缓存层支持和后端数据库的联动。
3、Write Back策略:类似于文章所讲的读写缓存模式+异步写回策略。写操作只写缓存,比较简单。而读操作如果命中缓存则直接返回,否则需要从数据库中加载到缓存中,在加载之前,如果缓存已满,则先把需要淘汰的缓存数据写回到后端数据库中,再把对应的数据放入到缓存中。这种策略的优点是,写操作飞快(只写缓存),缺点是如果数据还未来得及写入后端数据库,系统发生异常会导致缓存和数据库的不一致。这种策略经常使用在操作系统Page Cache中,或者应对大量写操作的数据库引擎中。
7.操作缓存或数据库发生异常时如何处理?例如缓存操作成功,数据库操作失败,或者反过来,还是有可能会产生不一致的情况。
解决方案是,根据业务设计好更新缓存和数据库的先后顺序来降低影响,或者给缓存设置较短的有效期来降低不一致的时间。如果需要严格保证缓存和数据库的一致性,即保证两者操作的原子性,这就涉及到分布式事务问题了,常见的解决方案就是我们经常听到的两阶段提交(2PC)、三阶段提交(3PC)、TCC、消息队列等方式来保证了,方案也会比较复杂,一般用在对于一致性要求较高的业务场景中。
三.异常处理
缓存异常一般有4个问题分别是:
1.缓存中的数据和数据库中的数据不一致问题;
数据不一致的原因有两条:
删除缓存值或更新数据库失败而导致数据不一致,你可以使用重试机制确保删除或更新操作成功。
在删除缓存值、更新数据库的这两步操作中,有其他线程的并发读操作,导致其他线程读取到旧值,应对方案是延迟双删。
第一种原因
删改:既要在数据库删改,又要在缓存中删除,主要考虑的就是这种情况;
图片来自极客专栏《Redis核心技术与实战》
解决方法是重试机制:
将要删改的操作放入消息队列,如下图,如果删除失败,从消息队列中取出,再去执行,如果多次失败,就向业务层发送报错信息;
图片来自极客专栏《Redis核心技术与实战》
第二种原因
即使这两个操作第一次执行时都没有失败,当有大量并发请求时,应用还是有可能读到不一致的数据。
情况一:先删除缓存,再更新数据库。
图片来自极客专栏《Redis核心技术与实战》
在线程 A 更新完数据库值以后,我们可以让它先 sleep 一小段时间,再进行一次缓存删除操作。之所以要加上 sleep 的这段时间,就是为了让线程 B 能够先从数据库读取数据,再把缺失的数据写入缓存,然后,线程 A 再进行删除。所以,线程 A sleep 的时间,就需要大于线程 B 读取数据再写入缓存的时间。这个时间怎么确定呢?建议你在业务程序运行的时候,统计下线程读数据和写缓存的操作时间,以此为基础来进行估算。这样一来,其它线程读取数据时,会发现缓存缺失,所以会从数据库中读取最新值。因为这个方案会在第一次删除缓存值后,延迟一段时间再次进行删除,所以我们也把它叫做“延迟双删”。
情况二:先更新数据库值,再删除缓存值。
如果线程 A 删除了数据库中的值,但还没来得及删除缓存值,线程 B 就开始读取数据了,那么此时,线程 B 查询缓存时,发现缓存命中,就会直接从缓存中读取旧值。不过,在这种情况下,如果其他线程并发读缓存的请求不多,那么,就不会有很多请求读取到旧值。而且,线程 A 一般也会很快删除缓存值,这样一来,其他线程再次读取时,就会发生缓存缺失,进而从数据库中读取最新值。所以,这种情况对业务的影响较小。
图片来自极客专栏《Redis核心技术与实战》
总结:
缓存和数据库不一致的问题。针对这个问题,我们可以分成读写缓存和只读缓存两种情况进行分析。对于读写缓存来说,如果我们采用同步写回策略,那么可以保证缓存和数据库中的数据一致。只读缓存的情况比较复杂,我总结了一张表,以便于你更加清晰地了解数据不一致的问题原因、现象和应对方案。
2.缓存雪崩;
表现:大量的应用请求无法在 Redis 缓存中进行处理,紧接着,应用将大量请求发送到数据库层,导致数据库层的压力激增。
诱发原因:1.大量缓存数据同时失效;2.实例宕机
解决方法:
对于第一种原因:
1.尽量设置过期时间不要在同一时间,如果必须在同一时间,可以对时间做微调(给时间加一个很小的随机数);
2.服务降级,如果访问非核心数据,暂时停止从缓存种查询这些数据,返回预定义的信息;如果是核心数据,可以查缓存或者数据库;
对于第二种原因:
1.服务熔断或者请求限流
服务熔断就是暂停业务应用对缓存接口的访问,就是请求不会到达redis实例,而是直接返回,等redis实例恢复之后再允许访问
请求限流就是在业务系统的请求入口前端控制每秒进入系统的请求数,避免过多请求被发送到数据库;
2.提前预防,用主从方式的redis缓存高可用集群,主库宕机之后从库可以继续提供缓存服务;
3.缓存击穿;
表现:缓存击穿是指,针对某个访问非常频繁的热点数据的请求,无法在缓存中进行处理,紧接着,访问该数据的大量请求,一下子都发送到了后端数据库,导致了数据库压力激增,会影响数据库处理其他请求。
诱发原因:热点数据过期失效
解决方法:对于访问频繁的热点数据,不设置过期时间
4.缓存穿透;
表现:缓存穿透是指要访问的数据既不在 Redis 缓存中,也不在数据库中,导致请求在访问缓存时,发生缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据。
诱发原因:
业务层误操作:缓存中的数据和数据库中的数据被误删除了,所以缓存和数据库中都没有数据;
恶意攻击:专门访问数据库中没有的数据。
解决方法:
1.缓存空值或者缺省值
2.使用布隆过滤器判断数据是否存在
3.在请求入口的前端进行请求检查
总结:
图片来自极客专栏《Redis核心技术与实战》
尽量使用预防式方案:
缓存雪崩,合理地设置数据过期时间,以及搭建高可靠缓存集群;
缓存击穿,在缓存访问非常频繁的热点数据时,不要设置过期时间;
缓存穿透,提前在入口前端实现恶意请求检测,或者规范数据库的数据删除操作,避免误删除。
扩展:缓存污染
什么是缓存污染?
在一些场景下,有些数据被访问的次数非常少,甚至只会被访问一次。当这些数据服务完访问请求后,如果还继续留存在缓存中的话,就只会白白占用缓存空间。这种情况,就是缓存污染。
怎样解决缓存污染?
解决缓存污染就是将不会再被访问的数据从缓存里删除,而删除哪些又要看淘汰策略
LRU策略可以解决,但是LRU策略在扫描式单次查询时会造成缓存污染,所以redis增加了LFU策略;
volatile-random 和 allkeys-random 是随机选择数据进行淘汰,无法把不再访问的数据筛选出来,可能会造成缓存污染。如果业务层明确知道数据的访问时长,可以给数据设置合理的过期时间,再设置 Redis 缓存使用 volatile-ttl 策略。当缓存写满时,剩余存活时间最短的数据就会被淘汰出缓存,避免滞留在缓存中,造成污染。
当我们使用 LRU 策略时,由于 LRU 策略只考虑数据的访问时效,对于只访问一次的数据来说,LRU 策略无法很快将其筛选出来。而 LFU 策略在 LRU 策略基础上进行了优化,在筛选数据时,首先会筛选并淘汰访问次数少的数据,然后针对访问次数相同的数据,再筛选并淘汰访问时间最久远的数据。
在具体实现上,相对于 LRU 策略,Redis 只是把原来 24bit 大小的 lru 字段,又进一步拆分成了 16bit 的 ldt 和 8bit 的 counter,分别用来表示数据的访问时间戳和访问次数。为了避开 8bit 最大只能记录 255 的限制,LFU 策略设计使用非线性增长的计数器来表示数据的访问次数。
在实际业务应用中,LRU 和 LFU 两个策略都有应用。LRU 和 LFU 两个策略关注的数据访问特征各有侧重,LRU 策略更加关注数据的时效性,而 LFU 策略更加关注数据的访问频次。通常情况下,实际应用的负载具有较好的时间局部性,所以 LRU 策略的应用会更加广泛。但是,在扫描式查询的应用场景中,LFU 策略就可以很好地应对缓存污染问题了,建议你优先使用。
此外,如果业务应用中有短时高频访问的数据,除了 LFU 策略本身会对数据的访问次数进行自动衰减以外,我再给你个小建议:你可以优先使用 volatile-lfu 策略,并根据这些数据的访问时限设置它们的过期时间,以免它们留存在缓存中造成污染。
四.扩展机制
大内存 Redis 实例的潜在问题内存快照 RDB 生成和恢复效率低,以及主从节点全量同步时长增加、缓冲区易溢出。
内存快照 RDB 生成和恢复效率低,以及主从节点全量同步时长增加、缓冲区易溢出。
所以可以使用SSD,SSD成本低,内存大,访问速度快。