08-redis集群

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

节点

集群有多个节点组成,刚开始时,每个节点相互独立,都处于只包含自己的集群中,要组建一个集群,就需要将其余节点连接起来,构成一个包含多个节点的集群
通过如下命令查看当前节点以及集群中其余节点信息:

CLUSTER NODES

通过如下命令来完成连接各个节点:

CLUSTER MEET <ip> <port>

启动节点

根据配置文件中cluster-enabled配置选项决定是否启用集群模式

集群数据结构

每个节点会使用一个cluster结构来记录自己的状态,并为集群所有其余节点都创建一个相应的cluster结构,以此记录其他节点状态

struct clusterNode {
    mstime_t    ctime;                   // 创建节点时间
    char        name[REDIS_CLUSTER_NAMELEN];    // 节点名字,有40个十六进制字符组成
    int         flags;                   // 节点标识
    uint64_t    configEpoch;              // 节点当前配置纪元,用于实现故障转移
    char        ip[REDIS_IP_STR_LEN];     // 节点的IP地址
    int         port;                   // 节点的端口号  
    clusterLink  *link;                  // 保存连接节点所需的有关信息  
    
    ...;
};

clusterNode结构的link属性是一个clusterLink结构,该结构报讯了连接节点所需的有关信息

typedef struct clusterLink {
    mstime_t    ctime;          // 连接创建时间
    int         fd;             // TCP套接字描述符
    sds         sndbuf;         // 输出缓冲区
    sds         rcvbuf;         // 输入缓冲区
    struct clusterNode *node;     // 与这个节点相关联的节点,如果没有的话为NULL
} clusterLink;

每个节点都保存着一个clusterState结构,这个结构记录了在当前节点的视角下,集群目前所处的状态

typedef struct clusterState {
    clusterNode     *myself;        // 指向当前节点的指针
    uint64_t        currentEpoch;   // 集群当前的配置纪元,用于实现故障转移
    int             state;         // 集群当前状态:在线还是下线
    int             size;          // 集群中至少处理着一个槽的节点数量
    dict            *nodes;        // 集群节点名单(包括myself节点)
    
    ...;
} clusterState;

CLUSTER MEET命令的实现

通过向节点A发送CLUSTER MEET命令,客户端可以让接受命令的节点A将另一个节点B添加到节点A当前的集群当中:---- AB节点进行握手

​ A -> B : A为B创建clusterNode结构,向B节点发送MEET消息

​ B -> A : B接受到消息,为A创建clusterNode结构,向A节点回复PONG消息

​ A -> B : A接受到PONG消息后,向B发送PING消息

这样握手完成,之后,节点A将节点B的信息通过Gossip协议传播给集群中其余节点,让其他节点也与B节点进行握手

最终,一段时间后,节点B会被集群中所有节点认识

槽指派

redis集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分为16384个槽(slot),数据库中的每个键都属于这16384个槽的其中一个,集群中的每个节点可以处理0个或最多16384个槽

当16384个槽都有节点处理时,集群处于上线状态;任何一个槽没有被处理,那么集群处于下线状态,通过如下命令查看:

CLUSTER INFO

通过如下命令指定槽:

CLUSTER ADDSLOTS <slot> [slot ...]

eg: CLUSTER ADDSLOTS 0 1 2 3 4 ... 5000

记录节点的槽指派信息

struct clusterNode {
  ...;
  unsigned char slots[16384/8];  // 二进制数组
  int           numslots;  // 记录该节点负责处理的槽的数量
  ...; 
};

数组中二进制位1时,表示该节点负责该槽,否则表示该节点不负责槽i

传播节点的槽指派信息

一个节点除了会将自己负责的槽记录在clusterNode结构的slots属性和numslots属性之外,还会将自己的slots数组通过消息发送给集群中的其他节点,以此来告知其他节点自己目前负责哪些槽

记录集群所有槽的指派信息

typedef struct clusterState {
  ...;
  clusterNode *slots[16384];
  ...;
} clusterState;

solts数组包含16384个项,每个数组项都是一个指向clusterNode结构指针。注意如果slots[i]为NULL,表示这个槽没有指派给任何节点;

虽然clusterState.slots数组记录了集群中所有槽指派信息,但使用clusterNode结构的slots数组记录单个节点的槽指派信息仍然是有必要的

CLUSTER ADDSLOTS命令的实现

需要同时设置clusterState.slots数组以及该节点的clusterNode.slots数组二进制

先检查输入槽是否被指派,否则就向客户端返回错误,并终止命令执行

在集群中执行命令

当数据库所有的槽(16384)都指派之后,集群进入上线状态

这时候,当客户端向节点发送与数据库键有关的命令时,接收命令的节点会计算出命令演出里的数据库键属于哪个槽,并检查这个槽是否指派给了自己:

  • 如果键所在的槽正好就指派了当前节点,那么节点直接执行这个命令
  • 如果键所在的槽并没有指派给当前节点,那么节点会向客户端返回一个MOVED错误,指引客户端转向至正确的节点,并再次发送之前想要执行的命令

那么怎么计算某个键属于哪个槽、怎么判断某个槽是否由自己负责,以及MOVED错误的实现方法

计算键属于哪个槽

def slot_number(key):
  return CRC16(key) & 16383

其中CRC16(key)用于计算键key的CRC-16校验和,而 ”& 16383“则相当于“ % 16384”;

这里对于N为2的次幂,那么:
x % n = x & (n - 1)

CLUSTER KEYSLOT命令是查看键属于哪个槽:

判断槽是否由当前节点负责处理

当节点计算出键所属的槽 i 之后,节点会检查自己在clusterState.slots数组中的项 i,判断键所在的槽是否由自己负责:

  • 若clusterState.slots[i] == clusterState.myself,说明槽 i 由当前节点负责,节点可以执行客户端发送的命令
  • 若clusterState.slots[i] != clusterState.myself,说明槽 i不 由当前节点负责,会根据clusterState.slots[i]指向的clusterNode结构所记录的节点IP和端口号,向客户端返回MOVED错误,指引客户端转向至正在处理槽 i 的节点

MOVED错误

MOVED错误的格式为:

MOVED <slot> <ip>:<port>

一个集群客户端通常会与集群中的多个节点创建套接字连接,而所谓的节点转向实际上就是还一个套接字来发送命令
如果客户端尚未与想要转向的节点创建套接字连接,那么客户端会先根据MOVED错误提供的IP地址与端口号来连接节点,然后进行转向

集群模式下的redis-cli客户端在接收到MOVED错误时,并不会打印MOVED错误,而是根据MOVED错误自动进行转向

但是如果是单机模式下的redis-cli,MOVED错误会被客户端打印出来

节点数据库的实现

集群节点保存键值对以及键值对过期时间方式同单机服务器完全相同;

在数据库方面的一个区别是,集群节点只能使用0号数据库,而单机服务没有这个限制。

另外,除了将键值对保存在数据库之外,节点还会通过跳跃表保存槽与键之间的关系:

typedef struct clusterState {
  ...;
  zskiplist *slots_to_keys; // 每个节点的分值是一个槽号,每个节点的成员是一个数据库键
  ...;
} clusterState;

当往数据库中添加或删除键值对时,会更新这个跳跃表

有了这个跳跃表节点就可以很方便的对某个或某些槽的所有数据库键进行批量操作

重新分片

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容