首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Redis内核中的集群模式数据结构和原理分析

Redis内核中的集群模式数据结构和原理分析

原创
作者头像
Ryan_小鱼
发布2025-07-14 16:16:10
发布2025-07-14 16:16:10
1400
举报

Redis内核中的集群模式数据结构分析

在集群模式下,每个Redis节点都会在内存中去维护clusterState这样的核心数据结构。用于记录当前节点视角下Redis集群的整体情况,主要的组成结构如下:

=> myself : 指针指向节点本身,在redis内存中会去维护clusterNode数据结构(结构体),clusterNode会存储节点的相关信息,比如IP,端口,flags角色等

=> state : 这个就是表示当前集群模式下的状态。如REDIS_CLUSTER_OK表示正常运行,REDIS_CLUSTER_FAIL表示故障

=> currentEpoch : 集群版本号,用于故障转移时仲裁新主节点(值最大的节点优先当选)。故障转移或节点变更时递增,解决集群分裂场景的冲突‌

=> nodes:指针指向nodes字典,包含集群模式下的所有节点信息。key为节点ID,value为指向clusterNode结构的指针‌

Redis节点之间的三次握手原理分析

Redis三次握手建立连接和TCP三次握手建立连接不是一回事,Redis自己的meet协议下的三次握手是在应用层。而TCP三次握手是基于底层网络通信建立的连接

比如集群模式下有A,B,C三个节点,当A节点感知到B节点的存在之后,A就会在内存中为B节点建立B的ClusterNode对象,并给B发送一个MEET消息

B接收到A发送给它的MEET消息后,B就也能感知到A节点的存在,并在B节点的内存中为A节点创建一个ClusterNode,并发送一个PONG消息给A节点,告诉A,我已经接收到你的MEET消息了

A节点收到B节点发送给他的PONG消息后,会再次给B节点发送一个PING消息,告诉B节点我已经接收到你发给我的PONG消息,基于MEET协议的三次握手就此完成

接着A节点会再次给C节点三次握手建立连接,三次握手完成后,此时B和C还没有进行三次握手,他们是还没有进行连接,但是此时并没有关系,因为 redis Server通过Gossip 协议(流言协议),就是把信息像流言一样,传播到集群中他能感知到的其他的所有的节点,也就是可以将刚刚建立的node信息传播到其他的节点。这样的话B节点自然也就能感知到C节点的存在,也会发起三次握手和C节点建立连接

基于slots槽位机制的数据分片原理分析

建立Redis集群后,假如有100GB的数据要写入到Redis中,集群模式下有5个节点,那么每个节点就要写入20GB的数据。这个就叫分布式数据存储

问题是你知道写入一条数据的时候,它底层是怎么把这条数据具体写入到哪个节点的?也就是数据要写入到哪个数据分片(data sharding)。还有一个问题就是数据在redis各个节点之间要怎么迁移(reblance)

这里就涉及到扩容缩容的概念,比如说你当前3个节点你觉得不够用,那你想要多增加两个节点,刚增加的这两个节点没有数据,显然不合理,所以这里就涉及到要把其他有数据的节点上的数据迁移到新加入的节点,也就是让已有节点上的数据变少,新加入节点的数据变多,这个就是扩容

至于缩容,就是好比你公司想要节约成本,就让两个节点下线,五个节点变成三个节点,就要把下线的两个节点数据迁移到剩下的三个节点上面,这也是一个数据迁移的过程

每个Redis节点都有N多个数据分片,每次写入数据的时候,就会按照一定的路由算法,把一条数据写入到一个节点中的一个数据分片。做扩容加机器的时候,就按照一定的算法把指定的数据分片迁移到新节点上面就可以了。做缩容的时候,就按照一定的算法把下线节点的数据分片迁移到剩下的节点就可以了

每个Redis节点的数据分片数量是固定的,Redis 集群将数据划分为 ‌16384 个固定槽位slots,给集群下的每个节点分配一部分槽位,每个槽位就是一个数据分片

