《Redis设计与实现》读书笔记(二十八) ——Redis集群节点结构与槽分配

《Redis设计与实现》读书笔记(二十八) ——Redis集群节点结构与槽分配

(原创内容,转载请注明来源,谢谢)

一、概述

redis集群是redis的分布式数据库的解决方案,集群通过分片(sharding)来进行数据共享,并提供复制和故障转移的功能。

二、集群的节点

1、节点组成

一个redis集群由多个节点组成,每个节点是一个运行在集群模式下的redis服务器。集群还没建立好时,每个节点可以看成是一个独立的集群,将各个节点联系起来,就会形成一个真正有效的集群。

集群的命令是,clustermeet <ip> <port>,向一个节点node发送此命令发送给另一节点,可以让节点node与发送的ip:port进行一次握手,成功后node将记录该ip与port,添加到集群中。

clusternode命令,可以查看当前节点的集群。

2、启动节点

启动节点的时候,服务器会根据redis配置文件中的cluster-enabled选项,来决定该服务器是否开启集群模式。

由于节点也是一个redis服务器,因此其会使用redis服务器的各种特性,包括用文件事件来处理命令和回复、执行serverCron定时函数、键值对保存的方式、rdb与aof持久化、发布订阅、复制、lua脚本环境与执行lua脚本过程、用redisServer保存服务器状态、redisClient保存客户端状态等。

但是,集群的节点也有一些特殊性,包括特有的定时函数clusterCron(用于节点的其他数据发送gossip消息,检查节点是否断线,检查是否需要对下线节点进行故障转移等),另外还有集群节点特殊的数据结构,保存在cluster.h文件中,包括clusterNode、clusterLink、clusterState等。

三、集群节点数据结构

1、clusterNode结构

该结构保存节点的当前状态,包括名字、ip地址、创建时间、配置纪元、端口号等。集群里的每个节点,都会建立一个clusterNode来记录自身状态,并为集群中的其他节点(包括主从节点)建立相应的clusterNode结构。

主要信息如下:

         struct clusterNode{
         mstime_t ctime;//节点创建的时间
         char name[REDIS_CLUSTER_NAMELEN];//节点名称,由40位16进制字符组成
         int flags;//节点标识,标记节点主从、是否下线等状态
         uint64_tconfigEpoch;//节点创建纪元,用于故障转移
         char ip[REDIS_IP_STR_LEN];//节点ip地址
         int port;//节点端口号
         clusterLink *link;//保存节点所需的有关信息
         //….其他内容省略
}

2、clusterLink结构

该结构是保存在clusterNode中的一个属性,其用于保存节点所需有关信息,如套接字描述符、输入缓冲区和输出区缓冲等。

         typedef struct clusterLink{
         mstime_t ctime;//节点连接的创建时间
         int fd;//套接字描述符
         sds sndbuf;//输出缓冲区,保存等待发送给其他节点的信息
         sds rcvbuf;//输入缓冲区,保存从其他节点接收到的的信息
         struct clusterNode*node;//与这个连接相关的节点,没有就是null
}

redisClient和clusterLink结构都有套接字和相应的输入和输出缓冲区,但是区别在于redisClient是用于连接客户端,clusterLink用于连接其他节点。

3、clusterState结构

每个节点都保存一个这个结构,记录当前节点视角下,集群目前所处的状态,例如集群是否在线、当前有几个节点、当前配置纪元等。

         typedef struct clusterState{
         clusterNode  *myself;//指向当前节点的指针
         uint64_tcurrentEpoch;//集群当前配置纪元,用于故障转移
         int state;//集群当前状态,是否在线
         int size;//集群中至少处理一个槽的节点数量
         dict *nodes;//集群节点名单,包括myself节点,键是节点名称,值是对应的cluster Node结构
         //….其他信息
}clusterState;

4、节点结构

三个节点组成的集群,如下图所示:

其中,每个节点都会有一个这样的结构,区别在于myself指针都是指向各自节点自己。

4、cluster meet实现

cluster meet <ip> <port>,对节点a发节点b的ip,则将节点b加入到节点a的集群中,同时节点b也会将jieda加入到集群中。流程如下:

1)节点a为节点b创建一个clusterNode结构,并将该结构添加到自己的clusterState.nodes字典里面。

2)节点a根据ip和端口号,给节点b发送一条meet消息。

3)b会为a创建一个clusterNode,也添加到b自己的clusterState.nodes。

4)b向a回复pong信息。

5)a收到后,会再给b发送一个ping信息。

6)b接收到a的ping信息后,握手完成。

握手流程如下:

7)之后,节点a会将节点b的信息,以gossip协议传播给a当前集群中的其他节点,这样其他节点也会与b进行上述操作。

四、槽(slot)指派

1、概述

redis集群通过分片的方式,来保存键值对。集群的整个数据库被分为16384个槽,数据库中的每个键,都属于16384个槽中的一个,集群每个节点可以处理0~16384个槽。

