Redis基础知识学习笔记

本文为专栏《Redis核心技术与实战 - 蒋德钧 - 极客时间》的学习笔记。另外,在此还要感谢评论区大神、课表明 Kaito 同窗的分享与帮助。react

✔️ 知识点总览

首先咱们都知道 Redis 是一个很是经典的,高性能的,“单线程”的键值数据库。web

为何高性能呢?除了 Redis 是基于内存的数据库以外,还要归功于它的底层数据结构。高效的数据结构是Redis快速处理数据的基础。算法

除了数据结构之外,为何Redis是“单线程”的,却还可以那么快?那咱们就须要了解 Redis 的线程模型究竟是怎样的。数据库

对于一款数据库来讲,光够快是不够的,还须要够强壮,也就是常说的高可用。后端

对于 Redis 的高可用来讲,基于内存的数据库有一个致命问题:一旦发生宕机,内存中的数据将会所有丢失。若是单纯地从后端数据库恢复数据,是很是耗费性能且耗时的。因此持久化机制对于 Redis 来讲是十分必要的。而咱们知道,读写磁盘是很是耗时的操做,那么 Redis 是如何在保证高性能的前提下实现持久化机制的呢?这就须要来了解一下 AOFRDB 了。数组

高可用不止包括宕机后的数据恢复,还包括服务尽可能少的中断。Redis 采用了主从库读写分离的模式,具体是如何实现的呢?数据如何同步?又是如何保证主从数据一致的呢?同时还要兼顾到在此过程当中尽可能不要让主库中断对外提供服务。这就须要了解 Redis 的主从架构了。缓存

那么这又带来了新的问题:主库挂了怎么办?若是主库挂了咱们确定须要一个新的主库,好比把某一个从库切换为主库。那么须要考虑的问题是:如何判断主库真的挂了?若是切换的话应该选哪一个从库做为新主库?切换完成后如何将新主库的信息通知给从库和客户端呢?markdown

Redis 经过哨兵机制实现了主从库自动切换功能,高效解决了主动复制模式下故障转移的问题。网络

了解哨兵机制后,新的问题又又又来了:该由哪一个哨兵执行主从切换?若是哨兵挂了还能执行主从切换吗?数据结构

达到了够快,够强以后,最后还要看够不够装。若是须要存储的数据量很是庞大怎么办?咱们须要了解什么是切片集群以及它的实现方案。

至此,就对 Redis 相关的基础知识点有了一个全局的大致上的了解,而后针对每一个点再进行深挖。

✔️ 数据类型 & 数据结构

Redis有哪几种数据类型?底层数据结构是怎样的?他们之间是如何对应的?

Redis 中的全部数据都是以键值对的形式保存在全局哈希表中,每一个键值对的值又对应了多种数据类型,借用专栏中的一张图:

有序集合为何选择跳表而不是红黑树?

有序集合选择跳表而没有选择红黑树,是由于虽然插入删除查找时间复杂度相同,可是根据区间查找这个操做红黑树没有跳表效率高。

整数数组和压缩列表在查找操做的时间复杂度上没有很大优点,为何仍是被 Redis 选为底层数据结构?

一是由于Redis是内存数据库,须要尽可能优化内存,提升内存利用率。数组和压缩列表是很是紧凑的数据结构,比链表占用的内存要少。

二是由于数组对CPU高速缓存支持更友好(空间局部性:访问数组时会将访问元素附近的多个元素一块儿带到高速缓存中)。因此当集合数据元素比较少时,默认采用内存紧凑排列的方式存储,同时可以利用CPU高速缓存,不会下降访问速度。当元素数量超过阈值以后,避免查询时间复杂度过高,保证查询效率,转为哈希或者跳表结构。

什么是渐进式rehash?过程是怎样的?

当全局哈希表内数据愈来愈多,某些冲突链会过长,查询效率下降。因此 Redis 会进行 rehash 操做:增长哈希桶数量,减小单个桶中的元素。

