Redis缓存

Redis作为缓存的普遍性,以及它在业务应用中的重要作用,需要系统了解缓存的一些列内容,包含工作原理、替换策略、异常处理等。

1. Redis为什么适合用于缓存?

一个系统中的不同层之间的访问速度不一样,才需要缓存。这样可以把需要频繁访问的数据放在缓存中,以加快它们的访问速度。

在计算机系统中,默认有两种缓存:

  • CPU的末级缓存,即LLC,用来缓存内存中的数据,避免每次从内存中取数据。
  • 内存中的高速页缓存,即page cache,用来缓存磁盘中的数据,避免每次从磁盘中获取数据。

跟内存相比,LLC的访问速度更快,跟磁盘相比,内存的访问更快,可以得出第一个特征:在一个层次化的系统中,缓存一定是一个快速的子系统,数据存在缓存中时,能避免每次从慢速子系统中存取数据。在应用系统上,Redis是快速子系统,数据库是慢速子系统。

LLC的大小是MB级别,page cache的大小是MB级别,而磁盘的大小是TB级别,可以得出第二个特征:缓存系统的容量大小总是小于后端慢速系统,不可能把所有数据都放在缓存系统中。

2. 缓存策略

2.1 旁路缓存策略

Cache Aside

读取缓存、读取数据库和更新缓存操作需要在应用程序中完成。Redis缓存就属于旁路缓存。

2.2 读穿/写穿策略

Read/Write Through

应用程序只和缓存交互,不在和数据库交互。由缓存才能和数据库交互,相当于读取和更新数据库的操作由缓存代理了。

在开发过程中,相比Cache Aside策略要少见,主要因为分布式组件,如Redis不提供写数据库和加载数据库数据的功能。在使用本地缓存的时候,经常会用这种策略。

2.3 写回策略

Write Back

在更新数据的时候,只更新缓存,同时将缓存数据设置为脏数据,然后返回,不会更新数据库。对于数据库的更新,通过批量异步更新的方式进行。实际上该策略也不能应用到常用的数据库和缓存的场景中,因为redis并没有异步更新数据库的功能。

Write back是计算机系统结构中的设计,如CPU的缓存,Page Cache都采用了Write Back策略。

Write Back策略特别适合写多的场景,因为发生写操作,只更新缓存就可以。但带来的问题是数据不是强一致,而且有数据丢失的风险。因为缓存一般使用内存,内存是非持久化的,一旦缓存机器断电,会造成缓存数据丢失。比如:写入文件在断电的时候会有部分丢失,就是因为page cache还没来记得刷盘导致。

3. 缓存类型

按照Redis缓存是否接受写请求,可以把它分成只读缓存和读写缓存。

3.1 只读缓存

应用要读取数据的话,会先调用Redis GET接口,查询数据是否存在。而所有数据的写请求,会直接发往后端的数据库,在数据库中增删改,对于删改的数据来说,Redis已经缓存了对应数据,所以应用需要把这些缓存数据进行删除,Redis中就没有这些数据了。

当应用再次读取这些数据是,会发生缓存缺失,应用会把这些数据从数据库读出来,写到Redis缓存中。这些数据再次被读取是,就可以直接从缓存中获取了。

只读缓存在数据库中更新数据的好处:所有最新的数据都在数据库中,而数据库提供数据可靠性保障,这些数据不会有丢失的风险。例如:图片、视频这些用户只读的数据,就可以用只读缓存的类型。

3.2 读写缓存

对于读写缓存来说,除了读请求会发送到缓存处理,所有的写请求也会发送到缓存,在缓存中直接对数据进行增删改操作。得益于Redis的高性能访问特性,数据的增删改操作可以在缓存中快速完成,可以提高业务应用的响应速度。

但是,使用读写缓存时,最新的数据是在Redis,而Redis是内存数据库,一旦出现断点或宕机,内存的数据就会丢失。也就是应用的最新数据可能会丢失,给业务带来风险。

根据业务应用对数据的可靠性和缓存性能的不同要求,会有同步直写和异步回写两种策略。同步直写策略优先保证数据可靠性,而异步写回策略优先提供快速响应。

3.2.1 同步直写

写请求发送给缓存的同时,也会发送给数据库处理,等缓存和数据库都写完数据,才给客户端返回。这样即使缓存宕机,最新的数据仍然在数据库,提供了数据可靠性保证。

但是这样会降低缓存的访问性能,因为缓存处理写请求的速度很快,而数据库处理写请求速度较慢,整体上导致响应较慢。

3.2.2 异步回写

所有写请求都在缓存中处理,等到增改的数据被缓存中淘汰时,再将它们写回到数据库。如果发生宕机,数据还没写回数据库,会有丢失的风险。

3.3 总结

选择只读缓存还是读写缓存,主要看对写请求是否有加速的需求:

  • 如果需要对写请求加速,选择读写缓存。
  • 如果写请求很少,或者只需要提升读请求的响应速度,选择只读缓存。