Redis集群slots分配与内核数据结构

redis集群,会通过slots命令完成对每个节点的槽位分配,但是每个版本的slots命令都是不一样的。每个节点内存维护的ClusterNode结构体,还会维护slots槽位集合和numslots槽位数量,记录节点分配到的槽位信息

每个节点维护的clusterState数据结构中,会维护一个slots,它是一个指针数组,每个元素对应一个槽位,数组下标表示槽位编号,元素值为 clusterNode* 类型指针,指向负责该槽位的‌主节点实例‌,表示当前槽位分配到哪个Redis节点了

Redis集群模式下的三次握手,建立连接的时候,会把自己分配到的槽位信息同步给其他节点。这样A在内存中为B建立clusterNode的时候,就可以在clusterNode中设置对应的slots和numslots,还会在自己clusterState维护的slots表,把对应的槽位指向具体的clusterNode

集群模式下只有当所有的节点都分配到slots槽位后,这个集群状态才会是REDIS_CLUSTER_OK,否则就是REDIS_CLUSTER_FAIL

基于slots槽位的命令执行流程分析

cluster集群下,所有节点基于Meet协议三次握手+Gossip流言协议完成所有节点的连接,只要有一个节点主动发起连接通过Gossip协议就可以让其他节点之间互相三次握手完成连接

在每个节点底层维护的clusterNode中,lots是一个二进制码表,索引值就是slots槽位编号,对应的元素值就是二进制值,表示状态,如果槽位编号不是分配给我当前节点自己的,状态值为1,否则状态值为0

程序员通过Jedis或者RedisTemplate等redis-client发送一条命令往redis写入数据的时候,这个命令是随机发送到Redis集群中的某一个节点,然后redis-server会通过CRC算法对key算出一个值,再把这个值跟16384做一个二进制运算,得到的结果就是这条数据要写入的槽位slot。

接着就把这个槽位编号到自己的clusterstate维护的那个slots槽位表找到该槽位指向的clusterNode,如果这个clusterNode是myself,就说明这条数据是要写入到自己本身的。就会基于前面那个redis网络通信流程把数据写入到redis-server

如果找到的clusternode不是myself,就会给客户端发送一个MOVED,把slot+目标地址返回给客户端,客户端再基于目标地址把命令重定向发送给目标Redis-server,redis-server就会根据slot到slots表查找clusterNode,确定该槽位指定的clusternode是自己本身,就确定是由我自己来写入这条数据,执行写入的操作

基于跳跃表的slots和key关联关系

这里需要有跳跃表数据结构的基础

跳跃表skipList是Redis的sorted Set底层实现的数据结构。在跳跃表中,数据被存储在节点中,而且每个节点都会有一组指向其他节点的指针,我们也叫多级索引结构。跳跃表是一种扩展的有序链表。每个节点都会维护一个score分值,节点按照score分值大小从小到大排序

在跳跃表中,每个节点包含一个数据元素和一组指向其他节点的指针。这些指针分布在不同的层级,每个层级的指针数量都比下一层级少。最底层(第0层)包含所有的元素,而最高层则只包含少数几个元素。这样,查找操作可以在高层级开始,快速跳过那些不需要的元素。

在简单的有序列表中,要访问节点节点3需要经过节点1、2、3共3个节点;要访问节点9需要经过节点1、2、3…8、9共9个节点

在跳跃表中,要访问节点节点3需要经过节点1、3共2个节点(通过L1索引);要访问节点9需要经过节点1、7、9共3个节点(通过L3索引)。可以看到查找的复杂度得到极大提升。

跳跃表的查找、插入和删除操作

查找操作

从最高层索引的头节点开始,如果当前节点的下一个节点的值小于要查找的值,则向右移动。如果当前节点的下一个节点的值大于要查找的值,则向下移动

插入操作

