认识Redis 以及 Redis常见问题(缓存穿透、缓存雪崩、缓存击穿、热key\大key、数据不一致等)
迪丽瓦拉
2025-05-29 20:15:01
0

为什么要使用Redis?

原因很简单,快!

在大并发,高负载的网站中必须考虑速度问题.Redis数据库中的所有数据都存储在内存中。由于内存的读写速度远快于硬盘,因此Redis的的的在性能上对比其他基于硬盘存储的数据库有非常明显的优势。

数据库的工作模式按存储方式可分为:硬盘数据库和内存数据库。
Redis 将数据储存在内存里面,读写数据的时候都不会受到硬盘 I/O 速度的限制,所以速度极快。

(1)硬盘数据库的工作模式:

在这里插入图片描述

(2)内存数据库的工作模式:

在这里插入图片描述

项目中使用Redis,主要考虑性能和并发。

性能:

如下图所示,在大并发的情况下,所有的请求直接访问数据库,数据库会出现连接异常。这个时候,就需要使用Redis做一个缓冲操作,让请求先访问到Redis,而不是直接访问数据库。

在这里插入图片描述

并发:

如下图所示,在大并发的情况下,所有的请求直接访问数据库,数据库会出现连接异常。这个时候,就需要使用Redis做一个缓冲操作,让请求先访问到Redis,而不是直接访问数据库。

在这里插入图片描述

Redis 与其它数据库的区别

在这里插入图片描述

Redis 中的各大典型问题

在这里插入图片描述

关于缓存穿透

当查询 Redis 中没有的数据时,该查询会下沉到数据库层,同时数据库层也没有该数据,当这种情况大量出现或被恶意攻击时,接口的访问全部透过 Redis 访问数据库,而数据库中也没有这些数据,我们称这种现象为"缓存穿透"。缓存穿透会穿透 Redis 的保护,提升底层数据库的负载压力,同时这类穿透查询没有数据返回也造成了网络和计算资源的浪费。

在这里插入图片描述

解决方案:

  • 在接口访问层对用户做校验,如接口传参、登陆状态、n 秒内访问接口的次数;
  • 利用布隆过滤器,将数据库层有的数据key存储在位数组中,以判断访问的key在底层数据库中是否存在;

基于布隆过滤器,我们可以先将数据库中数据的 key 存储在布隆过滤器的位数组中,每次客户端查询数据时先访问Redis:

这里简单介绍一下什么是布隆过滤器

布隆过滤器,它是一种概率型数据结构,用于判断一个元素是否在集合中。
当布隆过滤器说,某个数据存在时,这个数据可能不存在;当布隆过滤器说,某个数据不存在时,那么这个数据一定不存在。

布隆过滤器原理

BloomFilter的算法是,首先分配一块内存空间做 bit 数组,数组的 bit 位初始值全部设为 0。
加入元素时,采用 k 个相互独立的 Hash 函数计算,然后将元素 Hash 映射的 K 个位置全部设置为 1。
检测 key 是否存在,仍然用这 k 个 Hash 函数计算出 k 个位置,如果位置全部为 1,则表明 key 存在,否则不存在。
如下图所示:
在这里插入图片描述
哈希函数会出现碰撞,所以布隆过滤器会存在误判。

客户端查询数据时先访问Redis

  • 如果Redis内不存在该数据,则通过布隆过滤器判断数据是否在底层数据库内;
  • 如果布隆过滤器告诉我们该 key 在底层库内不存在,则直接返回 null 给客户端即可,避免了查询底层数据库的动作;
  • 如果布隆过滤器告诉我们该 key 极有可能在底层数据库内存在,那么将查询下推到底层数据库即可;
    在这里插入图片描述
    布隆过滤器有误判率,虽然不能完全避免数据穿透的现象,但已经可以将99.99%的穿透查询给屏蔽在Redis层了,极大的降低了底层数据库的压力,减少了资源浪费。

关于缓存击穿