3.4 只读缓存 VS 读写缓存(同步直写)

只读缓存和读写缓存中的同步直写有类似操作,下面进行分析差别

只读缓存:

  • 优点:数据库和缓存可以保持完全一致,并且缓存中永远保留经常访问的热点数据。
  • 缺点:每次修改操作都会把缓存的数据删除,会触发一次缓存缺失,到数据库获取数据,这个访问延迟会变大。

读写缓存:

  • 优点:修改后的数据永远在缓存中存在,再次访问能够直接命中缓存,不必从数据库查询,有较好的性能,适合修改又立即访问的业务场景。
  • 缺点:在高并发场景下,存在多个操作同时修改一个值得情况下,可能会导致缓存与数据库不一致。

数据库修改失败场景:

  • 只读缓存:如果数据库修改失败,缓存的数据库不会被删除,数据库和缓存仍然保持一致。
  • 读写缓存:先修改缓存,再修改数据库,如果缓存修改成功,数据库修改失败,数据库和缓存数据就会不一致。如果先修改数据库,再修改缓存,并发场景下可能会导致缓存与数据库不一致。

小结:

  • 只读缓存:牺牲了一定性能,优先保证数据库和缓存的一致性,更适合对于一致性要求比较高的业务场景。
  • 读写缓存:对于数据库和缓存一致性要求不高,或者不存在并发修改同一个值的情况,使用读写缓存可以保证更好的访问性能。

4. 缓存与数据库不一致

对于读写缓存来说,想要保证缓存和数据库的数据一致,必须要采用同步直写策略。 并且要使用事务机制,保证更新缓存和数据库具有原子性,要么都成功,要么都失败。

只读缓存会有问题,主要在删改数据的时候。根据几种情况,将场景整理如下:

操作 是否有并发请求 潜在问题 现象 应对方案
先删缓存,再更新数据库 缓存删除成功,但数据库更新失败 应用从数据库读到数旧数据 数据库没更新成功,业务上也失败,是正常的场景。
先删缓存,再更新数据库 缓存删除后,还没更新数据库,有并发请求 并发请求从数据库读取旧数据,并更新到缓存,导致后续请求读取到了旧数据 延迟双删
先更新数据库,再删缓存 数据库更新成功,但是缓存删除失败 应用从缓存读取到旧数据 重试缓存删除
先更新数据库,再删缓存 数据库更新成功后,还没删除缓存,有并发读请求 并发请求从缓存中读到旧值 等待缓存删除完成,期间会有短暂的不一致存在。属于正常情况

4.1 延迟双删

在并发请求下,先删除缓存再更新数据库,可能在更新数据库前,有并发的读请求。A线程删除缓存,B线程并发读取数据,这个时候就会导致缓存中的数据是旧值。

延迟双删就是在线程A更新完数据库后,再sleep一小段时间,再进行一次缓存删除操作。但是实际上这个操作并不靠谱,因为无法确认sleep的时间,而且在业务线程中进行sleep会严重影响接口的吞吐量。所以比较靠谱的是用延迟队列,至于延迟时间就需要根据业务做一定的判断。

4.2 重试缓存删除

具体来说,就是把要删除的缓存值,或者要更新的数据库值暂存在消息队列(如:RocketMQ)。当应用没有成功删除缓存值或者更新数据库值时,从消息队列中读取消息,对缓存再次进行删除或更新。

当成功删除或更新缓存时,要从消息队列中把消息去除,避免重复操作。如果操作没成功,需要进行重试,当重试超过一定次数,还没有成功,就需要向业务发送报错信息。

5. 击穿、穿透、雪崩

这三个问题一旦发生,会有大量的请求发送到数据库。

5.1 缓存雪崩

概念: 是指大量的应用请求无法在Redis缓存中进行处理,应用将大量的请求发送数据库。

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

解决方案:

  • 避免给大量数据设置相同的过期时间,如果需要这些数据同时失效,可以给每个过期时间加点随机数。(大量数据同时过期,Redis在那一时刻延迟也会变高。)

  • 服务降级,发生缓存雪崩时,针对不同数据采取不同的处理方式。

    • 业务访问非核心数据:暂停从缓存中查询这些数据,可以直接返回预定义信息、空值或错误信息。
    • 业务访问核心数据:仍然查询缓存,如果缓存缺失,可以继续通过数据库读取。

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

一般来说一个Redis实例可以支持数万级别的请求处理吞吐量,而单个数据库可能只能处理数千级别的请求处理吞吐量,两者处理能力可能差了近10倍。所有请求打到数据库可能会让数据库崩溃。

解决方案:

  • 业务系统实现服务熔断或者请求限流机制。
  • 事前预防:Redis缓存主从集群

5.2 缓存击穿

概念: 非常频繁的热点数据的请求,无法在缓存处理,访问该数据的大量请求,发送到数据库。缓存击穿的情况,

出现原因: 热点数据过期时。

解决策略: 特别频繁的热点数据,不设置过期时间。