首先进行查找操作,找到插入位置。然后随机生成一个层数,根据这个层数在每一层插入新节点

删除操作

首先进行查找操作,找到要删除的节点。然后在每一层删除这个节点。

这里要引入一个层的概念

跳跃表节点的 level 数组可以包含多个元素, 每个元素都包含一个指向其他节点的指针, 程序可以通过这些层来加快访问其他节点的速度, 一般来说, 层的数量越多, 访问其他节点的速度就越快.

每次创建一个新跳跃表节点的时候, 程序都根据幂次定律 (power law,越大的数出现的概率越小) 随机生成一个介于 1 和 32 之间的值作为 level 数组的大小, 这个大小就是层的 “高度”

关于跳跃表的其他核心概念

前进指针

每个层都有一个指向表尾方向的前进指针 (leveli.forward 属性), 用于从表头向表尾方向访问节点.

跨度

层的跨度(leveli.span 属性)用于记录两个节点之间的距离:

  1. 两个节点之间的跨度越大, 它们相距得就越远.
  2. 指向 NULL 的所有前进指针的跨度都为 0 , 因为它们没有连向任何节点.

跨度用来计算排位 (rank) : 在查找某个节点的过程中, 将沿途访问过的所有层的跨度累计起来, 得到的结果就是目标节点在跳跃表中的排位.

后退指针

节点的后退指针 (backward) 用于从表尾向表头方向访问节点, 它每次只能后退至前一个节点.

分值和成员

节点的分值 (score) 是一个 double 类型的浮点数, 跳跃表中的所有节点都按分值从小到大排序.

节点的成员对象 (obj) 是一个指针, 它指向一个 字符串对象, 而字符串对象则保存着一个 SDS 值.

在同一个跳跃表中, 各个节点保存的 成员对象必须是唯一的,但是分值却可以是相同的 : 分值相同的节点将按照成员对象在字典序中的大小来进行排序, 成员对象较小的节点会排在前面(靠近表头的方向), 而成员对象较大的节点则会排在后面(靠近表尾的方向).

跳跃表的优缺点小结

优点

  • 跳跃表的查找、插入和删除操作的时间复杂度都是O(log N),在处理大量数据时具有很高的效率。
  • 跳跃表的实现相对简单,比如相比于平衡树,跳跃表不需要进行复杂的旋转和平衡操作。
  • 跳跃表支持有序操作,如获取最小值、最大值或进行范围查找。

缺点:

  • 跳跃表的空间复杂度是O(N),每个元素都需要存储在跳跃表中,这可能会占用较多的内存。
  • 跳跃表的性能依赖于随机数生成器,如果随机数生成器的质量不高,可能会影响跳跃表的性能。

接着就说说Redis集群模式下是怎么通过SkipList来来关联slot和key

这里的score的值其实就是key对应的slot槽位的编号,通过这种方式把key和slot槽位关联起来。通过skipList跳跃表维护,可以实现高效查询key对应的slot槽位

集群扩容时的slots转移过程与ASK分析

如果有新的节点加入进来的时候,可以通过一个命令,指定把某个节点上的部分slots迁移到新节点上。

=> 在对源节点执行CLUSTER SETSLOT <slot> MIGRATING <目标节点ID>。该命令将指定槽位(slot)标记为“迁出状态”。在源节点的clusterState中有一个migration_slots_to,指向槽位数组,里面对应的槽位的值指向的clusterNode就是新加入的redis节点的clusterNode

=> 当新节点通过三次握手和其他节点建立连接后,加入到当前集群中,在它对应的clusterState也会维护一个importing_slots_from槽位数组,里面每个槽位的值指向源节点的clusterNode

=> slots迁移是分批进行的,也是会源节点的migration_slots_to是正在进行迁移的slots槽位,而不是待迁移的slots槽位

=> 目标节点从import_slots_from获取从源节点迁移过来的槽位编号,通过跳跃表获取到槽位数据

