Redis 面经

1、说说什么是 Redis?

Redis 是 Remote Dictionary Service,也就是远程字典服务,是一种基于键值对的 NoSQL 数据库。支持的数据结构 比 HashMap 这种一般的键值对强大得多,支持 string、hash、 list、set、zset、Bitmaps、HyperLogLog、GEO 等。而且 Redis 的数据都存放在内存中,所以它的读写性能非常高。并且 Redis 还提供了持久化机制,从而将内存得数据持久化到硬盘上,在发生断电或机器故障时也能保证内存中的数据不会丢失。除此之外,Redis 还提供了过期时间、发布订阅、事务、流水线、Lua 脚本等功能,是互联网领域使用最广泛的缓存中间件。

1.1 Redis 和 MySQL 的区别?

  1. 从数据库结构的角度:Redis 是非关系型数据库,MySQL是关系型数据库。
  2. 从数据存储的角度:Redis 数据存储在内存中,MySQL 数据存储在硬盘中。
  3. 从性能的角度:Redis 基于内存读写,性能会比 MySQL 高得多。
  4. 从事务的角度:Redis 只能保证 ID,MySQL 保证 ACID。

1.2 项目里哪里用到了 Redis?

1.3 部署过 Redis 吗?

我是直接在本地部署的单机版,只需要下载 Redis 的安装包,解压后运行 redis-server 命令即可。

2、Redis 可以用来干什么?

Redis 可以用来做缓存、排行榜/计数器、分布式锁等等。

  1. 缓存:缓存是 Redis 最常见的用途,由于 Redis 的数据存储在内存中,所以读写速度非常快,远超基于磁盘存储的数据库。使用 Redis 缓存可以极大地提高应用的响应速度和吞吐量。
  2. 排行榜/计数器:Redis 的 ZSet 非常适合用来实现排行榜的功能,可以根据 score 进行排序,实时展示用户的活跃度。同时 Redis 的原子递增操作可以用来实现计数器功能。
  3. 分布式锁:Redis 可以实现分布式锁,用来控制跨多个进程的资源访问。

3、Redis 有哪些数据类型?

Redis 有五种基本数据类型,这五种数据类型分别是:String、Hash、List、Set、Sorted Set(也叫 Zset)。
在这里插入图片描述

3.1 简单介绍下 String?

String 是字符串,key 是字符串,value 是:字符串、数字、二进制。
字符串的主要使用场景:缓存、计数、共享 Session、限速。

3.2 简单介绍下 Hash?

Hash 是哈希,也叫键值对集合,key 是字符串,value 是 Map 集合,比如说 value = { name : ‘沉默王二’, age : 18 }。
哈希的主要使用场景:缓存用户信息、缓存对象。

3.3 为什么使用 Hash 类型而不使用 String 类型序列化存储?

在这里插入图片描述
使用 Hash 比使用 String 存取更方便。

3.4 简单介绍下 List?

List 是列表,按照插入顺序排序。可以添加一个元素到列表的头部(左边)或者尾部(右边)。
列表的主要使用场景:消息队列、文章列表。

3.5 简单介绍下 Set?

Set 是无序集合,元素是唯一的,不允许重复。
无序集合的主要使用场景:去重存储、集合运算、​​随机抽取。

3.6 简单介绍下 Zset?

Zset 是有序集合,比 Set 多了一个排序属性 score。
有序集合的主要使用场景:排行榜、延时任务、范围查询、加权统计。
在这里插入图片描述

4、Redis 为什么快呢?

Redis 的速度非常快,单机的 Redis 就可以支撑每秒十几万的并发,性能是 MySQL 的几十倍。原因主要有几点:

  1. 基于内存的数据存储:Redis 将数据存储在内存当中,使得数据的读写操作避开了磁盘 I/O。而内存的访问速度远超硬盘,这是 Redis 读写速度快的根本原因。
  2. 单线程模型:Redis 使用单线程模型来处理客户端的请求,这意味着在任何时刻只有一个命令在执行。这样就避免了线程切换和锁竞争带来的消耗。
  3. IO 多路复⽤:基于 Linux 的 select/epoll 机制。该机制允许内核中同时存在多个监听套接字和已连接套接字,内核会一直监听这些套接字上的连接请求或者数据请求,一旦有请求到达,就会交给 Redis 处理,就实现了所谓的 Redis 单个线程处理多个 IO 读写的请求。
  4. 高效的数据结构:Redis 提供了多种高效的数据结构,如字符串(String)、哈希(Hash)、列表(List)、集合(Set)、有序集合(Sorted Set)等,这些数据结构经过了高度优化,能够支持快速的数据操作。

5、能说一下 I/O 多路复用吗?

IO 多路复用是一种高效管理多个 IO 事件的技术,通过单线程监控多个文件描述符(fd),实现高并发的 IO 操作。
常见的 I/O 多路复用机制包括 select、poll 和 epoll 等。Redis 就是使用 epoll 这样的 I/O 多路复用机制,在单线程模型下实现高效的网络 I/O,从而支持高并发的请求处理。

5.1 举例子说一下 I/O 多路复用?

假设你是一个老师,让 30 个学生解答一道题目,然后检查学生做的是否正确,你有下面几个选择:

  1. 按顺序逐个检查,先检查 A,然后是 B。这中间如果有一个学生卡住,全班都会被耽误。也就是用循环挨个处理 socket,根本不具有并发能力。
  2. 创建 30 个分身,每个分身检查一个学生的答案是。这种类似于为每一个用户创建一个进程或者线程处理连接。
  3. 站在讲台上等,谁解答完谁举手。这时 C、D 举手,表示他们解答问题完毕,你下去依次检查 C、D 的答案,然后继续回到讲台上等。此时 E、A 又举手,然后去处理 E 和 A。

第一种就是阻塞 IO 模型,第三种就是 I/O 复用模型。

在这里插入图片描述
Linux 系统有三种方式实现 IO 多路复用:select、poll 和 epoll。例如 epoll 方式是将用户 socket 对应的 fd 注册进 epoll,然后 epoll 帮你监听哪些 socket 上有消息到达,这样就避免了大量的无用操作。此时的 socket 应该采用非阻塞模式。这样,整个过程只在进行 select、poll、epoll 这些调用的时候才会阻塞,收发客户消息则不会阻塞,整个进程或者线程就被充分利用起来,这就是事件驱动,所谓的 reactor 模式。

5.2 select、poll 和 epoll 的实现原理?

  1. select 使用位图管理 fd,每次调用都需要将 fd 集合从用户态复制到内核态。最大支持 1024 个文件描述符。
  2. poll 使用动态数组管理 fd,突破了 select 的数量限制。
  3. epoll 使用红黑树和链表管理 fd,每次调用只需要将 fd 集合从用户态复制到内核态一次,不需要重复复制。

6、Redis 为什么早期选择单线程?

官方 FAQ 表示,因为 Redis 是基于内存的操作,CPU 成为 Redis 的瓶颈的情况很少见,Redis 的瓶颈最可能是内存的大小或者网络限制。如果想要最大程度利用 CPU,可以在一台机器上启动多个 Redis 实例。

PS:网上有回答吐槽官方的解释有些敷衍,其实就是历史原因,开发者嫌多线程麻烦,后来这个 CPU 的利用问题就被抛给了使用者。

同时 FAQ 里还提到了, Redis 4.0 之后开始变成多线程,除了主线程外,也有后台线程在处理一些较为缓慢的操作,例如清理脏数据、无用连接的释放、大 Key 的删除等等。

7、Redis 6.0 使用多线程是怎么回事?

单线程模型意味着 Redis 在大量 IO 请求时,无法充分利用多核 CPU 的优势。在 Redis 6.0 中,多线程主要用来处理网络 IO 操作,命令解析和执行仍然是单线程完成,这样既可以发挥多核 CPU 的优势,又能避免锁和上下文切换带来的性能损耗。

