《Redis设计与实现》笔记

200 阅读15分钟

动态字符串

SDS(simple dynamic string)

SDS对比C语言中的string

C语言:需要遍历完整个string,找到末尾的空字符才知道整体长度,时间复杂度=O(N)

Redis:直接访问len就可以知道string实际长度


C语言: 如果忘了提前分配内存,那么会造成缓存溢出(见下图解释)

Redis: 长度小于1MB,扩容后为原先的两倍; 长度大于1MB,扩容后增加1MB; 字符串的长度最大值为 512MB

链表

链表的结构

这里注意一下:list结构和ListNode结构

image.png

字典

哈希表、哈希表节点、字典的实现

这里有一个问题,下图table数组的大小是多大呢?固定的还是可变的?这里就要用到下面的Rehash

image.png

对上面的存取步骤进行一下个人的理解:

首先计算键的hash值,看看是在上面4个指针中的第几个(就像查字典,查一个字的偏旁是什么)

然后拿着计算出来的hash值去找页数(拿着偏旁找页数,这里假设所有相同偏旁的都放在一个链表里也就是一个地方)

第一个不是就顺着链表往下找,找不到了那就说明不存在了

image.png

ReHash

什么时候使用rehash?

以下情况需要扩展

  • 服务器目前没有在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于1。
  • 服务器目前正在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于5。

以下需要收缩

  • 当哈希表的负载因子小于0.1时,程序自动开始对哈希表执行收缩操作。

怎么rehash?

image.png

渐进式rehash

对于所含k-v很多的dictht[0]来说,一次rehash完不现实,甚至可能造成宕机,所以他是一点一点来的

因为在进行渐进式rehash的过程中,字典会同时使用ht [0]和ht [1]两个哈希表,所以在渐进式rehash进行期间,字典的删除( delete)、查找( find)、更新( update)等操作会在两个哈希表上进行。例如,要在字典里面查找一个键的话,程序会先在ht[0]里面进行查找,如果没找到的话,就会继续到ht [ 1]里面进行查找,诸如此类。 另外,在渐进式rehash执行期间,新添加到字典的键值对一律会被保存到ht[1]里面,而ht[0]则不再进行任何添加操作,这一措施保证了ht [0]包含的键值对数量会只减不增,并随着rehash操作的执行而最终变成空表。

跳跃表

跳跃表数据结构

image.png

为什么左边那个不算在length里面呢?可能因为他是起点位置,是最高的

zskipListNode结构

还是以上图为例

  • 层:就是节点的高度,一般是在1-32之间随机生成,他用这个主要是用来加快访问速度的
  • 前进指针:L1-L32都有一个指针,指向下一个元素
  • 跨度:前进指针上标的那个数字,用来表示例如两个L5之间的距离
  • 后退指针:BW
  • 分值和成员:就是o1、o2、o3上面的那个float小数,按照从小到大的数序排列(为什么见下)

跳跃表怎么工作的?

image.png
  • 从Skip List跳跃列表最顶层level3开始,往后查询到10 < 88 && 后续节点值为null && 存在下层level2
  • level2 10往后遍历,27 < 88 && 后续节点值为null && 存在下层level1
  • level1 27往后遍历,88 = 88,查询命中

整数集合

整数集合的数据结构

image.png

升级与降级

  • 升级:ontents[]中都是16位的数据,这时候如果插入一个32位的数据的话,前面所有16位的数据都会升级成32位的数据
  • 降级:如果编码是32位,但是里面数据16位就可以表示,那么也是不会降级的,系统不支持

压缩列表

缩列表数据结构

image.png

连锁更新

image.png

五大对象

Redis对象是什么?

image.png

REDIS_STRING

REDIS_ENCODING_INT

image.png

REDIS_ENCODING_RAW

系统需要分派两次内存,一次是redisObject,一次是sdshdr

image.png

REDIS_ENCODING_EMBSTR

相比起raw,系统只会分配和释放内存的次数都只有一次。什么时候用它呢:字节数<32的时候

image.png

REDIS_LIST

REDIS_ENCODING_ZIPLIST

元素大小<64B&&元素个数<512,使用ziplist

image.png

REDIS_ENCODING_LINKEDLIST

元素使用的是StringObject,使用的是对象,这是因为StringObject是唯一一个可以被其他对象所引用的

image.png

REDIS_HASH

REDIS_ENCODING_ZIPLIST

key对象和value对象大小都<64B&&k-v键值对个数<512,使用ziplist

image.png

REDIS_ENCODING_HT

为什么这里的dict实现细节会和上面讲到的实现细节是不一样的呢?这里明显看不出使用hash的痕迹,更像是一个数组+指针的形式,这就不满足hash表的特点了,(个人猜测是为了图方便的)

image.png

REDIS_SET

REDIS_ENCODING_INTSET

所有元素都是整数值&&元素个数<512,使用intset

image.png

REDIS_ENCODING_HT

image.png

REDIS_ZSET

REDIS_ENCODING_ZIPLIST

所有元素大小都<64B&&元素个数<128,使用ziplist