为了让 rehash 操做更加高效,Redis 默认使用两个全局哈希表,每次只使用其中一个。当元素数量达到阈值,便进行 rehash 操做:给另外一个哈希表分配更大的内存空间,将正在使用的哈希表数据 copy 过去,释放旧的哈希表空间。

可是这个过程涉及大量数据拷贝,一次性迁移会致使线程阻塞。为了不采起了渐进式 rehash 的操做:分配更大的新的空间以后,Redis 仍然正常处理请求,每处理一个请求,会将旧哈希表中第一个索引位置的全部元素copy 到新哈希表中。下一个请求时再 copy 一份。这样将一次性的大量拷贝分摊到了多个请求处理过程当中,避免了线程阻塞。

✔️ 线程模型

Redis为何用单线程?Redis是单线程的为何可以这么快?Redis的线程模型是什么样的?

Redis为何用单线程?

当咱们编写多线程程序,在刚开始增长线程数时,系统的吞吐率会上升。但增长到必定程度以后,吞吐量的提高会趋于平缓,甚至降低。这是由于多线程模式须要处理共享资源的并发访问控制,另外多个线程的切换也会消耗必定的资源。还有多线程也会提高系统的复杂度,增长开发难度,下降可维护性。最后就是 Redis 服务中运行的绝大多数操做的性能瓶颈都不是CPU,使用多线程的意义不大。

Redis是单线程的为何可以这么快?

首先Redis采用了高效的数据结构,这是它高性能的一个重要缘由。另外一个缘由就是 Redis 的线程模型:单线程下的多路复用机制。实际上是基于 reactor 模型的单 reactor 单线程模式。要注意 Redis 并非彻底单线程的,但主要的网络 IO 和键值对读写是由一个线程完成的,也是对外服务的主要流程,因此常说 Redis 是单线程的。其余功能好比持久化、异步删除、集群数据同步等都是其余额外线程完成的。

Redis的线程模型是什么样的?

Redis 单线程模型主要是文件事件处理器,包括4个部分:

  1. 多个 socket
  2. IO 多路复用程序
  3. 文件事件分派器
  4. 事件处理器(链接应答处理器、命令请求处理器、命令回复处理器)

客户端与服务端的一次通讯过程是这样的:

Redis 初始化时,会将 server socket 的 AE_READABLE 事件与链接应答处理器关联。

客户端 socket 向 Redis 请求链接,server socket 会产生一个 AE_READABLE 事件,IO 多路复用程序监听到以后,将 socket 压入队列,文件事件分派器从队列中获取 socket ,交给链接应答处理器。链接应答处理器建立一个能与客户端通讯的 socket01 ,并将 AE_READABLE 事件与命令请求处理器关联。

假设客户端发送一个 set 请求,Redis 中的 socket01 会产生 AE _READABLE 事件,IO 多路复用程序将 socket01 压入队列,事件分派器获取到 socket01 以后,由于事件已经关联了命令请求处理器,因此会交给命令请求处理器来处理。命令请求处理器完成写入操做,将 socket01 的AE_WRITABLE 事件与命令回复处理器关联。 客户端若是准备好接受返回结果了,Redis 的 socket01 会产生一个 AE_WRITABLE 事件,压入队列中,事件分派器找到关联命令的命令回复处理器,由命令回复处理器对 socket01 输入本次操做的结果(好比 ok ),解除socket01 的 AE_WRITABLE 事件与命令回复处理器的关联。这样就完成了一次通讯。

对照下图来看,注意 server socket 其实应该有多个,不要被误导。实在很差意思忘记图出自哪里了,侵删。

注意 Redis 6.0 引入了多线程,具体后边再说。

✔️ 持久化机制

什么是AOF日志?具体是如何实现的?

不一样于通常数据库的写前日志(WAL),AOF 是一种写后日志。AOF 日志是以文本的形式保存下来了 Redis 收到的每一条命令,先写入内存中的缓冲区,而后再择机落入磁盘。因为是命令执行成功后再记录日志,因此记录日志时再也不须要对命令进行语法检查,记录日志的过程也不会阻塞当前的写操做。

