Redis 面试题

基础

1.什么是 Redis?

Redis 一款基于内存的 k/v (键值对)的非关系型数据库(nosql)。

2.Redis 能干什么?

  1. 缓存
  2. 计数器
  3. 排行榜
  4. 消息队列
  5. 限流器
  6. 分布式锁
  7. 社交网络(点赞、关注、共同关注列表、推送、下拉刷新)

3.Redis 有哪些数据结构?

  1. 字符串(String):缓存功能、计数、共享 Session、限速
  2. 哈希(Hash):缓存用户信息、对象
  3. 列表(List):消息队列、文章列表
  4. 集合(Set):标签、共同关注
  5. 有序集合(ZSet):排行榜
  6. HyperLogLog(基数统计)
  7. BitMap:签到
  8. GEO:地理位置信息

4.Redis 为什么快?

  1. 基于内存,内存的读取速度是磁盘的上千倍,这是 Redis 读写速度快的根本原因
  2. 单线程模型:Redis 的命令执行是单线程的,因此在任何时候只有一个命令在执行,避免线程的切换和锁竞争带来的消耗
  3. IO 多路复用:一个线程可以同时监听多个 IO 操作,当有 IO 事件发生时,会通知线程并处理这个事件,从而实现同时处理多个 IO 操作的能力
  4. 高效的数据结构:如 String、List、Set、ZSet 等结构经过高度优化,能够支持快速的数据操作

5.什么是 IO 多路复用?

  • 第一种选择:按顺序逐个检查,先检查 A,然后是 B,之后是 C、D。。。这中间如果有一个学生卡住,全班都会被耽误。这种模式就好比,你用循环挨个处理 socket,根本不具有并发能力。
  • 第二种选择:你创建 30 个分身,每个分身检查一个学生的答案是否正确。 这种类似于为每一个用户创建一个进程或者- 线程处理连接。
  • 第三种选择,你站在讲台上等,谁解答完谁举手。这时 C、D 举手,表示他们解答问题完毕,你下去依次检查 C、D 的答案,然后继续回到讲台上等。此时 E、A 又举手,然后去处理 E 和 A。
  • 在传统的阻塞 IO 模型中,每个 IO 操作都会阻塞当前线程,直到 IO 操作完成
  • 但是在 IO 多路复用模型中,通过一个线程同时监听多个 IO 操作,当有 IO 事件发生时会通知这个线程并去处理这个事件,从而实现同时处理多个 IO 操作的能力
  • Linux 实现 IO 多路复用的三种方式:select、poll 和 epoll。例如 epoll 方式是将用户 socket 对应的 fd 注册进 epoll,通过 epoll 监听哪些 socket 上有消息到达。此时的 socket 应该采用非阻塞模型

6.Redis 6.0 的多线程是怎么回事?

Redis 6.0 的多线程用于处理数据的读写和协议的解析,而 Redis 命令的执行依旧是单线程。Redis 的性能瓶颈在于网络 IO 和内存大小而不是 CPU。

持久化

7.Redis 有哪些持久化方式?

RDB

RDB 持久化通过创建数据的数据集(快照)来工作,在指定的时间间隔内将 Redis 某一时刻的数据状态保存到磁盘的一个 RDB 文件中。

  • 可以通过 save(同步执行) 命令和 bgsave(异步执行) 命令手动触发 RDB 持久化操作。

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

    1. 在 redis.conf 文件中通过save <seconds> <changes>指令配置自动触发 RDB 持久化的条件

      1
      2
      3
      4
      TEXT
      save 900 1
      save 300 10
      save 60 10000

      如果至少有 1 个键被修改,900 秒后自动触发一次 RDB 持久化。

      如果至少有 10 个键被修改,300 秒后自动触发一次 RDB 持久化。

      如果至少有 10000 个键被修改,60 秒后自动触发一次 RDB 持久化。

    2. Redis 服务器没有禁用 RDB 持久化并通过 SHUTDOWN 命令正常关闭服务器,Redis 会自动执行一次 RDB 持久化

    3. 在 Redis 复制场景中,当一个 Redis 实例被配置为从节点并且与主节点建立连接时,它可能会根据配置接收主节点的 RDB 文件来初始化数据集。这个过程中,主节点会在后台自动触发 RDB 持久化,然后将生成的 RDB 文件发送给从节点。

AOF