image.png

REDIS_ENCODING_SKIPLIST

image.png

为什么有序列表同时需要跳跃列表+字典?

在理论上,有序集合可以单独使用字典或者跳跃表的其中一种数据结构来实现,但无论单独使用字典还是跳跃表,在性能上对比起同时使用字典和跳跃表都会有所降低。

举个例子,如果我们只使用字典来实现有序集合,那么虽然以o(1)复杂度查找成员的分值这一特性会被保留,但是,因为字典以无序的方式来保存集合元素,所以每次在执行范围型操作——比ZRANK、ZRANGE等命令时,程序都需要对字典保存的所有元进行排序,完成这种排序需要至少o(NlogN)时间复杂度,以及额外的o(N)内存空间(因为要创建一个数组来保存排序后的元素)。

另一方面,如果我们只使用跳跃表来实现有序集合,那么跳跃表执行范围型操作的所有优点都会被保留,但因为没有了字典,所以根据成员查找分值这一操作的复杂度从o(1)上升为o(logN)。因为以上原因,为了让有序集合的查找和范围型操作都尽可能快地执行,Redis 选择了同时使用字典和跳跃表两种数据结构来实现有序集合。

不用担心资源的浪费:系统内部会把他们指向同一个资源

内存回收+对象共享

简而言之:就是每个对象都有一个计数器,创建的时候=1,多一个引用+1,去一个引用-1,当=0的时候会进行回收

复制

首先想一下为什么要进行复制

网上看到的:就是Redis持久化了之后,但是磁盘也可能会丢失,所以可以多复制两份,是否是这样的还不知道?后面再返回来回答

旧版复制的原理、缺点

image.png

以下是SYNC命令的步骤:

image.png

以下是同步传播的介绍:

image.png

旧版的复制的主要的缺点就在于:SYNC这个命令

如果在SYNC的过程中断线了,那么重新连线的时候,SYNC这个命令会重新从0开始进行同步操作,而进行一次SYNC是机器浪费空间和时间的

新版复制的原理

针对旧版本的SYNC的问题,新版本使用PSYNC来代替SYNC:

image.png

主+从服务器复制偏移量

偏移量:简单理解就是已经传输了多少了多少字节,表示的是字节数(offset保存的应该是地址)。主+从服务器保存的offset一致还好,下次可以直接接着传输信息,但是如果不一样呢,比如从服务器offset小(因为网络原因没有收到),下次重新传的时候怎么办呢?如果部分重传的话,要怎么补偿掉线时候丢失的数据呢?

复制积压缓冲区

主服务器把最近的写操作写进到缓冲区里(是一个队列,但是长度固定,没满能一直进,满了就必须进一个出一个) 如果缺的offset在缓冲区里,那么就直接部分重新复制,否则全复制,缓冲区结构如下:

image.png

运行ID

运行ID在服务器启动时自动生成,由40个随机的十六进制字符组成,例如53b9b28df8042fdc9ab5e3fcbbbabff1d5dce2b3。简言之就是服务器的身份证

一次完整的复制实现

1.设置主服务器的IP地址+端口号:就是要设置主从服务器的套接字

2.建立套接字连接:主从服务器通过套接字建立连接,例如主服务器127.0.0.1:6379,从服务器127.0.0.1:12345,这时候从服务器已经是作为主服务器的一个客户端了

3.从服务器发送Ping信号:为了确保套接字是否正常+主服务器是否能处理命令请求,一切正常主服务器返回Pong信号

4.身份验证

5.从服务器发送端口信息: 主服务器在接收到这个命令之后,会将端口号记录在从服务器所对应的客户端状态的slave_listening port属性中

6.同步

7.命令传播

8.心跳检测

Sentinel(哨兵)

Sentinel是什么?

image.png

获得主服务器的信息(主服务器自身信息+它的从服务器)

image.png

获得从服务器的信息

image.png

向主服务器和从服务器发送信息

image.png

s_开头:告诉这个服务器我实在监视它,我自己是谁

m_开头:告诉主服务器他自己的信息,告诉从服务器,它的主服务器的信息

接受主+从服务器的频道消息(用于更新不同哨兵维护的同一个服务器实例)

image.png image.png

上面的所有前提是: 多个Sentinel(记a、b、c...)监听同一个服务器(记A)的情况

命令连接是Sentinel用来发送信息的,订阅连接Sentinel是用来接收消息的,说白了就是所有监视一个同一个服务器的Sentinel之间可以交互信息,这个交互的信息用来更新a、b、c...中自己各自维护的A实例,比如下图中的这个服务器示例的哨兵列表

image.png

Sentinel之间的命令连接(用于检测下线状态、选举领头、故障转移)

image.png

检查主观下线状态

image.png

检查客观下线状态

image.png

选取领头Sentinel

image.png

故障转移

image.png

集群

节点

首先弄明白节点是什么?

节点是:运行在集群模式下的Redis服务器

集群中的数据结构是什么?

ClusterNode:描述自身

每个服务器都有N个

image.png

