Redis基础篇常见知识点与面试题(下)

1.7、缓存和数据库一致性问题

这位大哥总结的很全面,可以看看
缓存和数据库一致性问题,看这篇就够了 (qq.com)

1.8、Redis为什么这么快

1、内存存储

Redis 是基于内存操作的数据库,不论读写操作都是在内存上完成的,直接访问内存的速度远比访问磁盘的速度要快多个数量级。

2、数据结构

Redis 提供了多种高效的数据结构,如字符串、哈希、列表、集合等,这些专门优化过的数据结构支持高效的读写操作。

具体来说,字符串结构,作者底层使用简单动态字符串(SDS)替换传统字符串,内部有一个 len 字段记录了字符串长度:实现了 O(1) 复杂度的 strlen 操作,并保证了二进制安全性。以及 Redis 在内部针对区分了多种 SDS 类型,不同大小的字符串会对应不同的 SDS 实现,有效的节省内存。

另外值得一说的是,Redis 中的 ZSet 会在数据较多的时候使用跳表实现。跳表是一种基于链表实现的数据结构,它可以通过具有多级索引的方式来加速查找元素。相比起正常的列表,它在插入、删除和搜索时都具备 O(logn) 的复杂度,并且相比起树实现起来更加简单。

3、非阻塞 IO

Redis 基于 IO 多路复用实现了非阻塞式 IO,采用 IO 多路复用技术,并发处理连接。通过 epoll 模型和自己实现的简单的事件框架,将 epoll 中的读、写、关闭、连接都转化成了事件,然后利用 epoll 的多路复用特性,把 IO 操作时间优化到了极致。

IO多路复用指定是一种同步的IO模型,实现一个线程监视多个文件句柄(文件描述符),一旦某个文件句柄准备就绪就能够通知到对应的应用程序进行相应的读写操作,没有文件句柄就会阻塞应用程序,从而释放CPU资源。其概念介绍如下:

I/O:网络IO,尤其在操作系统层面指数据在内核态和用户态之间的读写操作
多路:多个客户端连接
复用:复用一个或多个线程
IO多路复用:也就是说一个或一组线程处理多个TCP连接,使用单进程就能同时处理多个客户端连接,无需创建或者维护过多的进程/线程

一句话来说就是一个服务端进程可以同时处理多个套接字描述符,实现IO多路复用的模型有三种,可以分为select->poll->epoll三个阶段来描述。

image-20240306140758023

​ 将用户scoket对应的文件描述符注册进epoll,然后epoll帮你监听哪些socket上有消息到达,这样就避免了大量无用的操作,此时Socket应该采用非阻塞模式。这样,整个过程只在调用select、Poll、epoll这些调用时才会阻塞,收发客户消息时不会阻塞的,整个过程线程就被充分利用起来,这就是事件驱动的,所谓的reactor反应模式。

4、 线程模型

Redis 把所有主线操作使用单线程模型,将网络 IO 以及指令读写全部交由一个线程来执行。

这样可以带来避免线程创建而导致的性能消耗,多线程上下文切换而引起的 CPU 开销,以及避免了多个线程之间的竞争问题,比如临界区资源的线程安全、锁的申请、释放以及死锁等问题。

1.9、Redis如何实现到期删除的?

1. 到期时间

在 Redis 中,你可以使用以下四种命令为 key 设置到期时间:

  • EXPIRE:以为单位,设置 key 的有效时间。
  • PEXPIRE:以毫秒为单位,设置 key 的有效时间。
  • EXPIREAT:以为单位,设置 key 的到期时间戳。
  • PEXPIREAT:以毫秒为单位,设置 key 的到期时间戳。

其中,前两者指定的是 key 的有效时长,而后两者指定的是 key 到期时间点

不过,在 Redis 底层实现中,四种命令最终都会变为 key 到期时间点对应的时间戳,并被记录在一个到期字典中(哈希表)。

2. 删除策略

按官方文档的说法,Redis 的过期删除有两种方式

  • 主动删除:每 10 秒扫描一次数据库,随机抽 20 个 key,并删除其中到期的 key。如果到期 key 占比超过 25%,那么继续抽样,直到不满足条件或超时为止;
  • 被动删除:访问 key 时检查到期时间,如果已经到期就删除;
a. 定时删除

定时删除,就是在设置 key 的到期时间时,一并设置一个定时事件,等到事件触发时删除 key。

  • 优点: 可以及时释放资源,确保过期键能够被及时删除。
  • 缺点: 频繁的删除操作可能会占用大量的 CPU 时间。