缓存击穿和缓存穿透从名词上可能很难区分开来,它们的区别是:穿透表示底层数据库没有数据且缓存内也没有数据,击穿表示底层数据库有数据而缓存内没有数据。当热点数据 key 从缓存内失效时,大量访问同时请求这个数据,就会将查询下沉到数据库层,此时数据库层的负载压力会骤增,我们称这种现象为"缓存击穿"。
在这里插入图片描述

解决方案:

  • 延长热点key的过期时间或者设置永不过期,如排行榜,首页等一定会有高并发的接口;
  • 利用互斥锁保证同一时刻只有一个客户端可以查询底层数据库的这个数据,一旦查到数据就缓存至Redis内,避免其他大量请求同时穿过Redis访问底层数据库;
    在这里插入图片描述

在使用互斥锁的时候需要避免出现死锁或者锁过期的情况:

  • 使用 lua 脚本或事务将获取锁和设置过期时间作为一个原子性操作(如:set kk vv nx px 30000),以避免出现某个客户端获取锁之后宕机导致的锁不被释放造成死锁现象;
  • 另起一个线程监控获取锁的线程的查询状态,快到锁过期时间时还没查询结束则延长锁的过期时间,避免多次查询多次锁过期造成计算资源的浪费;

缓存雪崩

缓存雪崩是缓存击穿的"大面积"版,缓存击穿是数据库缓存到 Redis 内的热点数据失效导致大量并发查询穿过 Redis 直接击打到底层数据库,而缓存雪崩是指 Redis 中大量的 key 几乎同时过期,然后大量并发查询穿过 Redis 击打到底层数据库上,此时数据库层的负载压力会骤增,我们称这种现象为"缓存雪崩"。

事实上缓存雪崩相比于缓存击穿更容易发生,对于大多数公司来讲,同时超大并发量访问同一个过时key的场景的确太少见了,而大量key同时过期,大量用户访问这些key的几率相比缓存击穿来说明显更大。

在这里插入图片描述

解决方案:

  • 在可接受的时间范围内随机设置key的过期时间,分散key的过期时间,以防止大量的key在同一时刻过期;
  • 对于一定要在固定时间让key失效的场景(例如每日12点准时更新所有最新排名),可以在固定的失效时间时在接口服务端设置随机延时,将请求的时间打散,让一部分查询先将数据缓存起来;
  • 延长热点key的过期时间或者设置永不过期,这一点和缓存击穿中的方案一样;

在这里插入图片描述
在这里插入图片描述

关于热key问题

热key问题,就是瞬间有几十万的请求去访问redis上某个固定的key,从而压垮缓存服务的情情况。
其实生活中也是有不少这样的例子。比如XX明星结婚。那么关于XX明星的Key就会瞬间增大,就会出现热数据问题。这样会造成流量过于集中,达到物理网卡上限,从而导致这台 Redis 的服务器宕机。那接下来这个key的请求,就会直接怼到你的数据库上,导致你的服务不可用。

解决方案

(1)利用二级缓存

比如利用ehcache,或者一个HashMap都可以。在你发现热key以后,把热key加载到系统的JVM中。
针对这种热key请求,会直接从jvm中取,而不会走到redis层。
假设此时有十万个针对同一个key的请求过来,如果没有本地缓存,这十万个请求就直接怼到同一台redis上了。

现在假设,你的应用层有50台机器,OK,你也有jvm缓存了。这十万个请求平均分散开来,每个机器有2000个请求,会从JVM中取到value值,然后返回数据。避免了十万个请求怼到同一台redis上的情形。

(2)备份热key

这个方案也很简单。不要让key走到同一台redis上不就行了。我们把这个key,在多个redis上都存一份不就好了。接下来,有热key请求进来的时候,我们就在有备份的redis上随机选取一台,进行访问取值,返回数据。
假设redis的集群数量为N,步骤如下图所示

在这里插入图片描述

关于大key问题

所谓的大key问题是某个key的value比较大,所以本质上是大value问题。key往往是程序可以自行设置的,value往往不受程序控制,因此可能导致value很大。

设想一种场景:

在线音乐app中,某个歌单有很多用户收藏,假如有这样的数据结构:

  • 歌单和用户之间的映射关系采用redis存储

  • redis的key是歌单ID,长度可控且很小

  • redis的value是个list,list包含了用户ID

  • 用户可能很多,就导致list长度不可控

在这里插入图片描述

大 key 会造成什么影响?

  • 客户端超时阻塞。由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。
  • 引发网络阻塞。每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。
  • 阻塞工作线程。如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令。
  • 内存分布不均。集群模型在 slot 分片均匀情况下,会出现数据和查询倾斜情况,部分有大 key 的 Redis 节点占用内存多,QPS (每秒查询率) 也会比较大

解决方案

1、对大Key进行拆分

  • 将一个Big Key拆分为多个key-value这样的小Key,并确保每个key的成员数量或者大小在合理范围内,然后再进行存储,通过get不同的key或者使用mget批量获取。

2、对大Key进行清理

  • 对Redis中的大Key进行清理,从Redis中删除此类数据。Redis自4.0起提供了UNLINK命令,该命令能够以非阻塞的方式缓慢逐步的清理传入的Key,通过UNLINK,你可以安全的删除大Key甚至特大Key。

3、监控Redis的内存、网络带宽、超时等指标

  • 通过监控系统并设置合理的Redis内存报警阈值来提醒我们此时可能有大Key正在产生,如:Redis内存使用率超过70%,Redis内存1小时内增长率超过20%等。

4、定期清理失效数据

  • 如果某个Key有业务不断以增量方式写入大量的数据,并且忽略了其时效性,这样会导致大量的失效数据堆积。可以通过定时任务的方式,对失效数据进行清理。

5、压缩value

  • 使用序列化、压缩算法将key的大小控制在合理范围内,但是需要注意序列化、反序列化都会带来一定的消耗。如果压缩后,value还是很大,那么可以进一步对key进行拆分。

关于数据不一致

缓存不一致的情况有两种:

  • Redis缓存中是旧值;
  • 数据库中值是旧值;

缓存一致性需要保证的是,当缓存中有值的时候,数据库的值必须与缓存一致。

产生原因

根据是否接收写请求,可以将缓存分为读写缓存和只读缓存。两种发生缓存不一致的情况不同,需要分开来应对。
在只读缓存中,新增数据会直接写到数据库中,不会操作缓存,所以不会出现缓存不一致。删改数据时,需要删除数据库和缓存中的数据,在删改数据库和缓存时,无论哪个先后,中间出现故障都会产生旧值,即缓存不一致的情况。

在这里插入图片描述

解决方案

  • 第一种方案:采用延时双删策略(双淘汰策略)

    在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。

    具体的步骤就是:

      1)先删除缓存2)再写数据库3)休眠(一定的时间)毫秒4)再次删除缓存
    
  • 第二种方案:设置缓存过期时间

    给缓存设置过期时间,是保证最终一致性的解决方案。所有的写操作以数据库为准,只要到达缓存过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。

    方案弊端

    结合双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致,而且又增加了写请求的耗时。

  • 第三种方案:异步更新缓存(基于订阅binlog的同步机制)

      binlog日志用于记录所有更新了数据或者已经潜在更新了数据(例如,没有匹配任何行的一个DELETE)的所有语句。语句以“事件”的形式保存,它描述数据更改。
    

    技术整体思路:

    MySQL binlog增量订阅消费+消息队列+增量数据更新到 redis

      1)读Redis:热数据基本都在Redis2)写MySQL:增删改都是操作MySQL3)更新Redis数据:MySQ的数据操作binlog,来更新到Redis
    

    3.1)数据操作主要分为两大块:

      一个是全量(将全部数据一次写入到redis)一个是增量(实时更新)这里说的是增量,指的是mysql的update、insert、delate变更数据。
    

    3.2)读取binlog后分析 ,利用消息队列,推送更新各台的redis缓存数据。

      这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。
    

    其实这种机制,很类似MySQL的主从备份机制,因为MySQL的主备也是通过binlog来实现的数据一致性。

缓存预热