AOF 持久化通过记录 Redis 的写命令并将其追加到 AOF 文件中来工作,在要进行数据恢复时只需重新执行一遍这些命令就行。(主流的 Redis 持久化方式)

AOF 执行流程:

流程如下:

1)当 AOF 持久化功能被启用时,Redis 服务器会将接收到的所有写命令(比如 SET, LPUSH, SADD 等修改数据的命令)追加到 AOF 缓冲区(buffer)的末尾。

2)为了将缓冲区中的命令持久化到磁盘中的 AOF 文件,Redis 提供了几种不同的同步策略:

  • always:每次写命令都会同步到 AOF 文件,这提供了最高的数据安全性,但可能因为磁盘 I/O 的延迟而影响性能。
  • everysec(默认):每秒同步一次,这是一种折衷方案,提供了较好的性能和数据安全性。
  • no:不主动进行同步,交由操作系统决定何时将缓冲区数据写入磁盘,这种方式性能最好,但在系统崩溃时可能会丢失最近一秒的数据。

3)随着操作的不断执行,AOF 文件会不断增长,为了减小 AOF 文件大小,Redis 可以重写 AOF 文件:

  • 重写过程不会解析原始的 AOF 文件,而是将当前内存中的数据库状态转换为一系列写命令,然后保存到一个新的 AOF 文件中。
  • AOF 重写操作由 BGREWRITEAOF 命令触发,它会创建一个子进程来执行重写操作,因此不会阻塞主进程。
  • 重写过程中,新的写命令会继续追加到旧的 AOF 文件中,同时也会被记录到一个缓冲区中。一旦重写完成,Redis 会将这个缓冲区中的命令追加到新的 AOF 文件中,然后切换到新的 AOF 文件上,以确保数据的完整性。

4)当 Redis 服务器启动时,如果配置为使用 AOF 持久化方式,它会读取 AOF 文件中的所有命令并重新执行它们,以恢复数据库的状态。

8.RDB 和 AOF 的优缺点?

RDB 优点

  1. 只有一个紧凑的二进制文件(dump.rdb),非常适合备份和全量复制
  2. 容灾性好,可把 RDB 文件拷贝至远程机器或者文件系统中,用于容灾恢复
  3. 恢复速度快,回复速度远快于 AOF

RDB 缺点

  1. 实时性低,由于 RDB 是一个段时间进行数据持久化,无法做到实时持久化
  2. 存在兼容性问题:Redis 演进过程中存在多个格式的 RDB 版本,存在新老不兼容问题

AOF 优点

  1. 实时性好。aof 持久化可以配置 appendfsync 属性,有 always,每进行一次命令操作就记录到 aof 文件中一次。
  2. 数据一致性强。通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题

AOF 缺点

  1. AOF 文件比 RDB 大,恢复速度慢
  2. 数据集大的时候,比 RDB 启动效率低

9.RDB 和 AOF 如何选择?

  • 想达到媲美数据库的数据安全性就两种都选
  • 如果能接受数分钟内的数据丢失,可以只使用 RDB 持久化
  • 如果只需要数据在服务器运行时存在,可以不选择任何持久化方式

10.Redis 的数据恢复?

将 RDB 文件或 AOF 文件拷贝到 Redis 数据目录下,如果使用 AOF 恢复,配置文件开启 AOF,重启 Redis 实例即可。

Redis启动加载数据

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

开启 AOF 并且存在 AOF 文件就直接加载 AOF 文件,否则加载 RDB 文件(没开启 AOF 也是这样)。加载 AOF/RDB 文件成功就启动成功,否则启动失败并打印错误信息。

11.Redis 4.0 的混合持久化了解吗?

混合持久化模式会在 AOF 重写的同时生成一份 RDB 快照,并将这份快照作为 AOF 文件的一部分,最后再附加新的写入命令。这样恢复数据时,Redis 先加载 RDB 文件来恢复到快照时刻的状态,然后应用 RDB 之后记录的 AOF 命令来恢复之后的数据,又快又可靠。

缓存设计

12.什么是缓存穿透、缓存雪崩、缓存击穿?

缓存穿透

用户请求的数据既不在 Redis 中也不再 数据库中,导致大量请求直接抵达数据库,给数据库造成巨大压力。

解决办法:

  1. 缓存空值
  2. 布隆过滤器

缓存雪崩