总的来说,这个策略对内存优化而对 CPU 不友好,在 CPU 紧张而内存宽裕的场景中,它会将更多的 CPU 资源花费到没那么紧要的删除到期 key 操作上。

综合上述考量, Redis 并没有使用这种方式

b. 惰性删除

惰性删除就是 Redis 提到的被动删除。

被动删除不会主动的删除到期的 key,而是当访问 key 时再检查是否到期,如果到期了再将其删除。

它的优缺点与定时删除刚好相反:

  • 优点: 只在取出键时进行检查,避免了频繁的删除操作。
  • 缺点: 可能会导致内存积压问题。

惰性删除对 CPU 最友好,但是对内存就不友好了。尤其是当你需要在 Redis 中存放大量具备到期时间且不需要频繁访问的数据时,会造成内存积压。

c. 定期删除

定期删除就是 Redis 提到的主动删除。

定期删除会定时的随机从数据库中对 key 抽样(20 个),然后删除其中的过期键。如果此次抽样中,到期 key 的占比高于一定阈值(25%),则会再进行一次抽样删除,直到到期 key 占比没那么高。或者本次任务执行超时为止。

相对于定时删除和惰性删除,定期删除在内存和 CPU 消耗中取得了一个比较好的平衡

另外,使用抽样避免全量操作的思想在 Redis 中挺常见的,比如内存淘汰策略中的近似 LRU 和 LFU

3. 在持久化时

Redis 使用 AOF 与 RBD 两种方式来持久化内存中数据,这个过程同样需要考虑如何处理过期的 key:

  • AOF:当 key 因为到期而被删除时,将会向 AOF 追加一条 **DEL** 命令。如果在这个过程中进行了 AOF 重写,那么重写后的 AOF 文件中则将直接忽略掉这个过期的 key。
  • RDB:与 AOF 重写类似,在创建 RDB 的时候,过期的 key 会被直接忽略

4. 在集群中

当集群中的实例发现 key 到期后,实例会根据它自己是主节点还是从节点而采取不同的行为:

  • 如果是主节点,它会在删除这个过期 key 后向所有从节点发送一个 DEL 命令。
  • 如果是从节点,那么它将会将这个 key 标记为到期,但并不会真正的删除。只有当接到从主节点发来的 DEL 命令之后,才会真正的将过期键删除掉。

从节点不会主动删除 key,这是为了保证与主节点数据的一致性,以便当主从切换时后,仍然可以正常的处理过期 key。

不过当系统中有大量频繁过期的 key,且一个主节点有较多从节点的时候,这会带来更多的内存消耗。

1.10、Redis常用内存淘汰策略?

内存总是有限的,因此当 Redis 内存超出最大内存时,就需要根据一定的策略去主动的淘汰一些 key,来腾出内存,这就是内存淘汰策略。我们可以在配置文件中通过 maxmemory-policy 配置指定策略。

与到期删除策略不同,内存淘汰策略主要目的则是为了防止运行时内存超过最大内存,所以尽管最终目的都是清理内存中的一些 key,但是它们的应用场景和触发时机是不同的。

算上在 4.0 添加的两种基于 LFU 算法的策略, Redis 一共提供了八种策略供我们选择:

  • noeviction,不淘汰任何 key,直接报错。它是默认策略**。**
  • volatile-random:从所有设置了到期时间的 key 中,随机淘汰一个 Key。
  • volatile-lru: 从所有设置了到期时间的 key 中,淘汰最近最少使用的 key。
  • volatile-lfu: 从所有设置了到期时间的 key 中,淘汰最近最不常用使用的 key(4.0 新增)。
  • volatile-ttl: 从所有设置了到期时间的 key 中,优先淘汰最早过期的 key。
  • allkeys-random:从所有 key中,随机淘汰一个键(4.0 新增)。
  • allkeys-lru: 从所有 key 中,淘汰最近最少使用的 key。
  • allkeys-lfu: 从所有 key 中,淘汰最近最不常用使用的键。

淘汰范围来说可以分为不淘汰任何数据、只从设置了到期时间的键中淘汰和从所有键中淘汰三类。而从淘汰算法来分,又主要分为 random(随机),LRU(最近最少使用),以及 LFU(最近最不常使用)三种。

其中,关于 LRU 算法,它是一种非常常见的缓存淘汰算法。我们可以简单理解为 Redis 会在每次访问 key 的时候记录访问时间,当淘汰时,优先淘汰最后一次访问距离现在最早的 key。