ClusterState:描述自身对于全局的把握

每个服务器有1个

image.png

集群中的节点之间是怎么互相联系的?

image.png

槽指派

槽是什么?

Redis 集群中内置了 16384 个哈希槽,当需要在 Redis 集群中放置一个 key-value时,redis 先对 key 使用 crc16 算法算出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点。

Redis 集群没有使用一致性(?), 而是引入了哈希槽的概念。

Redis 集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽.集群的每个节点负责一部分hash槽。这种结构很容易添加或者删除节点,并且无论是添加删除或者修改某一个节点,都不会造成集群不可用的状态。

使用哈希槽的好处就在于可以方便的添加或移除节点。 当需要增加节点时,只需要把其他节点的某些哈希槽挪到新节点就可以了; 当需要移除节点时,只需要把移除节点上的哈希槽挪到其他节点就行了; 在这一点上,我们以后新增或移除节点的时候不用先停掉所有的 redis 服务。

这个优点可以对比一下普通的hash算法:比如有4台Redis服务器,普通hash思路是:%4来选择Redis服务器,但是这样的话,如果服务器数量发生变化,比如变成5台要%5,这样以前的每个东西都要发生变化,改变的成本太大。自我理解:槽就相当于是设置了虚拟的16384台服务器,直接计算%16384计算自己在哪个槽,虚拟的服务器是装在实际的服务器上面的,你计算出自己的虚拟位置(哪个槽)之后就可以去找他所在的真实的服务器了,这样真实的Redis服务器发生变化也不会成本很高,只需要变动有限台就好了

系统是怎么知道某个节点处理哪几个槽的?

自己怎么知道自己处理的是哪几个槽?ClusterNode可以看出来

用16384个二进制(÷8=2048个字节)来记录自己可以处理哪几个槽,如下图

image.png

自己怎么知道队友处理的是哪几个槽?ClusterState可以看出来

ClusterNode是对自身信息的把握,ClusterState是对全局信息的把握:前者是知道自己存储了哪些个槽,后者是知道所有槽存在于哪个节点

image.png

那么只有ClusterState没有ClusterNode行不行?显然不行,原因如下:

image.png

当所有槽都分配完毕,集群是怎么执行命令的?

比如增删改查k-v键值对的时候,会先对键用hash看看是否是在自己的节点之上,在的话执行操作,不在的话把节点的ip+端口号发给客户端,让客户端自己再去找对的节点

image.png

发送来的指令是MOVED指令

image.png

重新分片+ASKING命令

重新分片是什么

举个例子,对于之前提到的,包含7000、7001、7002三个节点的集群来说,我们可以向这个集群添加一个P为127.0.0.1,端口号为7003的节点(后面简称节点7003 ) :然后通过重新分片操作,将原本指派给节点7002的槽15001至16383改为指派给节点7003。具体实现的原理见书本266页。

重新分片的时候是怎么知道:我给谁,我接受谁的呢?还是得看ClusterState:如下图,左图是7003的State,右图是7002的State,都表示的是7002的16198槽给到7003

image.png

ASK错误

当客户端向源节点发送一个与数据库键有关的命令,并且命令要处理的数据库键恰好就属于正在被迁移的槽时: 源节点会先在自己的数据库里面查找指定的键,如果找到的话,就直接执行客户端发送的命令 相反地,如果源节点没能在自己的数据库里面找到指定的键,那么这个键有可能已经被迁移到了目标节点,源节点将向客户端返回一个ASK错误,指引客户端转向正在导入槽的目标节点,并再次发送之前想要执行的命令。

其中ASK错误是用户不可见的

ASK错误和MOVED错误的区别

ASK错误:就是某个槽在重新分片过程中,先去旧服务器的槽里查一下,找到就改,找不到再去新服务器的槽里找一下,是一种临时性的操作

MOVED错误:直接把后面的所有权限都改了,以后都去新服务器的槽里改,是一种永久性的操作

复制与故障转移

主节点+从节点

主节点:处理槽的节点,也就是正在工作的节点

从节点:主节点下线的时候,处理主节点的槽的备用的节点,一个从节点只能是一个主节点的复制

image.png

有没有类似于上面的sentinel(哨兵)系统呢?用多个节点来确保数据的不可丢失性

主节点和从节点的ClusterState结构(上图17-32)

image.png image.png

故障检测与故障转移

image.png

怎么选取新的主节点呢(7004和7005中怎么选取出新的主节点呢?)这里就要理解配置纪元,可以搜索一下Raft算法这个东西了。

当从节点知道主节点挂了之后,就马上群发消息,内容是投我投我,我是川普(😂),另一个说投我我是拜登,然后有投票权的主节点只有一次投票机会,先收到谁的消息就投谁,不可更改,最后谁的支持数超过一半谁就成为主节点。

故障转移过程先按下不表,就是把原来主节点的槽都设置成自己的

故障转移之后新的主节点会主动给所有发送一条PONG指令,告诉所有节点我从从节点变成了主节点。

消息

集群中的各个节点之间是怎么传输信息的呢?

image.png