在短时间内,Redis 的大量 key 同时失效或者 Redis 宕机,导致大量请求到达数据库,给数据库造成巨大压力

解决办法:

  1. 给 key 设置随机过期时间
  2. 搭建集群
  3. 限流和降级

13.说说布隆过滤器?

布隆过滤器是一个空间利用率极高的概率性数据结构,用于快速检查一个元素是否存在于一个集合中。

14.如果保证数据库和缓存的数据一致性?

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

16.怎么处理热 key?

热点 key:短时间被经常访问的键

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

  • QPS 集中在特定的 Key:总的 QPS(每秒查询率)为 10000,其中一个 Key 的 QPS 飙到了 8000。
  • 带宽使用率集中在特定的 Key:一个拥有上千成员且总大小为 1M 的哈希 Key,每秒发送大量的 HGETALL 请求。
  • CPU 使用率集中在特定的 Key:一个拥有数万个成员的 ZSET Key,每秒发送大量的 ZRANGE 请求。

如何对 key 进行监控?

①、客户端

客户端其实是距离 key“最近”的地方,因为 Redis 命令就是从客户端发出的,例如在客户端设置全局字典(key 和调用次数),每次调用 Redis 命令时,使用这个字典进行记录。

②、代理端

像 Twemproxy、Codis 这些基于代理的 Redis 分布式架构,所有客户端的请求都是通过代理端完成的,可以在代理端进行监控。

③、Redis 服务端

使用 monitorbigkeys 命令