而对于 LFU 算法,我们可以理解为 Redis 会在访问 key 时,根据两次访问时间的间隔计算并累加访问频率指标,当淘汰时,优先淘汰访问频率指标最低的 key。相比 LRU 算法,它避免了低频率的大批量查询造成的缓存污染问题

顺带一提,只要是有类似缓存机制的应用或多或少都会面对这种问题,比如老生常谈的 MySQL 连表查询,在数据量大的时候也会造成缓存污染。

1.11、Redis的跳表是什么?ZSet底层是怎么实现的?

跳表是 ZSet 的底层实现之一,它是一种包含多级链表的数据结构,它允许通过额外的索引层来实现快速查找,实现 O(logN) 的平均复杂度。

Redis 的跳表节点里面保存了 score 和 member,所有的节点都按照 score 排序,而当 score 相同时,会再按照 member 的字典顺序进行排序。

虽然理想情况下,跳表的相邻两层之间的节点数量比是 2:1 ,但是这样做会导致在操作时付出额外的代价重建索引,因此 Redis 的使用了一种随机算法来生成索引:生成一个 0 到 1 之间的随机数,然后判断是不是小于 0.25,如果是就加一层,然后继续重复这个动作,直到随机数大于 0.25 或者到了最大层高为止。

这个策略保证了高层节点相对较少而底层节点相对较多,进而保证了索引的节点密度会随着层级从底层往上逐渐减少。

相比起具有同样查找效率的二叉树,它占用的内存更小,对范围操作的支持更好,并且修改的代价更小,实现也更简单。

Zset的实现方式是跳表+字典,这种实现用于处理较大的数据集。在这种实现方式中,Redis 使用跳表的节点保存指向 member 的指针和 score ,同时又使用了字典保存 member 和 score 之间的对应关系,以便同时实现高效的随机查找和范围查找。

回到跳表本身,它是一种多级链表数据结构,它通过额外的索引层实现快速查找,使查找和插入操作的复杂度为 O(logN)。跳表的层数会影响操作的效率,因此 Redis 使用了一种随机算法来生成节点的层高,从而保证索引层节点密度从底层到顶层逐渐减少。

相对于具有相似查找效率的二叉树,跳表占用的内存更少,对范围操作的支持更好,修改的代价更小,并实现更简单。

1.12、Redis生产问题

1、缓存预热

Redis缓存预热是指在系统上线后,将相关的缓存数据直接加载到缓存系统,以避免在用户请求的时候,先查询数据库,然后再将数据回写到缓存。这样可以极大减少对数据库的压力,提高系统的响应速度和性能。缓存预热主要应用于秒杀场景,例如618购物节。

实现方案

数据入库后,程序员可以手动将这些数据从mysql中同步到redis中
通过中间件来完成,例如使用阿里巴巴的canal实现双写一致性,或者使用java的@PostConstruct注解

2、缓存穿透

请求去查询一条数据,先查redis无,后查mysql无,都查询不到数据。但是请求每次都打到mysql中了,导致后台数据库压力暴增,这种现象我们称为缓存穿透,redis在这种情况下就成了摆设。简单来说,就是一个数据既不在redis中也不再mysql中,数据库存在被多次暴击的风险。

在这里插入图片描述

解决方案一 :缓存空数据,查询返回的数据为空,仍把这个空结果进行缓存

解决方案二 :布隆过滤器

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

解决方案三:接口限流

根据用户或者 IP 对接口进行限流,对于异常频繁的访问行为,还可以采取黑名单机制,例如将异常 IP 列入黑名单

3、缓存击穿

缓存击穿是指当某个热点数据(被大量并发访问)在Redis缓存中过期时,所有对该数据的访问都会直接导致数据库承受巨大的压力,从而可能造成数据库崩溃。(简单来说就是热点key失效,暴打mysql)

在这里插入图片描述

第一可以使用互斥锁:当缓存失效时,不立即去load db,先使用如Redis的 setnx去设置一个互斥锁,当操作成功返回时再进行load db的操作并回设缓存,否则重试get缓存的方法。

第二种方案可以设置当前key逻辑过期

  1. 设置热点数据永不过期或者过期时间比较长。
  2. 针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。

4、缓存雪崩

缓存在同一时间大面积的失效,导致大量的请求都直接落到了数据库上,对数据库造成了巨大的压力。 这就好比雪崩一样,摧枯拉朽之势,数据库的压力可想而知,可能直接就被这么多请求弄宕机了。