但同时也带来了两个问题:一个是若是刚执行完命令尚未写 AOF 日志就宕机了,这时就有数据丢失的问题。另外一个是虽然向磁盘写日志不阻塞当前操做,但有可能会阻塞后续操做,由于 AOF 也是在主线程中执行的,而将数据写入磁盘这个操做是一个相对很慢的操做。

咱们能够经过配置有取舍地解决这两个问题。AOF的配置 appendfysnc 有三个值可选:

  1. Always:同步写回。每一个命令执行完,马上将AOF日志写到磁盘。
  2. Evertsec:每秒写回。每一个命令执行完,先讲AOF日志写到AOF内存缓冲区中,而后每隔一秒将缓冲区中的全部内容写入磁盘。
  3. No:由操做系统控制什么时候将缓冲区内容写回磁盘。

这三种方案都没法作到既兼顾高性能,又兼顾高可靠性。第一种可靠性最高,数据丢失几率很是小,但性能最差。第二种性能适中,宕机最多丢失 1 秒内的数据。第三种性能最好,但宕机时丢失的数据也更多。

AOF 日志愈来愈大怎么办?什么是 AOF 重写机制?重写的过程是怎样的?有哪些地方有可能会阻塞?

AOF 不断地往里追加内容,会变得愈来愈大。文件过大有可能系统有限制,没法保存。添加新的内容时效率也会更低。并且发生宕机后恢复数据须要一条一条执行很是多的命令,过程会变得很慢。这就须要 AOF 重写机制了。

AOF 重写就是以当前数据库的全部键值对为准,从新建立一个新的 AOF 日志,里边记录了全部键值对的写入命令。这样旧的 AOF 日志中对于一个键有可能有不少条命令,重写后就变为一条了。

Redis 为了保证高性能,重写时固然不会让主线程阻塞。重写过程能够总结为**“一个拷贝,两处日志”**。

重写时会由主线程 fork 出一个后台的子进程,fork 会拷贝一份主线程的内存给子进程。此时主线程不会阻塞,仍然继续处理新的操做,新的命令仍然会存在旧的AOF日志中。同时这些新的指令也会存在 AOF 重写缓冲区中。等子进程对拷贝的全部数据都重写完成以后,AOF 重写缓冲区中的内容也会写入新的 AOF 日志,完成以后就能够用新的 AOF 替代旧的了。

这里须要注意的是,Redis 为了不一次性拷贝大量的数据,采用了 Copy On Write 机制。fork 时复制给子进程的其实是内存页表(虚拟内存和物理内存的映射索引表),而不是实际内存数据。此时主进程和子进程共享内存中的数据。当主进程某个 key 有新数据写入时,会分配一块新的内存,将数据写入新的内存。这样主进程和子进程的数据就会逐渐分离。这里须要注意,Copy On Write 的粒度是内存页,也就是说主进程分配到一块新的内存以后,要把当前写入数据所在的内存页一块儿所有 copy 过去。

在这个过程当中,有两个有可能会阻塞的点须要注意:fork 时复制内存页表这个过程会消耗大量 CPU 资源,拷贝时是会阻塞进程的,阻塞时间取决于整个实例的内存大小。另外,Copy On Write 时,复制的粒度是内存页,默认一页的大小是 4kb。若是复制的 key 是一个 bigkey,那么从新申请大块内存并复制也是一个耗时比较长的过程。

还有若是系统开启了内存大页机制(Huge Page,内存页大小为 2M ),那么主进程申请内存后复制时阻塞的时间会大大增加。因此使用 Redis 时建议关闭系统 huge page 功能。(Huge Page 特性主要是为了提升 TLB 命中率,相同的内存大小下,Huge Page 能够减小页表项,TLB就能够缓存更多的页表项,能减小 TLB miss 致使的开销)

其实在不少丢失数据不敏感的业务场景,通常是不须要开启 AOF 的。

什么是RDB?RDB的机制是怎样的?

RDB,即内存快照。Redis 持久化的另外一种方案,配合 AOF 使用口感更佳。