8、说说 Redis 常用命令?

  1. 操作字符串的命令有:
  • SET key value:设置键 key 的值为 value。
  • GET key:获取键 key 的值。
  • DEL key:删除键 key。
  • INCR key:将键 key 存储的数值加 1。
  • DECR key:将键 key 存储的数值减 1。
  1. 操作哈希的命令有:
  • HSET key field value:向键为 key 的哈希表中设置字段 field 的值为 value。
  • HGET key field:获取键为 key 的哈希表中字段 field 的值。
  • HGETALL key:获取键为 key 的哈希表中所有的字段和值。
  • HDEL key field:删除键为 key 的哈希表中的一个或多个字段。
  1. 操作列表的命令有:
  • LPUSH key value:将一个值插入到列表 key 的头部。
  • RPUSH key value:将一个值插入到列表 key 的尾部。
  • LPOP key:移除并返回列表 key 的头元素。
  • RPOP key:移除并返回列表 key 的尾元素。
  • LRANGE key start stop:获取列表 key 中指定范围内的元素。
  1. 操作无序集合的命令有:
  • SADD key member:向集合 key 添加一个元素。
  • SMEMBERS key:返回集合 key 中的所有元素。
  • SREM key member:从集合 key 中移除一个元素。
  1. 操作有序集合的命令有:
    ZADD key score member:向有序集合 key 添加一个成员,或更新其分数。
    ZRANGE key start stop [WITHSCORES]:按照索引区间返回有序集合 key 中的成员,可选参数返回分数。
    ZREVRANGE key start stop [WITHSCORES]:返回有序集合 key 中,指定区间内的成员,按分数递减。
    ZREM key member:移除有序集合 key 中的一个或多个成员。

8.1 详细说说 set 命令?

在 Redis 中,设置键值对的命令是 set。set 命令有几个常用的参数:

  1. 可以通过 EX 或 PX 为键设置过期时间。
  2. NX 选项表示只有键不存在时才设置。
  3. XX 选项表示只有键存在时才设置。

8.2 SADD 命令的时间复杂度是多少?

向指定 Set 中添加 1 个或多个 member,如果指定 Set 不存在,会自动创建一个。时间复杂度 O(N) ,N 为添加的 member 个数。

8.3 INCR 命令了解吗?

INCR 命令是 Redis 中的一个原子操作,用于将存储在 key 中的数值加 1。Redis 的单线程模型确保每个命令都是原子执行的,不会被其他命令打断。

9、单线程 Redis 的 QPS 是多少?

Redis 的 QPS(Queries Per Second,每秒查询率)受多种因素影响,包括硬件配置(如 CPU、内存、网络带宽)、数据模型、命令类型、网络延迟等。根据官方的基准测试,一个普通服务器的 Redis 实例通常可以达到每秒数万到数十万的 QPS。(我的电脑是 2w)

10、Redis 持久化⽅式有哪些?有什么区别?

Redis 的持久化机制保证了 Redis 服务器在重启后数据不丢失,通过 RDB 和 AOF 文件来恢复内存中原有的数据。这两种持久化方式可以单独使用,也可以同时使用。

10.1 说一下 RDB?

RDB(Redis Database Backup File)是 Redis 的 ​快照持久化​​机制,通过在指定时间点生成内存数据的二进制快照(Snapshot),将数据保存到磁盘文件(默认文件名 dump.rdb)。可通过 save 和 bgsave 命令两个命令来手动触发 RDB 持久化操作:

  1. save 命令:会同步地将 Redis 的所有数据保存到磁盘上的一个 RDB 文件中。这个操作会阻塞所有客户端请求直到 RDB 文件被完全写入磁盘。
  2. bgsave 命令:会在后台异步地创建 Redis 的数据快照,并将快照保存到磁盘上的 RDB 文件中。命令会立即返回,Redis 服务器可以继续处理客户端请求。快照的创建过程是由一个子进程完成的。(推荐使用)

以下场景会自动触发 RDB 持久化:

  1. 在 Redis 配置文件(通常是 redis.conf)中,可以通过 save 指令配置自动触发 RDB 持久化的条件。这个指令可以设置多次,每个设置定义了一个时间间隔(秒)和该时间内发生的变更次数阈值。
save 900 1 # 如果至少有 1 个键被修改,900 秒后自动触发一次 RDB 持久化。
save 300 10 # 如果至少有 10 个键被修改,300 秒后自动触发一次 RDB 持久化。
save 60 10000 # 如果至少有 10000 个键被修改,60 秒后自动触发一次 RDB 持久化。
  1. 当 Redis 服务器通过 SHUTDOWN 命令正常关闭时,如果没有禁用,Redis 会自动执行一次 RDB 持久化,以确保数据在下次启动时能够恢复。
  2. 在 Redis 复制场景中,当一个 Redis 实例被配置为从节点并且与主节点建立连接时,它可能会根据配置接收主节点的 RDB 文件来初始化数据集。这个过程中,主节点会在后台自动触发 RDB 持久化,然后将生成的 RDB 文件发送给从节点。

10.2 说一下 AOF?

AOF(Append-Only File)​ 持久化通过记录每个写操作命令并将其追加到 AOF 文件中工作,恢复时通过重新执行这些命令来重建数据集。AOF 主要解决了数据持久化的实时性,目前已经是 Redis 持久化的主流方式。工作流程分为四个步骤:命令写入、文件同步、文件重写、重启加载:

  1. 命令写入:当 AOF 持久化机制被启用时,Redis 服务器会将接收到的所有写命令追加到 AOF 缓冲区的末尾。
  2. 文件同步:接着将缓冲区中的命令刷新到磁盘的 AOF 文件中,刷新策略有三种:
  • always:每次写命令都会同步到 AOF 文件。
  • everysec(默认):每秒同步一次。如果系统崩溃,可能会丢失最后一秒的数据。
  • no:在这种模式下,如果发生宕机,那么丢失的数据量由操作系统内核的缓存冲洗策略决定。
  1. 文件重写:随着 AOF 文件的不断增长,Redis 会启用重写机制来生成一个更小的 AOF 文件:
  • 将内存中每个键值对的当前状态转换为一条最简单的 Redis 命令,写入到一个新的 AOF 文件中。即使某个键被修改了多次,在新的 AOF 文件中也只会保留最终的状态。
  • Redis 会 fork 一个子进程,子进程负责重写 AOF 文件,主进程不会被阻塞。
  1. 重启加载:当 Redis 服务器重启时,会读取 AOF 文件中的所有命令并重新执行它们,以恢复重启前的内存状态。

10.3 AOF 文件存储的是什么类型的数据?

AOF 文件存储的是 Redis 所有的写操作命令,比如 SET、HSET、INCR 等。

10.4 AOF 重写期间命令可能会写入两次,会造成什么影响?

AOF 重写期间,Redis 会将新的写命令同时写入旧的 AOF 文件和重写缓冲区。这样会带来额外的磁盘开销。但可以防止在 AOF 重写尚未完成时,Redis 发生崩溃,导致数据丢失。即使重写失败,旧的 AOF 文件仍然是完整的。当重写完成后,会通过原子操作将新的 AOF 文件替换旧的 AOF 文件。

11、RDB 和 AOF 各自有什么优缺点?

  1. RDB 是一个非常紧凑的单文件(二进制文件 dump.rdb),代表了 Redis 在某个时间点上的数据快照。非常适合用于备份数据,比如在夜间进行备份,然后将 RDB 文件复制到远程服务器。但可能会丢失最后一次持久化后的数据。
  2. AOF 的最大优点是灵活,实时性好,可以设置不同的 fsync 策略,如每秒同步一次,每次写入命令就同步。但 AOF 文件往往比较大,恢复速度慢,因为它记录了每个写操作。

12、RDB 和 AOF 如何选择?

如果需要尽可能减少数据丢失,AOF 是更好的选择。尤其是在频繁写入的环境下,设置 AOF 每秒同步可以最大限度减少数据丢失。如果首要考虑性能,RDB 可能更适合。RDB 的快照生成对性能影响较小,并且数据恢复速度快。如果系统需要经常重启,并且希望系统重启后快速恢复,RDB 可能是更好的选择。虽然 AOF 也提供了良好的恢复能力,但重写 AOF 文件可能会比较慢。在许多生产环境中,同时启用 RDB 和 AOF 被认为是最佳实践:

  1. 使用 RDB 进行快照备份。
  2. 使用 AOF 保证崩溃后的最大数据完整性。

13、Redis 的数据恢复?