redis-cli monitor 和 bigkeys 命令的使用:redis-cli –bigkeys`

如何处理?

  1. 把热 key 打散到不同的服务器,降低压力。基本思路就是给热 Key 加上前缀或者后缀,见下例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // N 为 Redis 实例个数,M 为 N 的 2倍
    const M = N * 2
    //生成随机数
    random = GenRandom(0, M)
    //构造备份新 Key
    bakHotKey = hotKey + "_" + random
    data = redis.GET(bakHotKey)
    if data == NULL {
    data = redis.GET(hotKey)
    if data == NULL {
    data = GetFromDB()
    // 可以利用原子锁来写入数据保证数据一致性
    redis.SET(hotKey, data, expireTime)
    redis.SET(bakHotKey, data, expireTime + GenRandom(0, 5))
    } else {
    redis.SET(bakHotKey, data, expireTime + GenRandom(0, 5))
    }
    }
  2. 加入二级缓存。加⼊⼆级缓存,当出现热 Key 后,把热 Key 加载到 JVM 中,后续针对这些热 Key 的请求,直接从 JVM 中读取。

17.缓存预热怎么做呢?

  1. 直接写个缓存刷新页面或者接口,上线时手动操作
  2. 数据量不大时,可以在项目启动时自动加载
  3. 定时任务 + 分布式锁

18.热点 key 重建?问题?解决?

问题?

  • 当前 key 是一个爆款商品,并发量很大
  • 重建缓存是一个很耗时的操作,可能会有大量 SQL、多个 I/O、多个依赖等等。在缓存失效的瞬间会有许多线程来重建缓存,对后端压力过大

解决?

  1. 互斥锁。只允许一个线程进行缓存的重建任务
  2. key 永远不过期
    • 从缓存层面来看,确实没有设置过期时间,所以不会出现热点 key 过期后产生的问题,也就是“物理”不过期。
    • 从功能层面来看,为每个 value 设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去构建缓存。

底层结构

19.讲讲Redis 的底层数据结构?

Redis 的底层数据结构有动态字符串(sds)链表(list)字典(ht)跳跃表(skiplist)整数集合(intset)压缩列表(ziplist) 等。

20.Redis 的 SDS 和 C 中字符串相比有什么优势?

C语言 字符串的缺点

  1. 获取字符串长度复杂度高:因为 C 不保存数组的长度,每次都需要遍历一遍整个数组,时间复杂度为 O(n);
  2. 存在内存泄漏:C 字符串不记录自身长度带来的另外一个问题是容易造成缓存区溢出(buffer overflow),例如在字符串拼接的时候,新的
  3. 只能保存文本数据:

Redis 如何解决的?

  1. 多增加 len 表示当前字符串的长度:这样就可以直接获取长度了,复杂度 O(1);
  2. 自动扩展空间:当 SDS 需要对字符串进行修改时,首先借助于 lenalloc 检查空间是否满足修改所需的要求,如果空间不够的话,SDS 会自动扩展空间,避免了像 C 字符串操作中的溢出情况;
  3. 有效降低内存分配次数:C 字符串在涉及增加或者清除操作时会改变底层数组的大小造成重新分配,SDS 使用了 空间预分配惰性空间释放 机制,简单理解就是每次在扩展时是成倍的多分配的,在缩容是也是先留着并不正式归还给 OS;
  4. 二进制安全:C 语言字符串只能保存 ascii 码,对于图片、音频等信息无法保存,SDS 是二进制安全的,写入什么读取就是什么,不做任何过滤和限制;

21.

高可用

25.什么是主从复制吗?

主从复制,是指将一台 Redis 服务器的数据,复制到其他的 Redis 服务器。前者称为 **主节点(master)**,后者称为 **从节点(slave)**。且数据的复制是 单向 的,只能由主节点到从节点。Redis 主从复制支持 主从同步从从同步 两种,后者是 Redis 后续版本新增的功能,以减轻主节点的同步负担。

主从复制的主要作用?

  • 数据冗余: 主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
  • 故障恢复: 当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复 *(实际上是一种服务的冗余)*。
  • 负载均衡: 在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务 (即写 Redis 数据时应用连接主节点,读 Redis 数据时应用连接从节点),分担服务器负载。尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高 Redis 服务器的并发量。
  • 高可用基石: 除了上述作用以外,主从复制还是哨兵和集群能够实施的 基础,因此说主从复制是 Redis 高可用的基础。

26.Redis 的主从复制原理?

Redis主从复制工作流程

  1. 保存主节点(master)信息
    这一步只是保存主节点信息,保存主节点的 ip 和 port。
  2. 主从建立连接
    从节点(slave)发现新的主节点后,会尝试和主节点建立网络连接。
  3. 发送 ping 命令
    连接建立成功后从节点发送 ping 请求进行首次通信,主要是检测主从之间网络套接字是否可用、主节点当前是否可接受处理命令。
  4. 权限验证
    如果主节点要求密码验证,从节点必须正确的密码才能通过验证。
  5. 同步数据集
    主从复制连接正常通信后,主节点会把持有的数据全部发送给从节点。
  6. 命令持续复制
    接下来主节点会持续地把写命令发送给从节点,保证主从数据一致性。

27.主从数据同步的方式?

全量复制

它会把主节点的数据全部发送给从节点,当数据量较大时会对主节点和网络造成很大开销。一般用于初次复制场景。

全量复制

  1. 发送 psync 命令进行数据同步,由于是第一次进行复制,从节点没有复制偏移量和主节点的运行 ID,所以发送 psync-1。
  2. 主节点根据 psync-1 解析出当前为全量复制,回复+FULLRESYNC 响应。
  3. 从节点接收主节点的响应数据保存运行 ID 和偏移量 offset
  4. 主节点执行 bgsave 保存 RDB 文件到本地
  5. 主节点发送 RDB 文件给从节点,从节点把接收的 RDB 文件保存在本地并直接作为从节点的数据文件
  6. 对于从节点开始接收 RDB 快照到接收完成期间,主节点仍然响应读写命令,因此主节点会把这期间写命令数据保存在复制客户端缓冲区内,当从节点加载完 RDB 文件后,主节点再把缓冲区内的数据发送给从节点,保证主从之间数据一致性。
  7. 从节点接收完主节点传送来的全部数据后会清空自身旧数据
  8. 从节点清空数据后开始加载 RDB 文件
  9. 从节点成功加载完 RDB 后,如果当前节点开启了 AOF 持久化功能, 它会立刻做 bgrewriteaof 操作,为了保证全量复制后 AOF 持久化文件立刻可用。

部分复制

使用 psync{runId}{offset}命令实现。当从节点(slave)正在复制主节点 (master)时,如果出现网络闪断或者命令丢失等异常情况时,从节点会向主节点要求补发丢失的命令数据,如果主节点的复制积压缓冲区内存在这部分数据则直接发送给从节点,这样就可以保持主从节点复制的一致性。

部分复制

28.主从复制存在哪些问题呢?

  • 一旦主节点出现故障,需要手动将一个从节点晋升为主节点,同时需要修改应用方的主节点地址,还需要命令其他从节点去复制新的主节点,整个过程都需要人工干预。
  • 主节点的写能力受到单机的限制。
  • 主节点的存储能力受到单机的限制。

29.Redis 哨兵机制了解吗?

Redis 哨兵可以帮助我们自动完成节点的故障转移。

Redis 由两部分组成:

  • 哨兵节点: 哨兵系统由一个或多个哨兵节点组成,哨兵节点是特殊的 Redis 节点,不存储数据,对数据节点进行监控。
  • 数据节点: 主节点和从节点都是数据节点;

在复制的基础上,哨兵实现了 自动化的故障恢复 功能,下面是官方对于哨兵功能的描述:

  • 监控(Monitoring): 哨兵会不断地检查主节点和从节点是否运作正常。
  • 自动故障转移(Automatic failover):主节点 不能正常工作时,哨兵会开始 自动故障转移操作,它会将失效主节点的其中一个 从节点升级为新的主节点,并让其他从节点改为复制新的主节点。
  • 配置提供者(Configuration provider): 客户端在初始化时,通过连接哨兵来获得当前 Redis 服务的主节点地址。
  • 通知(Notification): 哨兵可以将故障转移的结果发送给客户端。

30.Redis 哨兵实现原理?

哨兵模式是通过哨兵节点完成对数据节点的监控、下线、故障转移。

Redis Sentinel工作流程

  • 定时监控

    Redis Sentinel 通过三个定时监控任务完成对各个节点发现和监控:

    1. 每隔 10 秒,每个 Sentinel 节点会向主节点和从节点发送 info 命令获取最新的拓扑结构
    2. 每隔 2 秒,每个 Sentinel 节点会向 Redis 数据节点的sentinel:hello 频道上发送该 Sentinel 节点对于主节点的判断以及当前 Sentinel 节点的信息
    3. 每隔 1 秒,每个 Sentinel 节点会向主节点、从节点、其余 Sentinel 节点发送一条 ping 命令做一次心跳检测,来确认这些节点当前是否可达
  • 主观下线和客观下线
    主观下线就是哨兵节点认为某个节点有问题,客观下线就是超过一定数量的哨兵节点认为主节点有问题。
    主观下线和客观下线

  1. 主观下线
    每个 Sentinel 节点会每隔 1 秒对主节点、从节点、其他 Sentinel 节点发送 ping 命令做心跳检测,当这些节点超过 down-after-milliseconds 没有进行有效回复,Sentinel 节点就会对该节点做失败判定,这个行为叫做主观下线。
  2. 客观下线
    当 Sentinel 主观下线的节点是主节点时,该 Sentinel 节点会通过 sentinel is- master-down-by-addr 命令向其他 Sentinel 节点询问对主节点的判断,当超过 个数,Sentinel 节点认为主节点确实有问题,这时该 Sentinel 节点会做出客观下线的决定
  • 领导者 Sentinel 节点选举
    Sentinel 节点之间会做一个领导者选举的工作,选出一个 Sentinel 节点作为领导者进行故障转移的工作。Redis 使用了 Raft 算法实现领导者选举。

  • 故障转移

    领导者选举出的 Sentinel 节点负责故障转移,过程如下:
    故障转移

    1. 在从节点列表中选出一个节点作为新的主节点,这一步是相对复杂一些的一步
    2. Sentinel 领导者节点会对第一步选出来的从节点执行 slaveof no one 命令让其成为主节点
    3. Sentinel 领导者节点会向剩余的从节点发送命令,让它们成为新主节点的从节点
    4. Sentinel 节点集合会将原来的主节点更新为从节点,并保持着对其关注,当其恢复后命令它去复制新的主节点

31.领导者 Sentinel 节点选举了解吗?

Redis 使用了 Raft 算法实 现领导者选举,大致流程如下:
领导者Sentinel节点选举

  1. 每个在线的 Sentinel 节点都有资格成为领导者,当它确认主节点主观 下线时候,会向其他 Sentinel 节点发送 sentinel is-master-down-by-addr 命令, 要求将自己设置为领导者。
  2. 收到命令的 Sentinel 节点,如果没有同意过其他 Sentinel 节点的 sentinel is-master-down-by-addr 命令,将同意该请求,否则拒绝。
  3. 如果该 Sentinel 节点发现自己的票数已经大于等于 max(quorum, num(sentinels)/2+1),那么它将成为领导者。
  4. 如果此过程没有选举出领导者,将进入下一次选举。

32.新的主节点是怎样被挑选出来的?

  1. 过滤:“不健康”(主观下线、断线)、5 秒内没有回复过 Sentinel 节 点 ping 响应、与主节点失联超过 down-after-milliseconds*10 秒。
  2. 选择 slave-priority(从节点优先级)最高的从节点列表,如果存在则返回,不存在则继续。
  3. 选择复制偏移量最大的从节点(复制的最完整),如果存在则返 回,不存在则继续。
  4. 选择 runid 最小的从节点。

33.Redis 集群了解吗?

前面说到了主从存在高可用和分布式的问题,哨兵解决了高可用的问题,而集群就是终极方案,一举解决高可用和分布式问题。

Redis 集群示意图Redis 集群示意图

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

34.切片集群了解吗?

切片集群是一种将数据分片存储在多个 Redis 实例上的集群架构,每个 Redis 实例负责存储部分数据。比如说把 25G 的数据平均分为 5 份,每份 5G,然后启动 5 个 Redis 实例,每个实例保存一份数据。

数据和实例之间如何映射呢?

在 Redis 3.0 之前,官方并没有针对切片集群提供具体的解决方案;但是在 Redis 3.0 之后,官方提供了 Redis Cluster,它是 Redis 官方推荐的分布式解决方案。

在 Redis Cluster 中,数据和实例之间的映射是通过哈希槽(hash slot)来实现的。Redis Cluster 有 16384 个哈希槽,每个键根据其名字的 CRC16 值被映射到这些哈希槽上。然后,这些哈希槽会被均匀地分配到所有的 Redis 实例上。

CRC16 是一种哈希算法,它可以将任意长度的输入数据映射为一个 16 位的哈希值。

例如,如果我们有 3 个 Redis 实例,那么每个实例可能会负责大约 5461 个哈希槽。

当需要存储或检索一个键值对时,Redis Cluster 会先计算这个键的哈希槽,然后找到负责这个哈希槽的 Redis 实例,最后在这个实例上进行操作。

35.集群中数据如何分区?

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

三分恶面渣逆袭:分布式数据分区三分恶面渣逆袭:分布式数据分区

#说说节点取余分区

节点取余分区是一种简单的分区策略,其中数据项通过对某个值(通常是键的哈希值)进行取余操作来分配到不同的节点。

类似 HashMap 中的取余操作,数据项的键经过哈希函数计算后,对节点数量取余,然后将数据项分配到余数对应的节点上。

缺点是扩缩容时,大多数数据需要重新分配,因为节点总数的改变会影响取余结果,这可能导致大量数据迁移。

三分恶面渣逆袭:节点取余分区三分恶面渣逆袭:节点取余分区

#说说一致性哈希分区

一致性哈希分区的原理是:将哈希值空间组织成一个环,数据项和节点都映射到这个环上。数据项由其哈希值直接映射到环上,然后顺时针分配到遇到的第一个节点。

从而来减少节点变动时数据迁移的量。

三分恶面渣逆袭:一致性哈希分区三分恶面渣逆袭:一致性哈希分区

Key 1 和 Key 2 会落入到 Node 1 中,Key 3、Key 4 会落入到 Node 2 中,Key 5 落入到 Node 3 中,Key 6 落入到 Node 4 中。

这种方式相比节点取余最大的好处在于加入和删除节点只影响哈希环中相邻的节点,对其他节点无影响。

但它还是存在问题:

  • 节点在圆环上分布不平均,会造成部分缓存节点的压力较大
  • 当某个节点故障时,这个节点所要承担的所有访问都会被顺移到另一个节点上,会对后面这个节点造成压力。

#说说虚拟槽分区?

在虚拟槽(也叫哈希槽)分区中,存在固定数量的槽位(例如 Redis Cluster 有 16384 个槽),每个键通过哈希算法(CRC16)映射到这些槽上,每个集群节点负责管理一定范围内的槽。

这种分区可以灵活地将槽(以及槽中的数据)从一个节点迁移到另一个节点,从而实现平滑扩容和缩容;数据分布也更加均匀,Redis Cluster 采用的正是这种分区方式。

三分恶面渣逆袭:虚拟槽分配三分恶面渣逆袭:虚拟槽分配

在虚拟槽分区中,槽是数据管理和迁移的基本单位。假设系统中有 4 个实际节点,假设为其分配了 16 个槽(0-15);

  • 槽 0-3 位于节点 node1;
  • 槽 4-7 位于节点 node2;
  • 槽 8-11 位于节点 node3;
  • 槽 12-15 位于节点 node4。

如果此时删除 node2,只需要将槽 4-7 重新分配即可,例如将槽 4-5 分配给 node1,槽 6 分配给 node3,槽 7 分配给 node4,数据在节点上的分布仍然较为均衡。

如果此时增加 node5,也只需要将一部分槽分配给 node5 即可,比如说将槽 3、槽 7、槽 11、槽 15 迁移给 node5,节点上的其他槽位保留。

36.能说说 Redis 集群的原理吗?

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

#集群创建

数据分区是在集群创建的时候完成的。

集群创建集群创建

设置节点
Redis 集群一般由多个节点组成,节点数量至少为 6 个才能保证组成完整高可用的集群。每个节点需要开启配置 cluster-enabled yes,让 Redis 运行在集群模式下。
节点和握手
节点握手
节点握手是指一批运行在集群模式下的节点通过 Gossip 协议彼此通信, 达到感知对方的过程。节点握手是集群彼此通信的第一步,由客户端发起命 令:cluster meet{ip}{port}。完成节点握手之后,一个个的 Redis 节点就组成了一个多节点的集群。

分配槽(slot)
Redis 集群把所有的数据映射到 16384 个槽中。每个节点对应若干个槽,只有当节点分配了槽,才能响应和这些槽关联的键命令。通过 cluster addslots 命令为节点分配槽。

分配槽分配槽

#故障转移

Redis 集群的故障转移和哨兵的故障转移类似,但是 Redis 集群中所有的节点都要承担状态维护的任务。

故障发现
Redis 集群内节点通过 ping/pong 消息实现节点通信,集群中每个节点都会定期向其他节点发送 ping 消息,接收节点回复 pong 消息作为响应。如果在 cluster-node-timeout 时间内通信一直失败,则发送节 点会认为接收节点存在故障,把接收节点标记为主观下线(pfail)状态。
主观下线
当某个节点判断另一个节点主观下线后,相应的节点状态会跟随消息在集群内传播。通过 Gossip 消息传播,集群内节点不断收集到故障节点的下线报告。当 半数以上持有槽的主节点都标记某个节点是主观下线时。触发客观下线流程。
主观下线和客观下线

故障恢复

故障节点变为客观下线后,如果下线节点是持有槽的主节点则需要在它 的从节点中选出一个替换它,从而保证集群的高可用。

故障恢复流程故障恢复流程

  1. 资格检查
    每个从节点都要检查最后与主节点断线时间,判断是否有资格替换故障 的主节点。
  2. 准备选举时间
    当从节点符合故障转移资格后,更新触发故障选举的时间,只有到达该 时间后才能执行后续流程。
  3. 发起选举
    当从节点定时任务检测到达故障选举时间(failover_auth_time)到达后,发起选举流程。
  4. 选举投票
    持有槽的主节点处理故障选举消息。投票过程其实是一个领导者选举的过程,如集群内有 N 个持有槽的主节 点代表有 N 张选票。由于在每个配置纪元内持有槽的主节点只能投票给一个 从节点,因此只能有一个从节点获得 N/2+1 的选票,保证能够找出唯一的从节点。
    选举投票
  5. 替换主节点
    当从节点收集到足够的选票之后,触发替换主节点操作。

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

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

37.说说集群的伸缩?

Redis 集群提供了灵活的节点扩容和收缩方案,可以在不影响集群对外服务的情况下,为集群添加节点进行扩容也可以下线部分节点进行缩容。
集群的伸缩其实,集群扩容和缩容的关键点,就在于槽和节点的对应关系,扩容和缩容就是将一部分数据迁移给新节点。

例如下面一个集群,每个节点对应若干个槽,每个槽对应一定的数据,如果希望加入 1 个节点希望实现集群扩容时,需要通过相关命令把一部分槽和内容迁移给新节点。
扩容实例缩容也是类似,先把槽和数据迁移到其它节点,再把对应的节点下线。

38.Redisson 能实现哪些功能?

Redisson 是 Redis 一个强大的客户端,除了实现了限流和分布式锁外,Redisson 还实现了以下功能:

1. 分布式集合和数据结构

  • **RMap (分布式映射)**:类似于 Java 的 Map,支持各种高级操作,如批量操作、条件更新、事件监听等。
  • **RSet (分布式集合)**:类似于 Java 的 Set,支持集合操作,如交集、并集、差集等。
  • **RList (分布式列表)**:类似于 Java 的 List,支持索引访问、列表遍历等操作。
  • **RQueue (分布式队列)**:支持先进先出(FIFO)操作,适合任务调度和消息传递。
  • **RDeque (分布式双端队列)**:支持双端操作,适合实现栈和队列的混合操作。
  • **RBlockingQueue (分布式阻塞队列)**:支持阻塞的队列操作,适合生产者-消费者模式。
  • **RBlockingDeque (分布式阻塞双端队列)**:支持阻塞的双端队列操作。

2. 分布式计数器和信号量

  • **RAtomicLong (分布式原子长整型)**:类似于 Java 的 AtomicLong,支持原子操作。
  • **RAtomicDouble (分布式原子双精度浮点型)**:类似于 Java 的 AtomicDouble,支持原子操作。
  • **RCountDownLatch (分布式倒计时锁存器)**:类似于 Java 的 CountDownLatch,用于等待其他操作完成。
  • **RSemaphore (分布式信号量)**:类似于 Java 的 Semaphore,用于控制并发访问量。
  • RFairLock (公平锁) 和 **RReadWriteLock (读写锁)**:提供更精细的并发控制。

3. 分布式对象

  • **RBucket (分布式对象桶)**:用于存储任意对象,类似于 Redis 的字符串操作。
  • **RBinaryStream (分布式二进制流)**:用于存储和读取二进制数据。

4. 分布式任务调度

  • **RScheduledExecutorService (分布式调度执行器服务)**:类似于 Java 的 ScheduledExecutorService,支持分布式任务调度和执行。
  • **RExecutorService (分布式执行器服务)**:用于在集群中执行和管理任务。

5. 分布式消息队列和事件

  • **RTopic (分布式主题)**:用于发布/订阅模式的消息传递,类似于 Redis 的 PUBLISHSUBSCRIBE
  • **RPatternTopic (分布式模式主题)**:支持模式匹配的发布/订阅消息传递。
  • **RStream (分布式流)**:用于处理 Redis Streams 的分布式消息传递。

6. 缓存和 MapReduce

  • **RLocalCachedMap (本地缓存映射)**:提供分布式 Map 与本地缓存结合的功能,减少网络延迟。
  • **RMapReduce (分布式 MapReduce)**:支持分布式数据处理和计算。

7. 地理位置和 BitSet

  • **RGeo (地理位置)**:支持地理位置的存储和查询,类似于 Redis 的 Geo 命令。
  • **RBitSet (分布式 BitSet)**:类似于 Java 的 BitSet,用于高效存储和操作位集合。

8. 事务和批处理

  • **RTransaction (分布式事务)**:支持 Redis 事务操作,确保操作的原子性。
  • **RBatched (批处理操作)**:支持批量执行 Redis 命令,提高效率。

9. 锁和同步机制

  • **RLock (分布式可重入锁)**:支持分布式可重入锁操作。
  • **RMultiLock (分布式多锁)**:支持多个锁的组合操作。
  • **RRedLock (分布式 Redlock)**:实现了 Redis 官方推荐的分布式锁算法 Redlock。

示例代码

以下是一些 Redisson 功能的示例代码:

分布式锁

1
2
3
4
5
6
7
8
9
10
11
RedissonClient redissonClient = Redisson.create();
RLock lock = redissonClient.getLock("myLock");

lock.lock();
try {
// 临界区代码
} finally {
lock.unlock();
}

redissonClient.shutdown();

分布式列表

1
2
3
4
5
6
7
8
9
10
11
RedissonClient redissonClient = Redisson.create();
RList<String> list = redissonClient.getList("myList");

list.add("item1");
list.add("item2");

for (String item : list) {
System.out.println(item);
}

redissonClient.shutdown();

分布式信号量

1
2
3
4
5
6
7
8
9
10
11
RedissonClient redissonClient = Redisson.create();
RSemaphore semaphore = redissonClient.getSemaphore("mySemaphore");

semaphore.acquire();
try {
// 受限操作
} finally {
semaphore.release();
}

redissonClient.shutdown();

通过这些高级功能,Redisson 极大地扩展了 Redis 的应用场景,使其不仅仅是一个缓存和简单的键值存储,还能用于复杂的分布式系统开发。