5.3 缓存穿透

概念: 要访问的数据既不在Redis缓存,又不在数据库。导致请求访问缓存是,发生缓存缺失,又去访问数据库,发现数据库也没有要访问的数据。

出现原因:

  1. 业务层误操作:缓存中的数据和数据库中的数据被误删除,所以缓存和数据库都没有数据。
  1. 恶意攻击:专门访问数据库中没有的数据。
  1. 延迟创建的数据:有些数据一开始不存在数据库,但是会走查缓存逻辑逻辑。比如:有个策略需要用户手动创建,那么判断策略的时候可能会走业务查缓存逻辑,会触发无缓存和数据库。

解决方案:

  • 缓存空值或缺省值。这种需要注意,当数据库有值的时候,缓存记得要清理。
  • 使用布隆过滤器快速判断数据是否存在,避免从数据库中查询数据是否存在,减轻数据库压力。(布隆过滤器存在,数据可能存在;布隆过滤器不存在,数据肯定不存在,hash的原理)
  • 请求入口对请求检测。把恶意的请求过滤掉,不让它访问后端缓存的数据库。

5.4 总结

问题 原因 应对方案
缓存雪崩 大量数据同时过期 缓存实例宕机 1.缓存过期时间加随机数,避免同时过期 2.服务降级 3.服务熔断 4.请求限流 5.Redis缓存主从集群
缓存击穿 访问非常频繁的热点数据过期 热点数据不设置过期时间
缓存穿透 缓存和数据库都没有要访问的数据 1.缓存空值或缺省值 2.使用布隆过滤器快速判断 3.请求入口对请求合法性校验

6. 缓存淘汰策略

缓存的容量必然小于后端数据库总量,有限的空间不可避免地会被写满,因此需要缓存数据的淘汰机制。

经验值得出:缓存容量设置为总数据量的15%到30%,可以兼顾访问性能和内存空间开销。再redis中,可以通过命令设定缓存的大小:

sql 复制代码
CONFIG SET maxmemory 4gb

Redis4.0后,总共有8中策略:

  • 不进行数据淘汰策略:只有noeviction这一种。

  • 会进行数据淘汰的7种策略:可以根据淘汰候选数据集分为两类

    • 在设置过期时间的数据中进行淘汰:volatile-random、volatile-ttl、volatile-lru、volatile-lfu
    • 在所有数据范围内进行淘汰:allkeys-lru、allkeys-random、allkeys-lfu

默认情况下,Redis在使用的内存超过maxmemory值时,并不会淘汰数据,也就是noeviction策略,对应到Redis缓存,一旦缓存被写满了,再有写请求时,Redis不再提供服务,而是直接返回错误。

对于volatile-random、volatile-ttl、volatile-lru、volatile-lfu这四种策略,在晒选的候选数据范围,被限制在已经设置过期时间的键值对上,也就是会有两个维度进行删除:

  • 键值对本身有过期时间,数据如果过期了,会进行删除。
  • 当达到maxmemory时,会根据具体策略进行删除。

这四种策略对应以下情况:

  • volatile-random:在过期时间的键值对中,进行随机删除。
  • volatile-ttl:根据过期时间的先后进行删除,越早过期越先被删除。
  • volatile-lru:使用LRU算法筛选设置了过期时间的键值对。
  • volatile-lfu:使用LFU算法筛选设置了过期时间的键值对。

allkeys-lru、allkeys-random、allkeys-lfu这三种淘汰策略会晒选数据时会选择所有键值对,无论是否设置了过期时间

  • allkeys-random:从所有键值对中随机选择并删除数据。
  • allkeys-lru:使用LRU算法从所有键值对中进行筛选。
  • allkeys-lfu:使用LFU算法从所有键值对中进行筛选。

7. 参考资料

相关推荐
White graces12 分钟前
Spring MVC练习(前后端分离开发实例)
java·开发语言·前端·后端·spring·java-ee·mvc
kingwebo'sZone3 小时前
ASP.net WebAPI 上传图片实例(保存显示随机文件名)
后端·asp.net
桑榆肖物3 小时前
一个简单的ASP.NET 一致性返回工具库
后端·asp.net
组态软件6 小时前
web组态软件
前端·后端·物联网·编辑器·html
Peter_chq6 小时前
【计算机网络】多路转接之select
linux·c语言·开发语言·网络·c++·后端·select
cnsxjean9 小时前
SpringBoot集成Minio实现上传凭证、分片上传、秒传和断点续传
java·前端·spring boot·分布式·后端·中间件·架构
kingbal9 小时前
SpringCloud:Injection of resource dependencies failed
后端·spring·spring cloud
刘天远10 小时前
django实现paypal订阅记录
后端·python·django
ℳ₯㎕ddzོꦿ࿐10 小时前
Spring Boot集成MyBatis-Plus:自定义拦截器实现动态表名切换
spring boot·后端·mybatis
Clown9510 小时前
go-zero(十) 数据缓存和Redis使用
redis·缓存·golang