当 Redis 中的数据丢失时,可以从 RDB 或者 AOF 中恢复数据。可以将 RDB 文件或者 AOF 文件复制到 Redis 的数据目录下,然后重启 Redis 服务,Redis 会自动加载数据文件并恢复数据。

Redis 启动时加载数据的流程:

  1. AOF 开启且存在 AOF 文件时,优先加载 AOF 文件。
  2. AOF 关闭或者 AOF 文件不存在时,加载 RDB 文件。

14、Redis 4.0 的混合持久化了解吗?

RDB 持久化是通过创建数据的快照来保存数据的,而 AOF 持久化则是通过记录每个写入命令来保存数据的。

两种方式各有优缺点。RDB 持久化的优点是恢复大数据集的速度比较快,但是可能会丢失最后一次快照以后的数据。AOF 持久化的优点是数据的完整性比较高,通常只会丢失一秒的数据,但是对于大数据集,AOF 文件可能会比较大,恢复的速度比较慢。

在 Redis 4.0 版本中,混合持久化模式会在 AOF 重写的时候同时生成一份 RDB 快照,然后将这份快照作为 AOF 文件的一部分,最后再附加新的写入命令。这样,当需要恢复数据时,Redis 先加载 RDB 文件来恢复到快照时刻的状态,然后应用 RDB 之后记录的 AOF 命令来恢复之后的数据更改,既快又可靠。
在这里插入图片描述

14.1 如何设置持久化模式?

可以通过编辑 Redis 的配置文件 redis.conf 来进行设置,或者在运行时通过 Redis 命令行动态调整。

RDB 持久化通过在配置文件中设置快照(snapshotting)规则来启用。这些规则定义了在多少秒内如果有多少个键被修改,则自动执行一次持久化操作。AOF 持久化是通过在配置文件中设置 appendonly 参数为 yes 来启用的,此外,还可以配置 AOF 文件的写入频率,这是通过 appendfsync 设置的。

15、Redis 的高可用?

Redis 除了单机部署外,还可以通过主从复制、哨兵模式和集群模式来实现高可用。

  1. 主从复制:允许一个 Redis 服务器(主节点)将数据复制到一个或多个 Redis 服务器(从节点)。实现读写分离,适合读多写少的场景。
  2. 哨兵模式:用于监控主节点和从节点的状态,实现自动故障转移。如果主节点发生故障,哨兵可以自动的将一个从节点升级为新的主节点。
  3. 集群模式:Redis 集群通过分片的方式存储数据,每个节点存储数据的一部分,用户请求可以并行处理。集群模式支持自动分区、故障转移,并且可以在不停机的情况下进行节点增加或删除。

16、主从复制了解吗?

主从复制是指将一台 Redis 服务器的数据,复制到其他的 Redis 服务器。前者称为主节点 master,后者称为从节点slave。且数据的复制是单向的,只能由主节点到从节点。在 Redis 主从架构中,主节点负责处理所有的写操作,并将这些操作异步复制到从节点。从节点主要用于读取操作,以分担主节点的压力和提高读性能。

16.1 主从复制主要的作用是什么?

  1. 数据冗余: 主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
  2. 故障恢复: 如果主节点挂掉了,可以将一个从节点提升为主节点,从而实现故障的快速恢复。通常会使用 Sentinel 哨兵来实现自动故障转移,当主节点挂掉时,Sentinel 会自动将一个从节点升级为主节点,保证系统的可用性。假如是从节点挂掉了,主节点不受影响,但应该尽快修复并重启挂掉的从节点,使其重新加入集群并从主节点同步数据。
  3. 负载均衡: 在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务 (即写 Redis 时连接主节点,读 Redis 时连接从节点),分担服务器负载。在读多写少的场景下,通过多个从节点分担读负载,可以提高 Redis 服务器的并发量。
  4. 高可用基石: 除了上述作用以外,主从复制还是哨兵和集群能够实施的基础。

16.2 主从复制出现数据不一致怎么办?

Redis 的主从复制是异步进行的,这意味着主节点在执行完写操作后,会立即返回给客户端,而不是等待从节点完成数据同步。在主节点将数据同步到从节点的过程中,可能会出现网络延迟或中断,从而导致从节点的数据滞后。为了解决数据不一致的问题,Redis 提供了 INFO replication 命令监控主从节点的复制进度,及时发现和处理复制延迟。

16.3 Redis 解决单点故障主要靠什么?

主从复制,当主节点发生故障时,可以通过手动或自动方式将某个从节点升级为新的主节点,继续对外提供服务,从而避免单点故障。Redis 的哨兵机制(Sentinel)可以实现自动化的故障转移,当主节点宕机时,哨兵会自动将一个从节点升级为新的主节点。另外,集群模式下,当某个节点发生故障时,Redis Cluster 会自动将请求路由到其他节点,并通过从节点进行故障恢复。

17、Redis 主从有几种常见的拓扑结构?

Redis 的复制拓扑结构可以支持单层或多层复制关系,根据拓扑的复杂性可以分为以下三种:一主一从、一主多从、树状主从结构。

  1. 一主一从结构:最简单的复制拓扑结构,用于主节点出现宕机时从节点提供故障转移支持。
  2. 一主多从结构:使得应用端可以利用多个从节点实现读写分离。读占比较大的场景下,可以把读命令发送到从节点来分担主节点压力。
  3. 树状主从结构:使得从节点不但可以复制主节点数据,同时可以作为其他从节点的主节点继续向下层复制。通过引入复制中间层,可以有效降低主节点负载和需要传送给从节点的数据量。

18、Redis 的主从复制原理了解吗?

  1. 保存信息:保存主节点的 ip 和 port。
  2. 建立连接:从节点发现新的主节点后,会尝试和主节点建立网络连接。
  3. 发送 ping 命令: 连接建立成功后从节点发送 ping 请求进行首次通信。
  4. 权限验证:如果主节点要求密码验证,从节点必须输入正确的密码才能通过验证。
  5. 同步数据集:主从复制连接正常通信后,主节点会把持有的数据全部发送给从节点。
  6. 命令持续复制:接下来主节点会持续地把写命令发送给从节点,保证主从数据一致性。

19、说说主从数据同步的方式?

使用 psync 命令完成主从数据同步,同步过程分为:全量复制和部分复制。

  1. 全量复制:一般用于初次复制场景,Redis 早期支持的复制功能只有全量复制,它会把主节点全部数据一次性发送给从节点,当数据量较大时,会对主从节点和网络造成很大的开销。
  2. 部分复制:主要是 Redis 针对全量复制的过高开销做出的一种优化措施。

20、主从复制存在哪些问题呢?

Redis 主从复制虽然实现了读写分离和数据备份,但也存在一些明显的缺点:

  1. 数据丢失问题:由于主从复制是异步的,如果主节点在数据尚未完全同步到从节点时崩溃,会导致数据丢失。
  2. 写操作压力大:写操作集中在主节点,从节点只能处理读操作。
  3. 脑裂:主节点和从节点可能无法相互通信,导致两个节点都被认为是主节点,形成多个主节点的情况。

20.1 脑裂问题了解吗?

Redis 的脑裂问题是指在主从模式或集群模式下,由于网络分区或节点故障,可能导致系统中出现多个主节点,从而引发数据不一致、数据丢失等问题。可以通过 Sentinel 模式和 Cluster 模式中的投票机制和强制下线机制来解决。

21、Redis 哨兵了解吗?

哨兵(Sentinel)机制是 Redis 提供的一个高可用性解决方案,主要用来监控 Redis 主从架构中的实例,并在主节点出现故障时,自动进行故障转移。
在这里插入图片描述

22、Redis 哨兵实现原理知道吗?

哨兵的工作流程包括定时监控、主观下线和客观下线、领导者 Sentinel 节点选举、故障转移等。
在这里插入图片描述
每个 Sentinel 实例会定期通过 PING 命令向主节点和从节点发送心跳包。如果一个节点长时间没有响应 PING 命令,Sentinel 会将该节点标记为主观下线。当多个 Sentinel 同时认为一个节点不可用时,该节点被标记为客观下线。当主节点被确认下线后,Sentinel 之间会通过 Raft 算法协商,选出一个领导者 Sentinel 来负责执行故障转移。
在这里插入图片描述

  1. 将某个从节点提升为新的主节点。
  2. 通知其他从节点重新复制新的主节点的数据。