=> 接着就是把源节点的clusterNode的slots二进制码表中迁移的slot编号状态值改为0,修改槽位数量,同时把node字典中迁移的slot编号引用的clusterNode修改为新节点的clusterNode

=> 对其他节点clusterState维护的nodes字典,同样也要把对应的迁移的slot指定的clusterNode改为当前新加入节点的clusterNode

如果客户端发送请求,刚好在源节点计算slot的时候,计算出slot是在当前节点,但是此时源节点在做数据迁移,刚刚好这个槽位也在迁移的范围内,此时源节点会给redisClient返回一个ACK,把目标节点的地址和slot告诉客户端,那么客户端就可以根据这个目标地址重定向发送请求,然后由目标节点负责去处理客户端发送的命令,把数据写入到目标节点中

Redis主从挂载后的内核数据结构分析

做主从同步的时候,从节点内存维护的clusterNode还会有一个slaveof指针,指向master节点的clusterNode,对主节点而言,它自己维护的clusterNode,除了自己本身的一些信息以外,会维护一个numslaves和slaves,numslaves表示从节点的数量,slaves会指向一个数据结构,它会指向一个从节点的指针数组。数组长度是numslaves,元素值是从节点clusterNode的指针

Redis SYNC主从复制原理以及缺陷

Redis2.x以前是通过SYNC来实现主从复制,但是这种会有很多缺陷。

Redis SYNC实现主从同步的流程和原理

=> master会通过bgsave操作生成一个RDB快照文件,把内存中的数据先同步到RDB文件中

=> 把master在RDB生成时间点之后的命令先写入到命令缓冲区中。

=> 把RDB快照文件传输给slave节点。slave会把这个文件加载到内存中,实现数据同步。

=> 同样的master也会把命令缓冲区的命令发送给slave。让slave数据基本上和master一致

后续不断有命令写入到master,还是会有数据和slave不一致,就有一个命令传播机制。如果有人对master发送一个命令,master也会把这个命令发送给slave,让slave和master一起去执行这个命令

如果slave掉线了或者某些原因除了故障,导致需要重启,slave重启之后会给master发送SYNC命令,要求master按照之前数据同步的方式把数据再次同步到slave,这就意味着要重新执行下面几个步骤

=> bgsave生成RDB文件,并通过网络传输到slave

=> 把RDB生成时间点后的命令写入到命令缓冲区,后面再去发送给slave执行

=> slave加载RDB文件到内存,实现数据同步

Redis SYNC实现主从同步的缺陷

最大的问题就是生成RDB文件,你要知道MASTER内存里的数据量是非常大的,少则几G,多则十几G甚至几十G。你要把这么大的数据量从内存写入到磁盘RDB文件,就会有大量的磁盘IO,非常耗时,而且把这么大的一个文件网络传输给slave,很容易把机器带宽打满,把网络资源耗尽

slave接收到RDB文件后,加载到自己的内存,很容易阻塞掉对外的服务和操作。基本上把所有的资源都耗费在加载RDB文件到内存中了

PSYNC的偏移量和复制积压缓冲区分析

新版本Redis会基于PSYNC针对slave断线重连实现增量同步,如果你断开时间较短,只需要把这段时间内有变更的命令发送给slave就可以了,但是如果你断开时间太长了就只能让slave发送SYNC,让master重新全量同步

redis master 每次执行命令传播的时候呢,都会去记录偏移量(就是每次传输数据的字节)。slave在接收到master传输给它的命令的时候也会去记录这样的一个偏移量

master会把一段时间内的命令和对应的数据偏移量都就记录到复制积压缓冲区,就是记录什么命令会导致多少偏移量增加。这个积压缓冲区是有限的,只能去记录一段时间内的数据