若是只是用 AOF 来作持久化,当数据量很大,操做记录不少的时候,若是要作故障恢复,须要一条一条执行不少命令,效率低下。须要 RDB 来配合处理。

生成 RDB 文件就是将某一时刻Redis中的全部数据以文件的形式写在磁盘上,若是宕机后恢复数据,只须要直接读入内存便可,相比 AOF 效率很高。

Redis提 供了两个指令来生成 RDB 文件:

  1. save:主线程中执行,会致使阻塞
  2. bgsave:建立一个子进程,专门用于写入 RDB 文件,默认配置。

bgsave 命令建立 RDB 文件的过程与 AOF 重写相似,一样借助 COW 技术。从主线程 fork 出子进程(拷贝内存页表),与主线程共享内存数据。当主线程有写操做发生时,复制对应的内存页。

要注意,RDB 文件不宜频繁生成。一方面会给磁盘带来很是大压力,并且有可能出现一次 RDB 尚未写完,后一次就已经开始了,从而陷入恶性循环。另外一方面 fork 的过程是会阻塞主线程的,也会影响性能。

还有就是能够采用增量快照的方式避免屡次全量快照的开销:在一次全量快照以后,记录下哪些数据被修改了,以后生成 RDB 只对修改的数据进行记录。但这会带来另外一个问题:记录修改操做会额外耗费不少的内存,Redis 的内存是很宝贵的资源。

因此说若是只是用 RDB 的方式作数据持久化,没法肯定一个很好的快照频率。若是频率过高影响性能,若是频率过低,宕机发生的话会丢失大量数据。因此通常的用法是 RDB 结合 AOF 同时使用。

以必定频率执行 RDB,在两次 RDB 之间,使用 AOF 日志记录。等到第二次 RDB 的时候,中间的 AOF 就能够清空了。恢复数据时首先使用 RDB 文件恢复大部分数据,而后在使用 AOF 恢复剩余的部分数据,这样就基本达到了鱼和熊掌兼得的目的。

对于 RDB 的频率,Redis 默认的配置是: 知足下边这三种任一种状况,都会执行 bgsave 命令

save 900 1 // 900 秒内,对数据库至少修改 1 次。下面同理
save 300 10
save 60 10000
复制代码

Redis 4.0 以后,AOF 重写时,就是将内存数据以 RDB 的格式写入 AOF 文件的开头。但带来的问题是 RDB 格式的数据可读性不好。

✔️ 主从架构

对于高可靠性,RDB 和 AOF 保证了数据尽可能不丢失,而服务尽可能少中断须要主从架构来保证。Redis 的主从库模式采用的是读写分离的方式。主从库均可以读,但写只能是主库,再由主库同步给从库。

Redis主从之间是如何实现数据一致的?数据同步的过程是怎样的?

在主从库第一次同步时,须要进行一次全量复制。从库和主库创建链接,并告诉主库即将进行同步。主库确认回复后,便可开始同步。

从库须要向主库发送 psync 命令:psync runID offset。runID 是每一个 Redis 实例都会自动生成的一个随机惟一 ID,用来标记示例。offset 是复制的偏移量。第一次复制时,runID 未知,传 ? 。offset 传 -1。 主库收到后会返回本身的 runID,和目前主库的复制进度 offset。

首次复制主库会将所有数据发送给从库,这个过程依赖于 RDB。也就是生成一份 RDB 文件发送给从库,从库会先清空数据库以后,将数据读入内存。在复制的同时,主库也会正常提供服务,并将新的写操做缓存在 replication buffer 中。最后,当 RDB 文件发送完成后,主库把 replication buffer 中的数据发送给从库,从库从新执行这些操做,同步就完成了。

完成了首次全量复制以后,主从之间会维护一个长链接,主库会将后续收到的全部命令经过连接同步给从库。 这里须要注意,若是有不少从节点挂在主节点上,主节点要和全部从库进行全量复制的话,会给主库带来极大的压力。通常会采用 主-从-从 模式,让更多的从节点挂在其余从节点上,这样能够分摊主库的压力。