在这里插入图片描述

针对 Redis 服务不可用的情况:

  1. 采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。
  2. 限流,避免同时处理大量的请求。
  3. 多级缓存,例如本地缓存+Redis 缓存的组合,当 Redis 缓存出现问题时,还可以从本地缓存中获取到部分数据。

针对热点缓存失效的情况:

  1. 设置不同的失效时间比如随机设置缓存的失效时间。
  2. 缓存永不失效(不太推荐,实用性太差)。
  3. 缓存预热,也就是在程序启动后或运行过程中,主动将热点数据加载到缓存中。

缓存预热如何实现?

常见的缓存预热方式有两种:

  1. 使用定时任务,比如 xxl-job,来定时触发缓存预热的逻辑,将数据库中的热点数据查询出来并存入缓存中。
  2. 使用消息队列,比如 Kafka,来异步地进行缓存预热,将数据库中的热点数据的主键或者 ID 发送到消息队列中,然后由缓存服务消费消息队列中的数据,根据主键或者 ID 查询数据库并更新缓存。

1.13、Redis性能优化

1、使用批量操作减少网络传输

使用批量操作可以减少网络传输次数,进而有效减小网络开销,大幅减少 RTT。

另外,除了能减少 RTT 之外,发送一次命令的 socket I/O 成本也比较高(涉及上下文切换,存在read()write()系统调用),批量操作还可以减少 socket I/O 成本。

Redis 中有一些原生支持批量操作的命令,比如:

  • MGET(获取一个或多个指定 key 的值)、MSET(设置一个或多个指定 key 的值)、
  • HMGET(获取指定哈希表中一个或者多个指定字段的值)、HMSET(同时将一个或多个 field-value 对设置到指定哈希表中)、
  • SADD(向指定集合添加一个或多个元素)
  • ……

pipeline

对于不支持批量操作的命令,我们可以利用 pipeline(流水线) 将一批 Redis 命令封装成一组,这些 Redis 命令会被一次性提交到 Redis 服务器,只需要一次网络传输。不过,需要注意控制一次批量操作的 元素个数(例如 500 以内,实际也和元素字节数有关),避免网络传输的数据量过大。

原生批量操作命令和 pipeline 的是有区别的,使用的时候需要注意:

  • 原生批量操作命令是原子操作,pipeline 是非原子操作。
  • pipeline 可以打包不同的命令,原生批量操作命令不可以。
  • 原生批量操作命令是 Redis 服务端支持实现的,而 pipeline 需要服务端和客户端的共同实现。

Lua 脚本

Lua 脚本同样支持批量操作多条命令。一段 Lua 脚本可以视作一条命令执行,可以看作是 原子操作 。也就是说,一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰,这是 pipeline 所不具备的。

并且,Lua 脚本中支持一些简单的逻辑处理比如使用命令读取值并在 Lua 脚本中进行处理,这同样是 pipeline 所不具备的。

不过, Lua 脚本依然存在下面这些缺陷:

  • 如果 Lua 脚本运行时出错并中途结束,之后的操作不会进行,但是之前已经发生的写操作不会撤销,所以即使使用了 Lua 脚本,也不能实现类似数据库回滚的原子性。
  • Redis Cluster 下 Lua 脚本的原子操作也无法保证了,原因同样是无法保证所有的 key 都在同一个 hash slot(哈希槽)上。

2、BigKey问题

简单来说,如果一个 key 对应的 value 所占用的内存比较大,那这个 key 就可以看作是 bigkey。具体多大才算大呢?有一个不是特别精确的参考标准:

  • String 类型的 value 超过 1MB
  • 复合类型(List、Hash、Set、Sorted Set 等)的 value 包含的元素超过 5000 个(不过,对于复合类型的 value 来说,不一定包含的元素越多,占用的内存就越多)。

bigkey 通常是由于下面这些原因产生的:

  • 程序设计不当,比如直接使用 String 类型存储较大的文件对应的二进制数据。
  • 对于业务的数据规模考虑不周到,比如使用集合类型的时候没有考虑到数据量的快速增长。
  • 未及时清理垃圾数据,比如哈希中冗余了大量的无用键值对。

bigkey 除了会消耗更多的内存空间和带宽,还会对性能造成比较大的影响。

大 key 还会造成阻塞问题。具体来说,主要体现在下面三个方面:

  1. 客户端超时阻塞:由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。
  2. 网络阻塞:每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。
  3. 工作线程阻塞:如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令。