23、领导者 Sentinel 节点选举了解吗?

Redis 使用 Raft 算法实现领导者选举的:当主节点挂掉后,新的主节点是由剩余的从节点发起选举后升级的。

  1. 每个在线的 Sentinel 节点都有资格成为领导者,当它确认主节点下线时候,会向其他哨兵节点发送命令,表明希望由自己来执行主从切换,并让所有其他哨兵进行投票。这个投票过程称为“Leader 选举”。
  2. 收到请求的 Sentinel 节点会进行判断,如果候选者的日志与自己的日志一样新,runid 也小于自己,并且之前未投票过,就会同意投票,回复 Y。否则回复 N。
  3. 候选者收到投票后会统计支持自己的得票数,如果候选者获得了集群中超过半数节点的投票支持,它将成为新的主节点。之后会向其他从节点发送心跳信号,告诉它们自己已成为主节点,并将其他节点的状态重置为从节点。
  4. 如果出现选票分裂。也就是没有候选者获得超过半数的选票,这次选举就会失败,所有候选者都会再次发起选举。为了防止无限制的选举失败,每个节点都会有一个选举超时时间,且是随机的。

24、Redis 集群了解吗?

主从存在高可用和分布式的问题,哨兵解决了高可用的问题,而集群就是终极方案,解决高可用和分布式问题。
在这里插入图片描述

  1. 高可用: 集群支持主从复制和主节点的自动故障转移 (与哨兵类似),当任一节点发生故障时,集群仍然可以对外提供服务。
  2. 数据分区: (或称数据分片) 是集群最核心的功能。集群将数据分散到多个节点,一方面突破了 Redis 单机内存大小的限制,存储容量大大增加。另一方面每个主节点都可以对外提供读服务和写服务,大大提高了集群的响应能力。

25、Redis Cluster 了解吗?

Redis Cluster 是一种将数据分片存储在多个 Redis 实例上的集群架构,每个 Redis 实例负责存储部分数据。比如说把 25G 的数据平均分为 5 份,每份 5G,然后启动 5 个 Redis 实例,每个实例保存一份数据。在 Redis 3.0 之后,官方提供了 Redis Cluster,数据和实例之间的映射通过哈希槽(hash slot)来实现。Redis Cluster 有 16384 个哈希槽,每个键根据其名字的 CRC16 值被映射到这些哈希槽上。然后,这些哈希槽会被均匀地分配到所有的 Redis 实例上。例如,我们有 3 个 Redis 实例,那么每个实例可能会负责大约 5461 个哈希槽。当需要存储或检索一个键值对时,Redis Cluster 会先计算这个键的哈希槽,然后找到负责这个哈希槽的 Redis 实例,最后在这个实例上进行操作。

26、集群中数据如何分区?

在 Redis 集群中,数据分区是通过将数据分散到不同的节点来实现的,常见的数据分区规则有三种:节点取余分区、一致性哈希分区、虚拟槽分区。

  1. 节点取余分区:最简单的分区策略,数据项通过对键的哈希值进行取余操作来分配到不同的节点。缺点是扩缩容时大多数数据需要重新分配。
  2. 一致性哈希分区:将哈希值空间组织成一个环,数据项和节点都被映射到这个环上。数据项由其哈希值直接映射到环上,然后顺时针分配到遇到的第一个节点,从而来减少节点变动时数据迁移的量。缺点是可能分布不均,故障转移会影响后续节点。
  3. 虚拟槽分区:槽位的数量是固定的(例如 Redis Cluster 有 16384 个槽),每个键通过哈希算法(如 CRC16)映射到这些槽上,每个集群节点负责管理一定范围内的槽。这种分区可以实现平滑扩缩容;数据分布也更加均匀,Redis Cluster 采用的正是这种分区方式。

27、能说说 Redis 集群的原理吗?

Redis 集群通过数据分区来实现数据的分布式存储,通过自动故障转移实现高可用。

集群创建:

  1. 设置节点:Redis 集群一般由多个节点组成,节点数量至少为 6 个才能保证组成完整高可用的集群。每个节点需要开启配置 cluster-enabled yes,让 Redis 运行在集群模式下。
  2. 节点握手:节点握手指一批运行在集群模式下的节点通过 Gossip 协议彼此通信, 达到感知对方的过程。节点握手是集群彼此通信的第一步,由客户端发起命令:cluster meet{ip}{port}。完成节点握手之后,Redis 节点就组成了一个多节点的集群。
  3. 分配槽:Redis 集群把所有的数据映射到 16384 个槽中。每个节点对应若干个槽,只有当节点分配了槽,才能响应和这些槽关联的键命令。通过 cluster addslots 命令为节点分配槽。

故障转移:

  1. 故障发现:Redis 集群内节点通过 ping/pong 消息实现节点通信,集群中的每个节点都会定期向其他节点发送 ping 消息,接收节点回复 pong 消息作为响应。如果在 cluster-node-timeout 时间内通信一直失败,则发送节点会认为接收节点存在故障,把接收节点标记为主观下线(pfail)状态。当半数以上持有槽的主节点都标记某个节点是主观下线时。触发客观下线流程。
  2. 故障恢复:故障节点变为客观下线后,如果下线节点是持有槽的主节点,则需要在它的从节点中选出一个替换它,从而保证集群的高可用。
    在这里插入图片描述
    在这里插入图片描述

27.1 部署 Redis 集群至少需要几个物理节点?

投票选举的环节,故障主节点也算在投票数内,假设集群内节点规模是 3 主 3 从,其中有 2 个主节点部署在一台机器上,当这台机器宕机时,由于从节点无法收集到 3/2+1 个主节点选票将导致故障转移失败。这个问题同样适用于故障发现环节。因此部署集群时所有主节点最少需要部署在 3 台物理机上才能避免单点问题。

28、说说集群的伸缩?

Redis 集群使用数据分片和哈希槽的机制将数据分布到不同的节点上。集群扩容和缩容的关键,在于槽和节点之间的对应关系。当需要扩容时,新的节点被添加到集群中,集群会自动执行数据迁移,重新分布哈希槽到新的节点。数据迁移的过程可以确保在扩容期间数据的正常访问和插入。
在这里插入图片描述
数据正在迁移时,客户端请求可能被路由到原有节点或新节点。Redis Cluster 会根据哈希槽的映射关系判断请求应该被路由到哪个节点,并在必要时进行重定向。如果请求被路由到正在迁移数据的哈希槽,Redis Cluster 会返回一个 MOVED 响应,指示客户端重新路由请求到正确的目标节点。这种机制也就保证了数据迁移过程中的最终一致性。当需要缩容时,Redis 集群会将槽从要缩容的节点上迁移到其他节点上,然后将要缩容的节点从集群中移除。

29、缓存击穿、缓存穿透、缓存雪崩了解吗?

缓存穿透、缓存击穿和缓存雪崩是指在使用 Redis 做缓存时可能遇到的三种高并发场景下的问题。

29.1 什么是缓存穿透?

缓存穿透是指查询不存在的数据,由于缓存没有命中(因为数据根本就不存在),请求每次都会穿过缓存去查询数据库。如果这种查询非常频繁,就会给数据库造成很大的压力。缓存穿透意味着缓存失去了减轻数据压力的意义。缓存穿透可能有两种原因:自身业务代码问题、恶意攻击。
在这里插入图片描述
主要有两种解决方案:

  1. 缓存空值/默认值
    客户端请求某个 ID 的数据,首先检查缓存是否命中。如果缓存未命中,查询数据库。如果数据库查询结果为空,将该空结果(如 null 或 {})缓存起来,并设置一个合理的过期时间。当后续请求再访问相同 ID 时,缓存直接返回空结果,避免每次都打到数据库。
    在这里插入图片描述
  2. 布隆过滤器
    通过布隆过滤器存储所有可能存在的合法数据的键,当请求到达时,先通过布隆过滤器判断该键是否存在:
  • 如果布隆过滤器认为该键不存在,直接返回空,不会查询数据库。
  • 如果布隆过滤器认为该键可能存在,则查询缓存和数据库。
    在这里插入图片描述
    两种解决方案的对比:
    在这里插入图片描述

29.2 什么是缓存击穿?