还须要考虑的地方是,网络链接阻塞甚至断开了怎么办?Redis 2.8 之前一旦主从节点网络断开,从库会从新进行一次全量复制,这个开销是很是大的。2.8 之后从库开始进行增量复制。具体是利用到了 repl_backlog_buffer 缓冲区。

repl_backlog_buffer 是一个环形缓冲区,每有一个从库挂到主库,都会分配一块出来。主库除了将全部命令同步给从库以外,也会在这个缓冲区中记录一份。当从库断开链接重连以后,从新发送命令到主库,主库根据 offset 在 repl_backlog_buffer 中找到断开的位置,将以后的命令发送给从库便可。因为 repl_backlog_buffer 是环形的,因此若是主从断开过久,新的缓存会把旧的缓存覆盖掉,这以后从库再连回来(或者网络延迟、从库执行缓慢致使),那就不得再也不从新进行一次全量复制了。 因此要控制好 repl_backlog_size 这个参数的大小。通常粗略计算为:repl_backlog_size = 主库写入命令速度 * 命令大小 - 主从库命令传输速度 * 命令大小。考虑到一些突发的请求压力,通常还会再在结果上乘2。若是并发峰值特别大,那么还能够设置为更大,或者考虑使用切片机群来分担主库请求压力。后边再说。

注意区分 replication buffer 和 repl_backlog_buffer。

前者是 Redis 服务端与客户端通讯时,用来交互数据的缓存。每一个客户端链接都会分配一块 buffer 出来。Redis 先把数据写入这个 buffer,而后将 buffer 中的数据发到 client socket 中经过网络发送出去。从库也是一个 client,也是同样的,专门用来将用户的写命令从主库传到从库。Redis 提供了 client-output-buffer-limit 参数限制这个 buffer 的大小,若是超过限制,主库会强制断开从库的链接。若是不限制,从库处理请求的速度又很慢的话,这个 buffer 会无限膨胀,最终致使 OOM。

然后者是为了主从同步设计的,避免一旦断开就要进行全量复制的性能开销。这个 buffer 只用来对比主从数据差别,真正信息传递仍是要靠 replication buffer。

什么是Redis的哨兵机制?基本流程是怎样的?

哨兵机制实现了Redis主从集群故障转移的功能,若是主库挂掉,能够自动执行主从切换。

哨兵机制主要解决主从切换的三个问题:

  1. 如何判断主库真的挂掉了?(监控)
  2. 选择哪一个从库做为主库?(选主)
  3. 如何把新主库的信息通知到从库和客户端?(通知)

哨兵机制的流程:

哨兵进程在运行时,会周期性地给全部主从库发送 PING 命令,监测他们是否正常运行。若是一个节点没有在规定的时间内响应哨兵,则会被标记为“主观下线”。

若是是从节点,下线影响不大,标记完就行了。若是是主节点,不能直接开始主从切换。由于有可能存在误判,好比网络堵塞或是主库压力比较大。主从切换的开销很大,必需要避免没必要要的开销。

哨兵通常也会集群部署,因此须要有超过一半的哨兵都认为主库“主观下线”,主库才会被标记为“客观下线”。这时才会触发主从切换流程。

哨兵经过筛选加打分来选择新的主库。

若是从库老是和主库断连,则说明这个从库网络情况很差,不适合作主库。这样的节点会被筛选掉。

剩下的节点中会进行三轮打分。

第一轮优先级最高的从库得分高。咱们能够经过配置,给从库不一样的优先级。人为给一个性能最好的机器上的从库优先级设为最高,那么主从切换时就会选这个从库做为主库。

第二轮判断从库和旧主库的同步程度,越接近的得分越高。这里是经过主从同步的 repl_backlog_buffer 中的 offset 对比判断。

若是还相等,就进行第三轮判断,ID号小的从库得分高。

选出新的主库以后,哨兵会把新主库的链接信息发送给其余从库,让他们执行 replicaof 命令,和新主库创建链接,并进行数据同步。同时哨兵也会通知客户端,让客户端将请求操做发送到新的主库节点。