缓存预热如字面意思,当系统上线时,缓存内还没有数据,如果直接提供给用户使用,每个请求都会穿过缓存去访问底层数据库,如果并发大的话,很有可能在上线当天就会宕机,因此我们需要在上线前先将数据库内的热点数据缓存至Redis内再提供出去使用,这种操作就成为"缓存预热"。

缓存更新

缓存服务(Redis)和数据服务(底层数据库)是相互独立且异构的系统,在更新缓存或更新数据的时候无法做到原子性的同时更新两边的数据,因此在并发读写或第二步操作异常时会遇到各种数据不一致的问题。如何解决并发场景下更新操作的双写一致是缓存系统的一个重要知识点。

第二步操作异常:缓存和数据的操作顺序中,第二个动作报错。如数据库被更新, 此时失效缓存的时候出错,缓存内数据仍是旧版本;

缓存更新的设计模式有四种:

  • Cache aside:查询:先查缓存,缓存没有就查数据库,然后加载至缓存内;更新:先更新数据库,然后让缓存失效;或者先失效缓存然后更新数据库;

  • Read through:在查询操作中更新缓存,即当缓存失效时,Cache Aside 模式是由调用方负责把数据加载入缓存,而 Read Through 则用缓存服务自己来加载;

  • Write through:在更新数据时发生。当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后由缓存自己更新数据库;

  • Write behind caching:俗称write back,在更新数据的时候,只更新缓存,不更新数据库,缓存会异步地定时批量更新数据库;

Cache aside:

为了避免在并发场景下,多个请求同时更新同一个缓存导致脏数据,因此不能直接更新缓存而是另缓存失效。

先更新数据库后失效缓存:并发场景下,推荐使用延迟失效(写请求完成后给缓存设置1s过期时间),在读请求缓存数据时若redis内已有该数据(其他写请求还未结束)则不更新。当redis内没有该数据的时候(其他写请求已令该缓存失效),读请求才会更新redis内的数据。这里的读请求缓存数据可以加上失效时间,以防第二步操作异常导致的不一致情况。

先失效缓存后更新数据库:并发场景下,推荐使用延迟失效(写请求开始前给缓存设置1s过期时间),在写请求失效缓存时设置一个1s延迟时间,然后再去更新数据库的数据,此时其他读请求仍然可以读到缓存内的数据,当数据库端更新完成后,缓存内的数据已失效,之后的读请求会将数据库端最新的数据加载至缓存内保证缓存和数据库端数据一致性;在这种方案下,第二步操作异常不会引起数据不一致,例如设置了缓存1s后失效,然后在更新数据库时报错,即使缓存失效,之后的读请求仍然会把更新前的数据重新加载到缓存内。

推荐使用先失效缓存,后更新数据库,配合延迟失效来更新缓存的模式;

在这里插入图片描述

四种缓存更新模式的优缺点:

  • Cache Aside:实现起来较简单,但需要维护两个数据存储,一个是缓存(Cache),一个是数据库(Repository);
  • Read/Write Through:只需要维护一个数据存储(缓存),但是实现起来要复杂一些;
  • Write Behind Caching:与Read/Write Through 类似,区别是Write Behind Caching的数据持久化操作是异步的,但是Read/Write Through 更新模式的数据持久化操作是同步的。优点是直接操作内存速度快,多次操作可以合并持久化到数据库。缺点是数据可能会丢失,例如系统断电等。

缓存降级

缓存降级是指当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,即使是有损部分其他服务,仍然需要保证主服务可用。可以将其他次要服务的数据进行缓存降级,从而提升主服务的稳定性。

降级的目的是保证核心服务可用,即使是有损的。如去年双十一的时候淘宝购物车无法修改地址只能使用默认地址,这个服务就是被降级了,这里阿里保证了订单可以正常提交和付款,但修改地址的服务可以在服务器压力降低,并发量相对减少的时候再恢复。

降级可以根据实时的监控数据进行自动降级也可以配置开关人工降级。是否需要降级,哪些服务需要降级,在什么情况下再降级,取决于大家对于系统功能的取舍。

关于Redis的集群(主从模式、哨兵模式、分片集群)

相关内容