缓存击穿是指一个或几个数据被高频访问,当这些数据在缓存中过期的那一刻,大量请求就会直接到达数据库,导致数据库瞬间压力过大。
在这里插入图片描述
主要有两种解决方案:

  1. 加锁更新:比如请求查询 A,发现缓存中没有,对 A 这个 key 加锁,同时去数据库查询数据,写入缓存,再返回给用户,这样后面的请求就可以从缓存中拿到数据了。
    在这里插入图片描述
  2. 过期时间更新:通过异步的方式动态延长 TTL,防止缓存击穿。

29.3 什么是缓存雪崩?

缓存雪崩是指在某一个时间点,由于大量的缓存数据同时过期或缓存服务器突然宕机了,导致所有的请求都落到数据库上(比如 MySQL),从而对数据库造成巨大压力,甚至导致数据库崩溃的现象。
在这里插入图片描述
主要有四种解决方案:

  1. 集群部署:采用分布式缓存而不是单一缓存服务器,可以降低单点故障的风险。即使某个缓存节点发生故障,其他节点仍然可以提供服务,从而避免对数据库的大量直接访问。可以利用 Redis Cluster。
    在这里插入图片描述
  2. 多级缓存:对于关键数据,除了在主缓存中存储,还可以在备用缓存中保存一份。当主缓存不可用时,可以快速切换到备用缓存,确保系统的稳定性和可用性。
  3. 随机过期时间:对于缓存数据,设置不同的过期时间,避免大量缓存数据同时过期。可以在原有过期时间的基础上添加一个随机值来实现,这样可以分散缓存过期时间,减少同一时间对数据库的访问压力。
  4. 限流和降级:可以设置合理的系统限流策略,如令牌桶或漏斗算法,来控制访问流量,防止在缓存失效时数据库被击垮。还可以实现降级策略,在缓存雪崩或系统压力过大时,暂时关闭一些非核心服务,确保核心服务正常运行。

30、能说说布隆过滤器吗?

布隆过滤器是一种空间效率极高的概率型数据结构,用于快速检查一个元素是否存在于一个集合中。
在这里插入图片描述
布隆过滤器由一个长度为 m 的位数组和 k 个哈希函数组成。

  • 开始时,布隆过滤器的每个位都被设置为 0。
  • 当一个元素被添加到过滤器中时,它会被 k 个哈希函数分别计算得到 k 个位置,然后将位数组中对应的位设置为 1。
  • 当检查一个元素是否存在于过滤器中时,同样使用 k 个哈希函数计算位置,如果任一位置的位为 0,则该元素肯定不在过滤器中;如果所有位置的位都为 1,则该元素可能在过滤器中。

30.1 布隆过滤器存在误判吗?

布隆过滤器的优点是空间效率和查询时间都远远超过一般的算法,缺点是存在误判和删除困难。当布隆过滤器保存的元素越多,被置为 1 的 bit 位就会越多。假设元素 x 没有存储过,但其他元素的哈希函数映射到位数组的三个位刚好都为 1 且恰好覆盖了元素 x 映射的位置,那么对于布隆过滤器来讲,元素 x 这个值就是存在的。

布隆过滤器的误判率取决于:

  1. 位数组的大小(m):位数组的大小决定了可以存储的标志位数量。如果位数组的大小过小,那么哈希碰撞的几率就会增加,从而导致更高的误判率。
  2. 哈希函数的数量(k):哈希函数的数量决定了每个元素在位数组中标记的位数。如果哈希函数太少,则过滤器很快会变得不精确;如果太多,误判率也会升高,效率下降。
  3. 存入的元素数量(n):存入的元素越多,哈希碰撞的几率越大,从而导致更高的误判率。

30.2 布隆过滤器支持删除吗?

布隆过滤器其实并不支持删除元素,因为多个元素可能哈希到一个布隆过滤器的同一个位置,如果直接删除该位置的元素,则会影响其他元素的判断。

30.3 为什么不能用哈希表而是用布隆过滤器?

布隆过滤器是一种基于位数组和多个哈希函数的概率型数据结构,适合在内存资源有限、数据量大且能容忍一定误判的场景下使用。哈希表虽然也可以精准判断,但需要存储实际数据,内存开销大,不适合大规模数据存储。

30.4 布隆过滤器的优点?

  1. 内存效率高:布隆过滤器只需要存储每个元素的哈希值,而不需要存储元素本身,因此内存占用非常小。
  2. 查询速度快:布隆过滤器只需要将元素通过多个哈希函数映射到位数组,并检查位状态即可。它不需要哈希表那样的复杂键值操作,时间复杂度接近常数时间,速度非常快。

31、如何保证缓存和数据库的数据⼀致性?

目前最流行的缓存读写策略 Cache Aside Pattern(旁路缓存模式)就是采用的先写数据库,再删缓存的方式。在项目中,我采用的是先写 MySQL,再删除 Redis 的方式来保证缓存和数据库的数据一致性。

  • 查询缓存成功:应用程序从缓存读数据,数据存在,直接返回。
  • 查询缓存失败:应用程序从缓存读数据,数据不存在,再从数据库中读数据,成功后,放入缓存。
  • 数据更新:先更新数据库,成功后,再删除缓存。
    先删除缓存是因为删除缓存的速度比更新缓存的速度要快得多。先更新数据库是因为如果先删除缓存可能会有其他线程查到数据库中旧的值写入缓存,从而出现数据库与缓存数据不一致的问题,发生概率很高是因为删除缓存速度快得多。PS:在高并发不保证强一致的情况下可以先更新缓存再写入数据库。

31.1 那假如对一致性要求很高,该怎么办呢?

缓存和数据库数据不一致的原因,常见的有两种:缓存删除失败、并发导致写入了脏数据。通常有四种方案可以解决:

  1. 引入消息队列保证缓存被删除:使用消息队列(如 Kafka、RabbitMQ)保证数据库更新和缓存更新之间的最终一致性。当数据库更新完成后,将更新事件发送到消息队列。有专门的服务监听这些事件并负责更新或删除缓存。
  2. 数据库订阅+消息队列保证缓存被删除:可以专门起一个服务(比如 Canal,阿里巴巴 MySQL binlog 增量订阅消费组件)去监听 MySQL 的 binlog,获取需要操作的数据。但增加了整个系统的复杂度。
  3. 延时双删防止脏数据:在第一次删除缓存之后,过一段时间之后,再次删除缓存。主要针对缓存不存在,但写入了脏数据的情况。在先删缓存,再写数据库的更新策略下发生的比较多。
  4. 设置缓存过期时间兜底:兜底策略,给缓存设置一个的过期时间,即使发生了缓存和数据库的数据不一致问题,也不会永远不一致下去,缓存过期后,自然就一致了。

32、如何保证本地缓存和分布式缓存的一致?

  1. 设置本地缓存的过期时间,这是最简单也是最直接的方法,当本地缓存过期时,就从 Redis 缓存中去同步。
  2. 使用 Redis 的 Pub/Sub 机制,当 Redis 缓存发生变化时,发布一个消息,本地缓存订阅这个消息,然后删除对应的本地缓存。
  3. Redis 缓存发生变化时,引入消息队列,比如 RocketMQ、RabbitMQ 去更新本地缓存。

32.1 如果在项目中多个地方都要使用到二级缓存的逻辑,如何设计这一块?

在设计时,应该清楚地区分何时使用一级缓存和何时使用二级缓存。通常情况下,对于频繁访问但不经常更改的数据,可以放在本地缓存中以提供最快的访问速度。而对于需要共享或者一致性要求较高的数据,应当放在一级缓存中。

32.2 本地缓存和 Redis 缓存的区别和效率对比?

Redis 可以部署在多个节点上,支持数据分片,适用于跨服务器的缓存共享。而本地缓存只能在单个服务器上使用。Redis 还可以持久化数据,支持数据备份和恢复,适用于对数据安全性要求较高的场景。并且支持发布/订阅、事务、Lua 脚本等高级功能。效率上,Redis 和本地缓存都是存储在内存中,读写速度都非常快。

33、怎么处理热 key?