须要注意的是,在主从切换期间,若是是读写分离的,那么读请求能够在从库正常执行。但主库挂掉,并且尚未新主库产生时,写请求会失败。若是不想让客户端感知到主从切换,须要客户端将写请求写入消息队列中,待主从切换完成后再从消息队列中拉取指令执行。

客户端与哨兵之间经过广播的方式同步主从节点信息。哨兵会向客户端广播主库地址。客户端也能够主动向哨兵获取,通常的SDK都封装了相应功能。

哨兵集群之间的通讯是经过 Redis 的 pub/sub 机制进行的。哨兵只要和主库创建了链接,就会在主库的一个固定主题 sentinel:hello 下发布本身的ip和端口等信息。全部的哨兵都会经过对这个主题的订阅和发布来发现其余哨兵。发现以后他们会彼此之间创建网络链接来通讯。

哨兵还须要链接从库来进行监控。这是经过向主库发送 INFO 命令来完成的。主库接收到 INFO 命令以后,会将从库列表发送给哨兵。

哨兵还须要链接客户端,向客户端广播监控、选主、切换等各个过程当中发生的事件。这也是经过 pub/sub 机制完成的。哨兵提供了多个消息主题,不一样主题包含了不一样的关键事件。好比主库主观上/下线,客观上/下线,主从切换等。

由哪一个哨兵来执行主从切换是如何肯定的?若是有哨兵挂了还能执行主从切换吗?

哨兵会经过选举机制来选出执行主从切换的哨兵。结合判断主库下线的流程一块儿来看:

当任何一个哨兵发现主库主观下线后,就会向其余哨兵发送消息,让他们马上确认主库状态。其余哨兵若是发现主库也主观下线下,返回 Y,不然返回 N。当一个哨兵得到了大于等于配置项中的 quorum 给定的数量的同意票时(包括本身的一张同意票),该哨兵标记主库为客观下线。

此时该哨兵会向其余哨兵发送一条消息来发起投票,表示但愿由本身来执行主从切换(至关于投资及一票)。最终成为 Leader 去执行主从切换的哨兵须要达成两个条件:得到超过半数以上的同意票而且同意票数量要大于等于 quorum。(3 个哨兵,quorum=2,那么就须要 2 票来当选)。

在一轮投票中,每一个哨兵只能投一票。当一个哨兵收到了其余哨兵的发起投票消息时,会投票给最早收到本轮投票消息的那个哨兵。或者本身自己已经判断主库客观下线,也发起了选举投票,那么他已经投票给本身了,就不能再投票给别的哨兵了。

若是在一轮投票中,没有诞生 Leader,哨兵集群会等待一段时间以后,从新进行选举。若是诞生了 Leader,就由 Leader 哨兵来执行主从切换。

须要注意:若是哨兵集群只有 2 个实例,一个哨兵想要成为 Leader,必须得到 2 票同意,此时的哨兵集群没法选举出 Leader,没法执行主从切换。因此一般至少要配置 3 个哨兵。

✔️ 切片集群

须要存储的数据量特别大的时候怎么办?什么是切片集群?官方提供的 Redis Cluster 是如何实现的?

比较容易想到的方案是:纵向扩展。也就是说升级单个 Redis 实例的资源配置,如增长内存容量、磁盘容量,使用更高配置的 CPU。但这个方案面临两个难点:一是会收到硬件和成本的限制。内存越高,升级时成本越大。二是以前提到的,RDB 持久化时,fork 子进程的操做的耗时,是和数据量成正比的,特别大的数据量会致使 fork 时阻塞主线程的时间变长。但纵向扩展的优势也很明显,那就是实现简单直接。

那么有没有更好的解决方案呢?有,那就是横向扩展,也就是 Redis 切片集群:横向增长 Redis 实例的数量,将数据均匀存放在多个实例上。那么就带来了两个问题:数据切片后,在多个实例之间如何分布?客户端如何找到想要的数据在哪一个实例上?