大 key 造成的阻塞问题还会进一步影响到主从同步和集群扩容。

综上,大 key 带来的潜在问题是非常多的,我们应该尽量避免 Redis 中存在 bigkey

bigkey 的常见处理以及优化办法如下(这些方法可以配合起来使用):

  • 分割 bigkey:将一个 bigkey 分割为多个小 key。例如,将一个含有上万字段数量的 Hash 按照一定策略(比如二次哈希)拆分为多个 Hash。

  • 手动清理:Redis 4.0+ 可以使用 UNLINK 命令来异步删除一个或多个指定的 key。Redis 4.0 以下可以考虑使用 SCAN 命令结合 DEL 命令来分批次删除。

  • 采用合适的数据结构:例如,文件二进制数据不使用 String 保存、使用 HyperLogLog 统计页面 UV、Bitmap 保存状态信息(0/1)。

  • 开启 lazy-free(惰性删除/延迟释放) :lazy-free 特性是 Redis 4.0 开始引入的,指的是让 Redis 采用异步方式延迟释放 key 使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程。

3、Hotkey问题

​ 如果一个 key 的访问次数比较多且明显多于其他 key 的话,那这个 key 就可以看作是 hotkey(热 Key)。例如在 Redis 实例的每秒处理请求达到 5000 次,而其中某个 key 的每秒访问量就高达 2000 次,那这个 key 就可以看作是 hotkey。hotkey 出现的原因主要是某个热点数据访问量暴增,如重大的热搜事件、参与秒杀的商品。

​ 处理 hotkey 会占用大量的 CPU 和带宽,可能会影响 Redis 实例对其他请求的正常处理。此外,如果突然访问 hotkey 的请求超出了 Redis 的处理能力,Redis 就会直接宕机。这种情况下,大量请求将落到后面的数据库上,可能会导致数据库崩溃。

因此,hotkey 很可能成为系统性能的瓶颈点,需要单独对其进行优化,以确保系统的高可用性和稳定性

hotkey 的常见处理以及优化办法如下(这些方法可以配合起来使用):

  • 读写分离:主节点处理写请求,从节点处理读请求。
  • 使用 Redis Cluster:将热点数据分散存储在多个 Redis 节点上。
  • 二级缓存:hotkey 采用二级缓存的方式进行处理,将 hotkey 存放一份到 JVM 本地内存中(可以用 Caffeine)。

4、慢查询

Redis 慢查询统计的是命令执行这一步骤的耗时,慢查询命令也就是那些命令执行时间较长的命令。

Redis 为什么会有慢查询命令呢?

Redis 中的大部分命令都是 O(1)时间复杂度,但也有少部分 O(n) 时间复杂度的命令,例如:

  • KEYS *:会返回所有符合规则的 key。
  • HGETALL:会返回一个 Hash 中所有的键值对。
  • LRANGE:会返回 List 中指定范围内的元素。
  • SMEMBERS:返回 Set 中的所有元素。
  • SINTER/SUNION/SDIFF:计算多个 Set 的交集/并集/差集。
  • ……

由于这些命令时间复杂度是 O(n),有时候也会全表扫描,随着 n 的增大,执行耗时也会越长。不过, 这些命令并不是一定不能使用,但是需要明确 N 的值。另外,有遍历的需求可以使用 HSCANSSCANZSCAN 代替。

除了这些 O(n)时间复杂度的命令可能会导致慢查询之外, 还有一些时间复杂度可能在 O(N) 以上的命令,例如:

  • ZRANGE/ZREVRANGE:返回指定 Sorted Set 中指定排名范围内的所有元素。时间复杂度为 O(log(n)+m),n 为所有元素的数量, m 为返回的元素数量,当 m 和 n 相当大时,O(n) 的时间复杂度更小。
  • ZREMRANGEBYRANK/ZREMRANGEBYSCORE:移除 Sorted Set 中指定排名范围/指定 score 范围内的所有元素。时间复杂度为 O(log(n)+m),n 为所有元素的数量, m 被删除元素的数量,当 m 和 n 相当大时,O(n) 的时间复杂度更小。
  • ……

针对此类原因,我们一般有以下两个原则:

  1. 尽量不使用O(N)以上复杂度的命令,某些数据排序或聚合操作,可以放在客户端处理。
  2. 执行O(N)命令时,保证 N 尽量的小(推荐 N <= 300 经验值),每次获取尽量少的数据,让 Redis 可以及时处理返回。
  • 25
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值