热 key,指在短时间内被频繁访问的键。热 key 可能会造成整体流量的不均衡(网络带宽、CPU 和内存资源),个别节点出现 OPS 过大的情况,极端情况下热点 key 甚至会超过 Redis 本身能够承受的 OPS。OPS(Operations Per Second)是 Redis 的一个重要指标,表示 Redis 每秒钟能够处理的命令数。

通常以 Key 被请求的频率来判定,比如:

  1. QPS 集中在特定的 Key:总的 QPS为 10000,其中一个 Key 的 QPS 飙到了 8000。
  2. 带宽使用率集中在特定的 Key:一个拥有上千成员且总大小为 1M 的哈希 Key,每秒发送大量的 HGETALL 请求。
  3. CPU 使用率集中在特定的 Key:一个拥有数万个成员的 ZSET Key,每秒发送大量的 ZRANGE 请求。
  • HGETALL 命令用于返回哈希表中,所有的字段和值。
  • ZRANGE 命令用于返回有序集中,指定区间内的成员。

对热 key 的处理,最关键的是对热 key 的监控:

  1. 客户端:距离 key 最近的地方,因为 Redis 命令就是从客户端发出的。可以在客户端设置全局字典,每次调用 Redis 命令时,使用这个字典进行记录。
  2. 代理端:比如 Codis 这样基于代理的 Redis 分布式架构,所有客户端的请求都是通过代理端完成的。可以在代理端进行监控。
  3. Redis 服务端:使用 monitor 命令统计热点 key 是很多开发和运维人员首先想到的方案,monitor 命令可以监控到 Redis 执行的所有命令。monitor 命令的使用:redis-cli monitor。还可以通过 bigkeys 参数分析热 Key。bigkeys 命令的使用:redis-cli --bigkeys。

只要监控到了热 key,对热 key 的处理就简单了:

  1. 热 key 打散到不同的服务器,降低压⼒。基本思路就是给热 Key 加上前缀或者后缀。
  2. 加⼊⼆级缓存,当出现热 Key 后,把热 Key 加载到 JVM 中,后续针对这些热 Key 的请求,直接从 JVM 中读取。这些本地的缓存工具有很多,比如 Caffeine、Guava 等,或者直接使用 HashMap 作为本地缓存都是可以的。
    PS:如果对热 Key 进行本地缓存,需要防止本地缓存过大。

34、缓存预热怎么做呢?

缓存预热是指在系统启动时,提前将一些预定义的数据加载到缓存中,以避免在系统运行初期由于缓存未命中所导致的性能问题。通过缓存预热,可以确保系统在上线后能够立即提供高效的服务,减少首次访问时的延迟。缓存预热的方法有多种,在项目中,我们采用了项目启动时自动加载的方式。

35、热点 key 重建?问题?解决?

开发的时候一般使用缓存+过期时间的策略,既可以加速数据读写,又保证数据的定期更新,这种模式基本能够满足绝大部分需求。但是有两个问题如果同时出现,可能就会出现比较大的问题:

  • 当前 key 是一个热点 key,并发量非常大。
  • 重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂的 SQL、多次 IO、多个依赖等。 在缓存失效的瞬间,有大量线程来重建缓存,造成后端负载加大,甚至可能会让应用崩溃。

要解决这个问题也不是很复杂,解决问题的要点在于:

  • 减少重建缓存的次数。
  • 数据尽可能一致。
  • 较少的潜在危险。

所以一般采用如下方式:

  1. 互斥锁:这种方法只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可。
  2. 永远不过期:包含两层意思:
  • 从缓存层面来看,确实没有设置过期时间,所以不会出现热点 key 过期后产生的问题,也就是物理不过期。
  • 从功能层面来看,为每个 value 设置一个逻辑过期时间,当发现超过过期时间后,会使用单独的线程去构建缓存。

36、无底洞问题吗?如何解决?

客户端的批量操作需要从不同节点上获取,涉及多次网络操作,批量操作会随着节点的增多,耗时不断增大。网络连接数变多,对节点的性能也有一定影响。常见的优化思路如下:

  1. 减少网络通信的次数。
  2. 命令本身的优化,例如优化操作语句等。
  3. 降低接入成本,例如客户端使用连接池、NIO 等。

37、Redis 报内存不足怎么处理?

Redis 内存不足有这么几种处理方式:

  1. 修改配置文件 redis.conf 的 maxmemory 参数,增加 Redis 可用内存。
  2. 也可以通过命令 set maxmemory 动态设置内存上限。
  3. 修改内存淘汰策略,及时释放内存空间。
  4. 使用 Redis 集群模式,进行横向扩容。

38、Redis key 过期策略有哪些?

Redis 的 key 过期回收策略主要有两种:惰性删除和定期删除。

  1. 惰性删除:当某个键被访问时,如果发现它已经过期,Redis 会立即删除该键,俗称惰性删除。如果一个已过期的键从未被访问,它就不会被删除,会占用额外的内存空间。
  2. 定期删除:即每隔一段时间,Redis 就会随机检查一些键是否过期,如果过期就删除。这种策略可以保证过期键及时被删除,但也会增加 Redis 的 CPU 消耗。

39、Redis 有哪些内存淘汰策略?

当 Redis 的内存使用达到最大值时,它会根据配置的内存淘汰策略来决定如何处理新的请求。
在这里插入图片描述
常见的策略有:

  1. noeviction:默认策略,不进行任何数据淘汰,直接返回错误信息。
  2. allkeys-lru:从所有键中,使用 LRU 算法淘汰最近最少使用的键。
  3. allkeys-lfu:从所有键中,使用 LFU 算法淘汰最少使用的键。
  4. volatile-lru:从设置了过期时间的键中淘汰最近最少使用的键。
  5. volatile-ttl:从设置了过期时间的键中淘汰即将过期的键。

40.1 LRU 和 LFU 的区别是什么?

LRU(Least Recently Used):基于时间维度,淘汰最近最少访问的键。适合访问具有时间特性的场景。
LFU(Least Frequently Used):基于次数维度,淘汰访问频率最低的键。更适合长期热点数据场景。

40、Redis 阻塞?怎么解决?

在这里插入图片描述

  1. API 或数据结构使用不合理:慢查询,在大对象上执行算法复杂度超过 O(n) 的命令。
  2. CPU 饱和:Redis 的 OPS 已经到了极限,可以通过水平扩展解决。
  3. 持久化相关的阻塞:fork 阻塞、AOF 刷盘阻塞、HugePage 写操作阻塞。

41、大 key 问题了解吗?

大 key 指的是存储了大量数据的键,比如:

  1. 单个简单的 key 存储的 value 很大,size 超过 10KB。
  2. hash,set,zset,list 中存储过多的元素(以万为单位)。

41.1 大 key 会造成什么问题呢?

  1. 客户端耗时增加,甚至超时。
  2. 造成 Redis 集群中数据倾斜。
  3. 主动删除、被动删等,可能会导致阻塞。
  4. 对大 key 进行 IO 操作时,会严重占用带宽和 CPU。

41.2 如何找到大 key?

bigkeys 参数:redis-cli --bigkeys。

41.3 如何处理大 key?

在这里插入图片描述

  1. 删除大 key:
  • Redis >= 4.0,可使用 UNLINK 命令非阻塞的删除大 key,逐步清理传入的大 Key。
  • Redis < 4.0,可使用 SCAN 命令执行增量迭代扫描 key,然后判断进行删除。
  1. 压缩和拆分 key:
  • 当 vaule 是 string 时,比较难拆分,则使用序列化、压缩算法将 key 的大小控制在合理范围内,但是序列化和反序列化都会带来额外的性能消耗。
  • 当 value 是 string 且压缩之后仍然是大 key 时,则需要进行拆分,将一个大 key 分为不同的部分,记录每个部分的 key,使用 multiget 等操作实现事务读取。
  • 当 value 是 list/set 等集合类型时,根据预估的数据规模来进行分片,不同的元素计算后分到不同的片。

42、Redis 常见性能问题和解决方案?

  1. Master 最好不要做任何持久化工作,包括内存快照和 AOF 日志文件,特别是不要启用内存快照做持久化。
  2. 如果数据比较关键,可以让某个 Slave 开启 AOF 备份数据,策略为每秒同步一次。
  3. 为了主从复制的速度和连接的稳定性,Slave 和 Master 最好在同一个局域网内。
  4. 尽量避免在压力较大的主库上增加从库。