当集群中,每一个槽都有节点在处理时,则这个集群是上线(ok)的状态;任意一个槽没有节点处理,则该集群下线(fail)。

采用命令cluster info,可以查看当前集群的状态,ok是上线,fail是下线。

通过向节点发送clusteraddslots <slot1> [slot2 slot3 ….]命令,可以将槽指派给节点。槽是用数字从0~16383进行编号的。

在哪个节点输入clusteraddslots,则对该节点指派槽。

2、记录节点指派信息

节点指派的槽的信息,记录在clusterNode结构体中:

         struct clusterNode{
         //….其他信息
         unsigned char slots[16384/8];//二进制数组
         int numslots;//节点处理的槽数量
         };

slots是一个二进制位数组,长度是2048个字节,共包含16384个二进制位。每一个下标代表8个槽,用二进制位表示。如果节点负责某个槽,则数组下标对应的二进制位的相应位置的值是1,否则是0。

例如,下图中的数组,表示节点负责的槽是1、3、5、8、9、10这几个。

使用二进制的方式,目的是便于获取、修改节点负责的槽,因为时间复杂度都是O(1)。

3、传播节点槽指派信息

节点被分配了槽,不仅会记录在节点自身的clusterNode结构体,还会将信息传播给集群的其他节点。

节点a收到节点b的槽分配信息,会从自身记录节点b信息clusterNode的结构中,相应的属性slots与numslots记录槽的位置与槽的数量。

4、记录集群所有槽指派信息

指派信息记录在clusterState结构体中:

typedef stuct clusterState{
//….其他信息
clusterNode *slots[16384];
}clusterState;

这个结构体中,数组每个下标表示一个槽,下标的值都是指针,指向负责该槽的节点。如果某个数组下标是null,表示目前没有节点负责该槽。

redis的设计非常巧妙,该slots属性可以快速找到每个槽对于的负责的节点,而节点内部clusterNode结构的slots,可以快速查找、改变某个节点负责的槽,且获取某个节点负责的全部槽的速度比从clusterState的slots中快得多。

5、槽指派的实现

槽指派之前,会先检查槽是否已经有节点负责,如果一个或以上的槽已经有节点负责,则停止指派,并且报错。

如果所有槽都没有节点负责,则修改clusterState的slots数组,将每个槽下标的值指向该节点的clusterNode结构;并修改该clusterNode的slots数组,将槽对应的二进制位置设置成1。

例如,执行命令clusteraddslots 1 2,节点变化如下:

完成上述命令写入后,节点会发消息通知集群的其他节点。

—written by linhxx 2017.09.15

原文发布于微信公众号 - 决胜机器学习(phpthinker)

原文发表时间:2017-09-15

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Java帮帮-微信公众号-技术文章全总结

01.线程状态/创建/启动

01.线程状态/创建/启动 多线程作为Java中很重要的一个知识点,在此还是有必要总结一下的。 一.线程的生命周期及五种基本状态 关于Java中线程的生命周期,...

4037
来自专栏Golang语言社区

Go语言_并发篇

当被问到为什么用Go语言,一定不得不提的是Go语言的并发程序编写。在C语言中编写非常繁琐复杂的并发程序在Go语言中总是显得如此便捷。 Go中并发程序依靠的是两个...

2777
来自专栏数据结构与算法

22:紧急措施

22:紧急措施 总时间限制: 1000ms 内存限制: 65536kB描述 近日,一些热门网站遭受黑客入侵,这些网站的账号、密码及email的数据惨遭泄露。你...

3918
来自专栏码生

django 学习笔记三

拦截到url 后,通过拦截到的URL和 request 做一些不同的响应(response)

1132
来自专栏社区的朋友们

django 初始化默认数据的一个方法

经常可能会有的一个需求就是,一个表中会有一些默认数据,这些数据未来是可以通过管理端来配置的。所以数据库在被建立之后,会有一些默认数据被插入表中。

2.7K3
来自专栏Java大联盟

Java面试手册:线程专题 ①

1372
来自专栏Android干货

Python IO编程

循环读取文件内容,一般读取文件内容一次读取完,内存是不够的,就要实现一次次少量数据读取

1202
来自专栏乐百川的学习频道

Flask 快速入门

Flask是一个Python编写的Web 微框架,让我们可以使用Python语言快速实现一个网站或Web服务。本文参考自Flask官方文档,大部分代码引用自官方...

33510
来自专栏Golang语言社区

Go语言_并发篇

当被问到为什么用Go语言,一定不得不提的是Go语言的并发程序编写。在C语言中编写非常繁琐复杂的并发程序在Go语言中总是显得如此便捷。 Go中并发程序依靠的是两个...

2894
来自专栏北京马哥教育

linux实用技巧:你该使用ctags查找源码了

linux实用技巧:你该使用ctags查找源码了 ---- 1.ctags简介: “哦,这个多的文件,我该如何去查看XX函数的实现!”相信...

3206

扫码关注云+社区

领取腾讯云代金券