Redis 3.0 开始提供了 Redis Cluster 方案实现切片集群。采用哈希槽来处理数据与实例的映射关系。一个切片集群一共有 16384 个哈希槽,每一个键值对都会根据它的 key 映射到一个哈希槽中:首先根据 key,按照 CRC16 算法计算获得一个 16bit 的值,而后再用这个值对 16384 取模,获得的就是在哈希槽中的位置。当咱们使用 cluster create 命令建立集群时,Redis 会自动将 16384 个槽位平均分配到全部实例上。固然也能够手动指定每一个实例上的槽位的数量。(注意手动分配时,必须将 16384 个槽位所有分配完,不然集群没法正常工做)。

那么客户端在访问集群时,计算出了须要的数据在哪一个哈希槽以后,如何肯定哈希槽在哪一个实例上呢?Redis 实例会把本身的哈希槽信息发给和他相链接的其余实例,完成哈希槽分配信息的扩散,集群中的每一个实例都会有全部哈希槽和实例的映射关系了。客户端会将收到的映射信息缓存在本地,当请求时,就能够根据哈希值找到哈希槽再找到实例了。

可是集群中的实例是会有新增和删除的,当新增或删除发生时,为了负载均衡,Redis 会将哈希槽在全部实例上从新分配一遍。这样哈希槽的映射关系就改变了,这时客户端又该怎么办呢?Redis Cluster 方案提供了一种重定向机制。若是客户端向一个实例发送了读写请求,可是该实例上并无这个键值对对应的哈希槽,他会返回 MOVED 命令相应结果,其中包含了正确的实例的访问地址:

GET hello:key
(error) MOVED 13320 172.16.19.5:6379
复制代码

还有另外一种状况,由于一个哈希槽中会分布多个键值对,那么就会有访问的哈希槽正在迁移中,一个部分已经到了新的实例上,一部分还在原实例上。这时若是客户端发送读写请求,若是旧实例上有要找的数据,那就正常执行指令。若是没有,会返回 ASK 命令给客户端,其中也携带了新的实例的地址。客户端收到后会向新实例发送 ASKING 命令请求。也就是说 ASK 命令表示槽位正在迁移中,我这里没有说明可能已经迁移到新的地址了,你去新的地址找找看。不会想 MOVED 命令更新客户端缓存的哈希槽映射信息,这样就避免了迁移中的数据找不到的状况发生。

为何 Redis Cluster 要采用「 key —> 哈希槽 —> 实例 」的方式?而不是直接储存 key 和实例之间的映射关系?

主要有如下几点缘由:

  1. 整个集群的 key 的总量是没法估量的,若是直接记录 key 和实例的映射关系,当 key 特别多时,这个映射表会很是庞大,不管存储在服务端仍是客户端都要占用大量的存储空间。而 Redis Cluster 方案中的哈希槽的总数是固定的,不会过分膨胀。
  2. Redis Cluster 采用的是无中心化的模式,客户端向某个节点访问一个 key,若是这个节点没有这个 key,须要有帮客户端纠正错误,路由到正确节点上的能力(MOVED/ASK)。这就须要每一个节点都拥有完整的哈希槽映射关系,节点之间须要交换这些信息。若是存储的是 key 和实例的映射,节点之间交换信息的量会很是大,大量消耗网络资源。
  3. 当集群中实例增长/减小,以及均衡数据的时候,节点之间要发生数据迁移,这会须要修改每一个 key 与节点的映射关系,维护成本很是高。
  4. 在 key 与实例之间增长一个中间层哈希槽,至关于将数据和节点解耦。key 经过哈希计算,只关系对应哪一个哈希槽。只消耗了不多的 CPU 资源,让数据分配的更均匀,并且还让映射关系的存储占用空间变得很小,有利于客户端和服务端的存储,节点交换信息也更加轻量。
  5. 当集群实例增长减小,数据均衡时,只须要以哈希槽为单位进行操做,简化了集群维护和管理的难度。

补充一点:哈希槽其实本质上就是一致性哈希 —— 有 16384 个槽位的哈希环。但相比直接的一致性哈希,哈希槽的方式多了一个中间层,也就是槽位,达到了解耦的目的,更方便数据迁移,下降维护难度。

相关文章
相关标签/搜索