43、使用 Redis 如何实现异步队列?

  1. 使用 list 作为队列,lpush 生产消息,rpop 消费消息。
  2. 使用 list 作为队列,lpush 生产消息,brpop 消费消息。
  3. 使用 Redis 的 pub/sub 来进行消息的发布/订阅。发布/订阅模式可以 1 对 N 的消息发布/订阅。发布者将消息发布到指定的频道频道,订阅相应频道的客户端都能收到消息,但是不可靠。

44、Redis 如何实现延时队列?

可以使用 Redis 的 zset(有序集合)来实现延时队列:

  1. 将任务添加到 zset 中,score 为任务的执行时间戳,value 为任务的内容。
  2. 定期(例如每秒)从 zset 中获取 score 小于当前时间戳的任务,然后执行任务。
  3. 任务执行后,从 zset 中删除任务。

45、Redis 支持事务吗?

Redis 支持简单的事务,可以将多个命令打包,然后一次性的,按照顺序执行。主要通过 multi、exec、discard、watch 等命令来实现:

  1. multi:标记一个事务块的开始。
  2. exec:执行所有事务块内的命令。
  3. discard:取消事务,放弃执行事务块内的所有命令。
  4. watch:监视一个或多个 key,如果在事务执行之前这个 key 被其他命令所改动,那么事务将被打断。

45.1 说一下 Redis 事务的原理?

在这里插入图片描述

  1. 使用 MULTI 命令开始一个事务。从这个命令执行之后开始,所有的后续命令都不会立即执行,而是被放入一个队列中。在这个阶段,Redis 只是记录下了这些命令。
  2. 使用 EXEC 命令触发事务的执行。一旦执行了 EXEC,之前 MULTI 后队列中的所有命令会被原子地执行。这些命令要么全部执行,要么全部不执行。
  3. 如果在执行 EXEC 之前决定取消事务,可以使用 DISCARD 命令,这会清空事务队列并退出事务状态。
  4. WATCH 命令用于实现乐观锁。WATCH 命令可以监视一个或多个键,如果在执行 MULTI 之后,执行 EXEC 之前,被监视的键被其他命令改变了,那么当执行 EXEC 时,事务将被取消,并且返回一个错误。

45.2 Redis 事务为什么不支持回滚?

引入事务回滚机制会大大增加 Redis 的复杂性,因为需要跟踪事务中每个命令的状态,并在发生错误时逆向执行命令以恢复原始状态。

45.3 Redis 事务的 ACID 特性如何体现?

ACID 一般指 MySQL 事务中的四个特性:原子性、一致性、隔离性、持久性。
Redis 事务中所有命令会按顺序一次性,但是运行时错误不保证原子性,也不支持部分失败后的自动回滚,因此 Redis 并不能保证一致性,我们必须通过程序逻辑来进行优化,比如 WATCH。Redis 事务在一定程度上提供了隔离性,事务中的命令不会被其他客户端的命令插入。Redis 的持久性依赖于其持久化机制(如 RDB 和 AOF),而不是事务本身。

45.4 Redis 事务满足原子性吗?要怎么改进?

不满足,Redis 事务不支持回滚,一旦 EXEC 命令被调用,所有命令都会被执行,即使有些命令可能执行失败。
可以通过 Lua 脚本来实现事务的原子性,Lua 脚本在 Redis 中是原子执行的,执行过程中间不会插入其他命令。

46、有 Lua 脚本操作 Redis 的经验吗?

Redis 的事务不具备强制性的原子性,但可以通过 Lua 脚本来增强 Redis 的原子能力。比如秒杀系统中我们可以用 Lua 脚本来实现扣减 Redis 库存的功能。

47、Redis 的管道 Pipeline 了解吗?

Pipeline 是 Redis 提供的一种优化手段,允许客户端一次性向服务器发送多个命令,而不必等待每个命令的响应,从而减少网络延迟。它的工作原理类似于批量操作,即多个命令一次性打包发送,Redis 服务器依次执行后再将结果一次性返回给客户端。通常在 Redis 中,每个请求都会遵循以下流程:

  1. 客户端发送命令到服务器。
  2. 服务器执行命令并将结果返回给客户端。
  3. 客户端接收返回结果。
    每一个请求和响应之间存在一次网络通信的往返时间(RTT,Round-Trip Time),如果大量请求依次发送,网络延迟会显著增加请求的总执行时间。因此批量写入大量数据或执行一系列查询时,可以将这些操作打包通过 Pipeline 执行。Redis 服务器接收到批量命令后,依次执行每个命令。Redis 服务器执行完所有命令后,将每条命令的结果一次性打包通过 TCP 返回给客户端。客户端一次性接收所有返回结果,并解析每个命令的执行结果。
    在这里插入图片描述

48、Redis 实现分布式锁了解吗?

分布式锁是一种用于控制多个不同进程在分布式系统中访问共享资源的锁机制。它确保在同一时刻,只有一个节点可以对资源进行访问,从而避免并发问题。可以使用 Redis 的 SET 命令实现分布式锁。同时添加过期时间防止死锁。

SET key value NX PX 30000
  1. key:锁名。
  2. value:锁的持有者标识,可以使用 UUID。
  3. NX 只在 key 不存在时才创建(避免覆盖锁)。
  4. PX 30000:设置锁的过期时间为 30 秒(防止死锁)。
String lockKey = "lock:order:123";
String uniqueId = UUID.randomUUID().toString();
boolean isLocked = redisTemplate.opsForValue()
    .setIfAbsent(lockKey, uniqueId, 10, TimeUnit.SECONDS);
if (isLocked) {
    try {
        // 执行业务逻辑
    } finally {
        // 释放锁
    }
}

48.1 什么是 setnx?

setnx 从 Redis 版本 2.6.12 开始被弃用,因为可以通过 set 命令的 NX 选项来实现相同的功能。使用 setnx 创建分布式锁时,虽然设置过期时间可以避免死锁问题,但可能存在这样的问题:线程 A 获取锁后开始任务,如果任务执行时间超过锁的过期时间,锁会提前释放,导致线程 B 也获取了锁并开始执行任务。这会破坏锁的独占性,并发访问资源,进而造成数据不一致。可以引入锁的自动续约机制,在任务执行过程中定期续期,确保锁在任务完成前不会过期。比如说 Redisson 的 RedissonLock 就支持自动续期,通过看门狗机制定期续期锁的有效期。

48.2 Redisson 了解吗?

开发中,我们可以使用专业的轮子 Redisson。Redisson 是基于 Redis 的 Java 驻内存数据网格,提供了一系列的 API 用来操作 Redis,其中最常用的功能就是分布式锁。实现源码在 RedissonLock 类中,通过 Lua 脚本封装 Redis 命令来实现。

48.3 项目中的分布式锁是怎么做的?

主要通过 Redisson 框架实现的 RedLock 来完成的。

48.4 你提到了Redlock,那它机制是怎么样的?

Redlock 是 Redis 作者提出的一种分布式锁实现方案,用于确保在分布式环境下安全可靠地获取锁。它的目标是在分布式系统中提供一种高可用、高容错的锁机制,确保在同一时刻,只有一个客户端能够成功获得锁,从而实现对共享资源的互斥访问。Redisson 中的 RedLock 是基于 RedissonMultiLock(联锁)实现的。RedissonMultiLock 的 tryLock 方法会在指定的 Redis 实例上逐一尝试获取锁。在获取锁的过程中,Redlock 会根据配置的 waitTime(最大等待时间)和 leaseTime(锁的持有时间)进行灵活控制。比如,如果获取锁的时间小于锁的有效期(TTL),则表示获取锁成功。通常,至少需要多数(如 5 个实例中的 3 个)实例成功获取锁,才能认为整个锁获取成功。如果指定了锁的持有时间(leaseTime),在成功获取锁后,Redlock 会为锁进行续期,以防止锁在操作完成之前意外失效。

48.5 红锁能不能保证百分百上锁?

不能,因为在分布式系统中,网络延迟、时钟漂移、Redis 实例宕机等因素都可能导致锁的获取失败。