后面slave重启之后,会通过 PSYNC 命令将自己的偏移量发送给主节点,主节点就可以拿它自己之前记录的偏移量跟slave记录的偏移量对比。看看差了多少偏移量,然后去复制积压缓冲区找找,看看偏移量差距在复制积压缓冲区查找对应的命令是否存在

如果存在,就可以从复制积压缓冲区获取命令,传输给slave去执行,如果不存在,就说明你落后太多了,就是你下线时间太长了,很多数据都没有同步了,就不能通过psync的方式来实现增量同步了,你就看只能通过SYNC来实现全量同步了

但是这里要注意:从节点记录的偏移量是在内存中,当从节点正常关闭下线的时候,会触发RDB机制,持久化到rdb文件,后续重启后,就会从rdb文件中读取最后一次记录的偏移量,然后发送psync命令给master,请求增量同步。

但是如果说slave是故障下线,或者说是突然宕机导致的下线,是不会触发RDB持久化,那么就会丢失最后一次记录的这个偏移量,那么后续重启就无法把有效的偏移量发送给master,就只能通过全量同步来实现数据恢复

Redis定时PING与疑似下线分析

在集群架构下,如果Master宕机了,其他Slave是怎么感知到master下线的?集群下的每个节点,不管是master还是slave,都会定时的给其他节点发送一个ping消息,尝试探测其他节点是否存活

其他节点接收到你发过来的ping消息,也会响应一个pong消息,告诉对方我还活着。如果说有节点在规定的时间内没有pong消息,比如说slave发送ping消息给master,但是master没有在指定的时间内发送pong消息,那么slave会给master标记pfail,表示疑似下线。(就是在slave自己的clusterNode里面维护的slaveof指向的那个clusterNode,把它的flags从MASTER改为MASTER&PFAIL)

Redis内核故障报告数据结构与下线标记

各个节点会把疑似下线的情报发送给其他节点,去跟其他节点进行信息交换。当其他节点接收到别人发给他的疑似下线的报告后,会在它的clusterNode中slaveof指向的那个clusterNode去设置一个fail_reports,这里就汇总了其他节点发给它的下线报告,通过链表把这些下线报告管理起来

每个节点在对某个节点汇总下线报告的时候,如果超过半数以上的master节点发送下线报告后就会把下线节点的flags修改为FAIL,表示当前节点已经下线,同时会把这个状态同步给其他节点,其他节点也会他们各自slaveof指向的那个clusterNode的flags改为FAIL。标记这个节点就算是已经正式下线了

Redis主节点选举算法以及故障转移

当master故障下线后,slave是可以感知到它自己的master已经正式下线了,那么它就会给其他的master发起一个投票请求,请求其他的master给自己投一票,选举自己为新的master节点,如果master还没有给其他slave投票,那么它就会给这个节点投一票

当自己得到了 n 节点/2+1 大多数人的投票,此时就可以把自己选举为一个新的 master。

就会去通知所有的节点,之前下线的 master,他负责的槽位 slots 都归自己管了,所有人都会更新自己内存里的数据结构,之前的其他的 salve,此时就会开始从新的 master 那里去同步数据。如果一轮投票里没人收到 n/2+1 大多数人的投票,此时开启下一轮投票就可以了

后续挂掉的那个master重启上线了,它也要挂载到新选举的那个master,作为它的slave去同步它的数据

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Redis内核中的集群模式数据结构分析
  • Redis节点之间的三次握手原理分析
  • 基于slots槽位机制的数据分片原理分析
  • Redis集群slots分配与内核数据结构
  • 基于slots槽位的命令执行流程分析
  • 基于跳跃表的slots和key关联关系
  • 集群扩容时的slots转移过程与ASK分析
  • Redis主从挂载后的内核数据结构分析
  • Redis SYNC主从复制原理以及缺陷
  • PSYNC的偏移量和复制积压缓冲区分析
  • Redis定时PING与疑似下线分析
  • Redis内核故障报告数据结构与下线标记
  • Redis主节点选举算法以及故障转移
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档