48.6 加分布式锁时 Redis 如何保证不会发生冲突?

  1. 使用 SET NX PX 或 SETNX 命令确保锁的获取是一个原子操作,同时设置锁的过期时间防止死锁。
  2. 使用 Lua 脚本将锁的检查和释放操作封装为一个原子操作,确保安全地释放锁。
  3. 使用 Redlock 算法确保锁的正确获取和释放。

48.7 Redisson 中的看门狗机制了解吗?

Redisson 提供的分布式锁是支持锁自动续期的,也就是说,如果线程在锁到期之前还没有执行完,那么 Redisson 会自动给锁续期。这就是看门狗机制。
在这里插入图片描述
看门狗启动后,每隔 10 秒会刷新锁的过期时间,将其延长到 30 秒,确保在锁持有期间不会因为过期而释放。当任务执行完成时,客户端调用 unlock() 方法释放锁,看门狗也随之停止。

48.8 检查锁的过程是原子操作吗?

在 Redis 的看门狗机制中,检查锁的过程并不是单独的一个步骤,而是与锁的续期操作绑定在一起,是通过 Lua 脚本完成的。因此,检查与续期是一个整体的原子操作,以确保只有持有锁的客户端才能成功续期。

49、说说 Redis 底层数据结构?

底层数据结构有:动态字符串、字典、链表、跳跃表、整数集合、压缩列表等。
比如说 string 是通过动态字符串实现的,hash 是通过字典实现的,list 是通过链表实现的,set 是通过字典实现的,zset 是通过跳跃表实现的。

49.1 简单介绍下 SDS?

Redis 是通过 C 语言实现的,但并没有直接使用 C 语言的字符串,而是自己实现了一种叫做动态字符串 SDS 的类型。

struct sdshdr {
    int len; // buf 中已使用的长度
    int free; // buf 中未使用的长度
    char buf[]; // 数据空间
};

SDS 保存了长度信息,这样就将获取字符串长度的时间复杂度由 O(n) 降低到了 O(1)。

49.2 简单介绍下链表 linkedlist?

Redis 的链表是⼀个双向无环链表结构,和 Java 中的 LinkedList 类似。链表的节点由一个叫做 listNode 的结构表示,每个节点都有指向其前置节点和后置节点的指针,同时头节点的前置和尾节点的后置均指向 null。

49.3 简单介绍下字典 dict?

Redis 使用 hash 表作为底层实现,一个哈希表可以有多个哈希表节点,每个哈希表节点保存了字典中的一个键值对。

49.4 简单介绍下跳表 skiplist?

跳表是有序集合 zset 的底层实现,思想是空间换时间,可以理解为可以实现二分查找的有序链表。由 zskiplist 和 zskiplistNode 组成,zskiplist 保存跳表的基本信息。zskiplistNode 保存跳表节点,每个跳表节点的层高是不固定的,每个节点都有一个指向保存了当前节点的分值和成员对象的指针。

49.5 为什么使用跳表?

因为 zset 要支持随机的插入和删除,所以不能使用数组来实现,关于排序问题,我们也很容易就想到红黑树、平衡树这样的树形结构,但是在高并发的情况下,树形结构需要执行一些类似于 rebalance 这样的可能涉及到整棵树的操作,相对来说跳跃表的变化只涉及局部。在复杂度与红黑树相同的情况下,跳跃表实现起来更简单,也更加直观,基于以上的考虑,Redis 采用了跳跃表这样的结构。

49.6 跳跃表是怎么实现的?

  1. 层:跳跃表节点的 level 数组可以包含多个元素,每个元素都包含一个指向其它节点的指针。程序可以通过这些层来加快访问其它节点的速度,层的数量越多,访问其它节点的速度就越快。每次创建一个新的跳跃表节点的时候,程序都根据幂次定律,随机生成一个介于 1 和 32 之间的值作为 level 数组的大小,这个大小就是层的高度。
  2. 前进指针:每个层都有一个指向表尾的前进指针(level[i].forward 属性),用于从表头向表尾方向访问节点。
  3. 跨度:层的跨度用于记录两个节点之间的距离。跨度是用来计算排位的:在查找某个节点的过程中,将沿途访问过的所有层的跨度累计起来,得到的结果就是目标节点在跳跃表中的排位。
  4. 分值和成员:节点的分值是一个 double 类型的浮点数,跳跃表中所有的节点都按分值从小到大来排序。节点的成员对象是一个指针,它指向一个字符串对象,而字符串对象则保存这一个 SDS 值。

49.7 为什么 hash 表范围查询效率比跳表低?

哈希表内的元素是无序的,必须遍历整个表来检查哪些元素满足条件,使其在范围查询的时间复杂度为 O(n)。而跳表可以从最高层开始,快速定位到范围的起始点,然后沿着下一层继续直到找到范围的结束点。这种分层的结构使得跳表在进行范围查询时非常高效,时间复杂度为 O(logn) 加上范围内元素的数量。

50、假如 Redis 里面有 1 亿个 key,其中有 10w 个 key 是以某个固定的已知的前缀开头的,如何将它们全部找出来?

使用 keys 指令可以扫出指定模式的 key 列表。但要注意 keys 指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。

我们可以使用 scan 指令,scan 指令可以无阻塞的提取出指定模式的 key 列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用 keys 指令长。

51、Redis 的秒杀场景下扮演了什么角色?

秒杀主要是指大量用户集中在短时间内对服务器进行访问,从而导致服务器负载剧增,可能出现系统响应缓慢甚至崩溃的情况。针对秒杀的场景来说,最终抢到商品的用户是固定的,也就是说 100 个人和 10000 个人来抢一个商品,最终都只能有 100 个人抢到。但是对于秒杀活动的初心来说,肯定是希望参与的用户越多越好,但真正开始下单时,最好能把请求控制在服务器能够承受的范围之内。解决这一问题的关键就在于错峰削峰和限流。当然了,前端页面的静态化、按钮防抖也能够有效的减轻服务器的压力。

51.1 如何实现错峰削峰呢?

  1. 预热缓存:提前将热点数据加载到 Redis 缓存中,减少对数据库的访问压力。
  2. 消息队列:引入消息队列异步处理请求,减少瞬时请求压力。消息队列就像一个水库,可以削减上游的洪峰流量。
  3. 多阶段多时间窗口:将秒杀活动分多阶段,每个阶段设置不同的时间窗口,让用户在不同的时间段内参与秒杀活动。
  4. 插入答题系统:在秒杀活动中加入答题环节,只有答对题目的用户才能参与秒杀活动,这样可以减少无效请求。

51.2 如何限流呢?

采用令牌桶算法:维护一个容器,按照固定的速率往容器中放令牌(token),请求到来时,从容器中取出一个令牌,如果容器中没有令牌,则拒绝请求。
在这里插入图片描述

  1. 使用 Redis 初始化令牌桶:
redis-cli SET "token_bucket" "100"
  1. 使用 Lua 脚本实现令牌桶算法;假设每秒向桶中添加 10 个令牌,但不超过桶的最大容量。
-- Lua 脚本来添加令牌,并确保不超过最大容量
local bucket = KEYS[1]
local add_count = tonumber(ARGV[1])
local max_tokens = tonumber(ARGV[2])
local current = tonumber(redis.call('GET', bucket) or 0)
local new_count = math.min(current + add_count, max_tokens)
redis.call('SET', bucket, tostring(new_count))
return new_count
  1. 使用 Shell 脚本调用 Lua 脚本:
#!/bin/bash
while true; do
    redis-cli EVAL "$(cat add_tokens.lua)" 1 token_bucket 10 100
    sleep 1
done
  1. 当请求到达时,需要检查并消耗一个令牌。
-- Lua 脚本来消耗一个令牌
local bucket = KEYS[1]
local tokens = tonumber(redis.call('GET', bucket) or 0)
if tokens > 0 then
    redis.call('DECR', bucket)
    return 1  -- 成功消耗令牌
else
    return 0  -- 令牌不足
end

调用 Lua 脚本:

redis-cli EVAL "$(cat consume_token.lua)" 1 token_bucket

52、客户端宕机后 Redis 服务端如何感知到?

每个客户端在 Redis 中维护一个特定的心跳键,用于表示客户端的健康状态,并设置的超时时间,例如 10 秒。客户端定期(如每 5 秒)更新这个心跳键的超时时间,保持它的存活状态,通常通过 SET 命令重设键的过期时间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值