redis--多机数据库的实现

主从结构--保障数据安全,进行分布式下负载均衡

复制

在redis中,用户可通过执行SLAVEOF或设置slaveof选项,
让一个服务器去复制另一个服务器,
称被复制的服务器为主服务器,
对主服务器进行复制的服务器被称为从服务器
- 旧版复制功能的实现
redis的复制功能分为同步和命令传播.
1.同步操作用于将从服务器的数据库状态更新至主服务器当前所处的数据库状态.
2.在主服务器的数据库状态被修改,
导致主从服务器数据库状态不一致时,
让主从服务器数据库再次一致.

- 同步
a.从服务器向主服务器发SYNC
b.主服务器接收SYNC,
执行BGSAVE,
在后台生成一个RDB文件+用缓冲区记录生成过程更改命令
c.主服务器的BGSAVE命令执行完毕时,
主服务器会将BGSAVE命令生成的RDB文件发给从服务器
从服务器接收并载入这个RDB文件,
将自己的数据库状态更新至主服务器执行BGSAVE命令时状态
d.主服务器将记录在缓冲区里的所有写命令发给从服务器
从服务器执行这些命令,
更新自己的数据库状态到与主服务器一致.
- 命令传播
同步后,
主服务器执行了更改数据库的命令,
将命令发给从服务器,
从服务器接收并执行,
使得两者再次一致
- 新版复制功能
a.完整重同步
b.部分重同步
- 部分重同步的实现
a.主服务器的复制偏移量和从服务器的复制偏移量
b.主服务器的复制积压缓冲区
c.服务器的运行ID
- 复制偏移量
a.主服务器每次向从服务器传播N个字节的数据时,
将自己的复制偏移量值+N
b.从服务器每次收到主服务器传来的N个字节的数据时,
将自己的复制偏移量值+N
- 复制积压缓冲区
复制积压缓冲区是由主服务器维护的一个固定长度先进先出队列,
默认大小为1MB.
主服务器的复制积压缓冲区会保存着一部分最近传播的写命令,
且复制积压缓冲区会为队列中的每个字节记录相应的复制偏移量.

当从服务器偏移落在复制积压缓冲区内,
则可执行部分重同步.
否则,还是需完整同步.
- 服务器运行ID
除了复制偏移量和复制积压缓冲区外,
实现部分重同步还需用到服务器运行ID.
从服务器利用自己保存ID,发给主服务器,
主服务验证此ID确实是自己,才需接着执行部分重同步逻辑.
否则,直接完整重同步.

复制的实现

- 设置主服务器的地址和端口
客户端向从服务器发以下命令
127.0.0.1:12345>SLAVEOF 127.0.0.1 6379
从服务器保存指定的主服务器ip和端口
struct redisServer
{
    
    
	...
	char *masterhost;
	int masterport;
	...
};
- 建立套接字连接
从服务器根据命令所设置的IP和端口,
创建连向主服务器的套接字连接
如从服务器创建的套接字能成功连接到主服务器,
则从服务器将为这个套接字关联一个专门用于处理复制工作的文件事件处理器.
- 发送PING命令
a.通过发送PING命令可检查套接字的读写状态是否正常
b.发送PING命令可检查主服务器是否能正常处理命令请求

从服务器在发送PING命令后,将遇到以下三种情况的一种
a.如主服务器向从服务器返回了一个命令回复,
但从服务器超时未收到回复.
此时,从服务器断开现有连接,重新创建连向主服务器的套接字
b.如主服务器向从服务器返回一个错误,
此时,从服务器断开并重新创建连向主服务器的套接字
c.如从服务器读到PONG回复
此时,从服务器可继续执行复制
- 身份验证
- 发送端口信息
从服务器向主服务器发送从服务器的监听端口号
- 同步
从服务器将向主服务器发送PSYNC命令,执行同步
- 命令传播

心跳检测

在命令传播阶段,
从服务器默认会以每秒一次的频率,向主服务器发送命令
REPLCONF ACK <replication_offset>
a.检测主从服务器的网络连接状态
b.辅助实现min-slaves选项
c.检测命令丢失
- 检测主从服务器的网络连接状态
如主服务器超过一秒没收到从服务器发来的REPLCONF ACK命令,
则主服务器就知道主从服务器之间的连接出现问题了.
- 辅助实现min-slaves配置选项
- 检测命令丢失
如果因网络故障,
主服务器传播给从服务器的写命令在半路丢失,
则当从服务器向主服务器发REPLCONF ACK命令时,
主服务器将发觉从服务器当前的复制偏移量少于自己的复制偏移量,
然后主服务器就会根据从服务器提交的复制偏移量,
在复制积压缓冲区里找到从服务器缺少的数据,
将其重发给从服务器.

Sentinel--保障主可随时下线,服务不中断

Sentinel是Redis的高可用性解决方案:
由一个或多个Sentinel实例组成的Sentinel系统
可监视任意多个主服务器,
及这些主服务器属下的所有从服务器,
并在被监视的主服务器进入下线状态时,
自动将下线主服务器属下的某个从服务器升级为新的主服务器,
然后由新的主服务器代替已下线的主服务器继续处理命令请求.

主从体系可以用来加强数据安全,做负载均衡等.
Sentinel是一个额外体系,用来监控主从体系.
主从体系的每个参与者均被Sentinel置于监视下,
主服务器下线到达指定时间,
Sentinel会对主服务器执行故障转移
a.挑选一个从服务器,
将这个从服务器升级为新的主服务器
b.向其余从服务器发新的复制指令,
让其称为新的主服务器的从服务器
c.新上限的服务器自动称为目前主服务器的从服务器
- 启动并初始化Sentinel
启动一个Sentinel可用
$redis-sentinel /path/to/your/sentinel.conf
或
$redis-server /path/to/your/sentinel.conf --sentinel
一个Sentinel启动时,需执行
a.初始化服务器
b.将普通redis服务器使用的代码替换为Sentinel专用代码
c.初始化Sentinel状态
d.根据配置文件,初始化Sentinel的监视主服务器列表
e.创建连向主服务器的网络连接
- 初始化服务器
- 使用Sentinel专用代码
- 初始化Sentinel状态
应用了Sentinel专用代码后,
接下来,
服务器会初始化一个sentinel.c/sentinelState结构
此结构保存了服务器中所有和Sentinel功能有关的状态
struct sentinelState
{
    
    
	...
	uint64_t current_epoch;
	dict *masters;
	int titlt;
	int running_scripts;
	mstime_t tilt_start_time;
	mstime_t previous_time;
	list *scripts_queue;
	...
} sentinel;
- 初始化Sentinel状态的masters属性
a.字典的键是被监视主服务器的名字
b.字典的值是被监视主服务器对应的sentinel.c/sentinelRedisInstance
每个sentinelRedisInstance结构代码一个被Sentinel监视的Redis服务器实例.
typedef struct sentinelRedisInstance
{
    
    
	int flags;
	char *name;
	char *runid;
	uint64_t config_epoch;
	sentinelAddr *addr;
	mstime_t down_after_period;
	int quorum;
	int parallel_syncs;
	mstime_t failover_timeout;
	...
}sentinelRedisInstance;

typedef struct sentinelAddr
{
    
    
	char *ip;
	int port;
}sentinelAddr;
- 创建连向主服务器的网络连接
创建连向被监视主服务器的网络连接
Sentinel将称为主服务器的客户端,可向主服务器发命令,获得回复.
对每个被Sentinel监视的主服务器来说,
Sentinel会创建两个连向主服务器的异步网络连接
a.命令连接
专门用于向主服务器发命令,接收回复
b.订阅连接
订阅主服务器的__sentinel__:hello频道
- 获取主服务器信息
Sentinel默认会以每十秒一次的频率,
通过命令连接向被监视的主服务器发INFO命令,
并通过分析INFO命令的回复来获取主服务器的当前信息

通过分析主服务器返回的INFO命令回复,
Sentinel可获取以下两方面的信息
a.一方面是关于主服务器本身的信息,
包括run_id域记录的服务器运行ID
及role域记录的服务器角色
b.另一方面是关于主服务器属下的所有从服务器的信息
主服务器返回的从服务器信息,
被用于更新主服务器实例结构的slaves字典,
这个字典记录了主服务器属下从服务器的名单
a.字典的键由Sentinel自动设置的从服务器名字,格式为ip:port
b.字典的值是从服务器对应的实例结构 

主服务器实例结构,从服务器实例结构均为sentinelRedisInstance结构

获取从服务器信息

当Sentinel发现主服务器有新的从服务器出现时,
Sentinel除了为此新从服务器创建相应的实例结构外,
还会创建连接到从服务器的命令连接和订阅连接.

创建命令连接后,
Sentinel默认下,
以每十秒一次的频率通过命令连接向从服务器发INFO命令,
并获得回复
根据INFO命令的回复,Sentinel会提取出以下信息:
a.从服务器的运行ID run_id
b.从服务器的角色 role
c.主服务器的IP地址master_host,及主服务器的端口号master_port
d.主从服务器的连接状态
master_link_status
e.从服务器的优先级
slave_priority
f.从服务器的复制偏移量
slave_repl_offset

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

默认下,
Sentinel会以每两秒一次的频率,
通过命令连接向所有被监视的主服务器和从服务器发以下格式命令
PUBLISH __sentinel__:hello "<s_ip>,<s_port>,<s_runid>,<s_epoch>,<m_name>,<m_ip>,<m_port>,<m_epoch>"
向服务器的__sentinel__:hello频道发了一条信息,
信息的内容由多个参数组成:
a.以s_开头的参数记录的是Sentinel本身的信息,
各个参数意义如下
s_ip		Sentinel的IP
s_port  Sentinel的port
s_runid
s_epoch
m_name
m_ip
m_port
m_epoch
如Sentinel正在监视的是主服务器,则m_xxx记录的是主服务器的信息
如Sentinel正在监视的是从服务器,
则这些参数记录的是从服务器正复制的主服务器的信息.

接收来自主服务器和从服务器的频道信息

当Sentinel与一个主服务器或从服务器建立起订阅连接后,
Sentinel会通过订阅连接,
向服务器发以下命令
SUBSCRIBE __sentinel__:hello
Sentinel对__sentinel__:hello频道的订阅会一直持续到Sentinel与服务器的连接断开.
即对每个与Sentinel连接的服务器,
Sentinel既通过命令连接向服务器的__sentinel__:hello频道发信息,
又通过订阅连接从服务器的__sentinel__:hello频道接收信息

任意主体向频道发了信息,
所有订阅了频道的主体皆可接收到消息.

- 更新sentinels字典
Sentinel为主服务器创建的实例结构中的sentinels字典
保存了除Sentinel本身外,
所有同样监视这个主服务器的其他Sentinel资料
a.sentinels字典的键是其中一个Sentinel的名字,格式为ip:port
b.sentinels字典的值是键所对应Sentinel的实例结构,

一个Sentinel接收到其他Sentinel发来的信息时,
目标Sentinel会从信息中分析并提取出以下两方面参数
a.与Sentinel有关的参数:
源Sentinel的IP,端口号,运行ID和配置纪元.
b.与主服务器有关的参数:
源Sentinel正在监视的主服务器的名字,IP,端口号,配置纪元

- 创建连向其他Sentinel的命令连接
当Sentinel通过频道信息发现一个新的Sentinel时,
它不仅会为新Sentinel在sentinels字典中创建相应的实例结构,
还会创建一个连向新Sentinel的命令连接
新Sentinel也创建连向此Sentinel的命令连接

检测主观下线状态

默认下,
Sentinel会以每秒一次的频率向所有与它创建了命令连接的实例
[包括主服务器,从服务器,其他Sentinel在内]发PING命令,
并通过实例返回的PING命令回复来判断实例是否在线

检查客观下线状态

当Sentinel将一个主服务器判断为主观下线后,
为确认这个服务器是否真的下线,
它会向同样监视这一主服务器的其他Sentinel进行询问,
当Sentinel从其他Sentinel那里接收到足够数量的已下线判断后,
Sentinel就将从服务器判定为客观下线.
并对主服务器执行故障转移操作.
- 发送SENTINEL is-master-down-by-addr命令
Sentinel使用
SENTINEL is-master-down-by-addr <ip> <port> <current_epoch> <runid>
命令询问其他Sentinel是否同意主服务器已下线
ip 				被Sentinel判断为主观下线的主服务器的IP
port			被Sentinel判断为主观下线的主服务器的端口号
current_epoch	Sentinel当前的配置纪元,用于选举领头Sentinel
runid			可以是*符号或Sentinel的运行ID
				*符号代表命令仅用于检测主服务器的客观下线状态,
				而Sentinel的运行ID则用于选举领头Sentinel
- 接收SENTINEL is-master-down-by-addr命令
当一个Sentinel[目标Sentinel]接收到另一个Sentinel[源Sentinel]发来的
SENTINEL is-master-down-by命令时,
目标Sentinel会分析并取出命令请求中包含的各个参数,
并根据其中的主服务器IP和端口号,
检查主服务器是否已下线
然后向源Sentinel返回一条含三个参数的Multi Bulk回复
a.<down_state>
目标Sentinel对主服务器的下线判断
b.<leader_runid>
*
目标Sentinel的局部领头Sentinel的运行ID
c.<leader_epoch>
目标Sentinel的局部领头Sentinel的配置纪元
- 接收SENTINEL is-master-down-by-addr命令回复
当一定数量的SENTINEL均认为主服务器已主观下线后,
源Sentinel将此主服务器对应的sentinelRedisInstance标记为客观下线.

选举领头Sentinel

当一个主服务器被判断为客观下线时,
监视此下线主服务器的各个Sentinel会协商,
选出一个领头Sentinel
由领头Sentinel对下线主服务器执行故障转移操作.

选举领头Sentinel的规则和方法
a.所有在线的Sentinel都有被选为领头Sentinel的资格
b.每次进行领头Sentinel选举后,
不论选举是否成功,
所有Sentinel的配置纪元的值都会自增一次.
c.在一个配置纪元里,
所有Sentinel都有一次将某个Sentinel设为局部领头Sentinel的机会,
且局部领头一旦设置,
在这个配置纪元里就不能再更改.
d.每个发现主服务器进入客观下线的Sentinel会要求其他Sentinel
将自己设为局部领头Sentinel
e.一个Sentinel向另一个Sentinel发
SENTINEL is-master-down-by-addr命令,
且命令中的runid参数不是*符号而是源Sentinel的运行ID时,
表示源Sentinel要求目标Sentinel将前者设置为局部领头Sentinel
f.Sentinel设置局部领头Sentinel规则是先到先处理.
之后,不再设置
g.目标Sentinel在接收到SENTINEL is-master-down-by-addr命令后,
将向源Sentinel返回一条命令回复,
回复中的leader_runid和leader_epoch记录了
目标Sentinel的局部领头Sentinel的运行ID和配置纪元
h.源Sentinel在收到目标Sentinel返回的命令回复后,
会检查回复中leader_epoch和自己的是否相同.
如相同,取出回复中leader_runid,
如其和源Sentinel的运行ID一致,
则表示目标Sentinel将源Sentinel设为了局部领头Sentinel
i.如某个Sentinel被半数以上的Sentinel设为了局部领头Sentinel
则此Sentinel称为领头Sentinel
j.一个配置纪元里,
只会出现一个领头Sentinel
k.如在给定时限内,
没有一个Sentinel被选举为领头Sentinel
则各个Sentinel一段时间后,
再次选举,
直到选出领头Sentinel

故障转移

选举出领头Sentinel后,
领头Sentinel将对已下线的主服务器执行故障转移操作,
该操作含以下三个步骤
a.在已下线主服务器从服务器中,
选出一个从服务器
将其转为主服务器
b.让已下线主服务器属下的所有从服务器改为复制新的主服务器
c.将已下线主服务器设为新的主服务器的从服务器,
当此旧的主服务器重新上限时,
它会成为新的主服务器的从服务器
- 选出新的主服务器
向其发SLAVEOF no one让其变为主服务器
在发送SLAVEOF on one命令后,
领头Sentinel会以每秒一次的频率,
向被升级的从服务器发INFO命令,
并观察命令回复中的角色信息,
当被升级服务器的role从原来的slave变为master时,
领头Sentinel就知道被选中的从服务器已经升为主服务器了.
- 修改从服务器的复制目标
领头Sentinel向已下线主服务器的其余从服务器发SLAVEOF,
让其称为新的主的从,并进行主从同步.

集群--对数据进行划分,达到分布式的负载均衡[集群中又包含的主从,Sentinel.三者结合构成了分布式&高性能&高可用的NoSQL–Redis]

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

节点

一个Redis集群常由多个节点组成
刚开始时候,
每个节点相互独立
都处于一个只包含自己的集群中

真正的集群需由多个节点构成
连接节点:
CLUSTER MEET <ip> <port>
向一个节点node发CLUSTER MEET
可让node与ip和port所指定的节点进行握手
握手成功时,
node节点会将ip和port所指定节点添加到node节点当前所在的集群中
- 启动节点
一个节点是一个运行在集群模式下的Redis服务器
Redis服务器在启动时会根据cluster-enabled配置选项
是否为yes来决定是否开启服务器的集群模式.
- 集群数据结构
clusterNode结构保存了一个节点的当前状态
struct clusterNode
{
    
    
	// 创建节点的时间
	mstime_t ctime;
	// 节点的名字
	char name[REDIS_CLUSTER_NAMELEN];
	// 节点标识
	int flags;
	// 节点当前的配置纪元
	uint64_t configEpoch;
	// 节点的IP地址
	char ip[REDIS_IP_STR_LEN];
	// 节点的端口号
	int port;
	// 保存连接节点所需的有关信息
	clusterLink *link;
	...
};

typedef struct clusterLink
{
    
    
	// 连接的创建时间
	mstime_t ctime;
	int fd;
	sds sndbuf;
	sds rcvbuf;
	// 与这连接相关联节点
	struct clusterNode *node;
}clusterLink;

typedef struct clusterState
{
    
    
	// 指向当前节点的指针
	clusterNode *myself;
	// 集群当前的配置纪元
	uint64_t currentEpoch;
	// 集群状态
	int state;
	// 集群中至少处理着一个槽的节点的数量
	int size;
	// 节点字典[节点名--节点结构]
	dict *nodes;
	...
}clusterState;
- CLUSTER MEET命令的实现
通过向节点A发送CLUSTER MEET命令,
可让接收命令的节点A将另一个节点B添加到节点A当前所在的集群里面
CUSTER MEET <ip> <port>
a.节点A会为节点B创建一个clusterNode结构,
将此结构添加到自己的clusterState.nodes字典里面
b.节点A根据CLUSTER MEET命令给定的IP和端口号,
向节点B发一条MEET
c.节点B收到MEET,为节点A建一个clusterNode结构,
并将该结构添加到自己的clusterState.nodes里
d.节点B向节点A返回一条PONG消息
e.节点A成功收到PONG,向节点B返回一条PING消息
f.节点B成功收到PING,握手完成.

之后节点A会将节点B的信息通过Gossip协议传播给集群中的其他节点,
让其他节点也与节点B进行握手.
一段时间后,节点B会被集群中的所有节点认识.

槽指派

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

当数据库中的16384个槽都有节点在处理时,
[每个槽都被指派给了集群中的某个节点]
集群处于上线状态.
如数据库中有任何一个槽没得到处理,
则集群处于下线状态.

通过向节点发CLUSTER ADDSLOTS命令,
可将一个或多个槽指派给节点负责
CLUSTER ADDSLOTS <slot> [slot...]	
- 记录节点的槽指派信息
struct clusterNode
{
    
    
	...
	// 16384个二进制位
	unsigned char slots[16384/8];
	// 节点负责处理的槽的数量,即slots中值为1的二进制位数量
	int numslots;
	...
};
- 传播节点的槽指派信息
一个节点除了会将自己负责处理的槽记录在clusterNode结构的slots,numslots.
还会将自己的slots数组通过消息发给集群中其他节点.
收到消息的集群中节点,
在自己的clusterState.nodes字典查找发来节点的clusterNode,
对结构中slots数组进行保存或更新.

- 记录集群所有槽的指派信息
typedef struct clusterState
{
    
    
	...
	clusterNode *slots[16384];
	...
}clusterState;
slots数组含16384个项
a.如slots[i]指针指向NULL,则表示槽i尚未指派给任何节点
b.如slots[i]指针指向一个clusterNode结构,
则表示槽i已经指派给了clusterNode结构所代表的节点

- CLUSTER ADDSLOTS命令的实现
命令接收一个或多个槽作为参数,
将所有输入的槽指派给接收该命令的节点负责
CLUSTER ADDSLOTS <slot> [slot ...]

收到消息节点更改其clusterState下slots和myself指向clusterNode的slots
来反映此变化.

在CLUSTER ADDSLOTS执行完毕后,
节点会通过发送消息告知集群中的其他节点

在集群中执行命令

对数据库中的16384个槽都进行了指派后,
集群会进入上线状态,
此时客户端就可向集群中的节点发送数据命令了.

当客户端向节点发送与数据库键有关的命令时,
接收命令的节点会计算出命令要处理的数据库键属于那个槽,
并检查此槽是否指派给了自己
a.如键所在的槽指派给了自己,直接执行命令
b.如键所在的槽没指派给当前节点,
则节点会向客户端返回一个MOVED错误,
指引客户端转向至正确的节点,
并再次发送之前想执行的命令

- 计算键属于哪个槽
def slot_number(key)
	return CRC16(key) & 16383
CRC16(key)用于计算键key的CRC-16校验和,
而& 16383语句则用于计算出一个介于0至16383之间的整数作为键key的槽号.
- 判断槽是否由当前节点负责处理
当节点计算出键所属的槽i之后,
节点会检查自己在clusterState.slots数组中的项i,
判断键所在的槽是否由自己负责
a.如clusterState.slots[i]等于clusterState.myself
则说明槽i由当前节点负责,节点直接执行命令
b.如clusterState.slots[i]不等于clusterState.myself,
根据clusterState.slots[i]指向的clusterNode记录的IP和端口号,
向客户端返回MOVED错误

- MOVED错误
节点发现键所在的槽并非由自己负责处理时,
节点向客户端返回一个MOVED错误
MOVED <slot> <ip>:<port>
客户端收到节点返回的MOVED错误时,
客户端会根据MOVED错误中提供的IP和端口号,
转向至负责处理槽slot的节点,
并向该节点重新发之前想执行的命令

一个集群客户端通常会与集群中的多个节点创建套接字连接,
所谓的节点转向实际是换一个套接字来发送命令.

- 节点数据库的实现
节点和单机服务器在数据库方面的一个区别是,
节点只能用0号数据库,
而单机Redis服务器没这一限制

除了将键值对保存在数据库里面外,
节点还会用clusterState结构的slots_to_keys跳跃表
来保存槽和键之间的关系.
typedef struct clusterState
{
    
    
	...
	zskiplist *slots_to_keys;
	...
}clusterState;
slots_to_keys跳跃表每个节点的分值是一个槽号
每个节点的成员是一个数据库键
a.当节点往数据库添加一个新的键值对时,
节点会将这个键及键的槽号关联到slots_to_keys跳跃表
b.节点删除数据库中的某个键值对时,
节点会在slots_to_keys跳跃表解除被删除键与槽号的关联

重新分片

Redis集群的重新分片操作
可将任意数量已经指派给某个节点的槽
改为指派给另一个节点
且相关槽所属的键值对也会从源节点被移动到目标节点.

重新分片可在线进行,
重新分片过程集群不需下线,
且源节点和目标节点可继续处理命令请求.
	
- 重新分配实现原理
Redis集群的重新分片操作由Redis的集群管理软件redis-trib
负责执行.
redis-tirb对集群的单个槽slot进行重新分配的步骤如下:
a.redis-trib对目标节点发
CLUSTER SETSLOT <slot> IMPORTING <source_id>
让目标节点准备好从源节点导入属于槽slot的键值对
b.redis-trib对源节点发
CLUSTER SETSLOT <slot> MIGRATING <targer_id>
命令,让源节点准备好将属于槽slot的键值对迁移至目标节点
c.redis-trib向源节点发
CLUSTER GETKEYSINSLOT <slot> <count>
获得最多count个属于槽slot的键值对的键名
d.对获得的每个键名
redis-trib向源发一个
MIGRATE <target_ip> <target_port> <key_name> 0 <timeout>
命令,
将被选中的键原子地从源节点迁移至目标节点.
e.重复3,4
f.redis-trib向集群中的任意一个节点发CLUSTER SETSLOT
<slot> NODE <target_id>命令,
将槽slot指派给目标节点,
这一指派信息会通过消息发至整个集群,
集群中所有节点会知道slot已经指派给了目标节点

ASK错误

在进行重新分片期间,
源节点向目标节点迁移一个槽过程中,
属于被迁移槽的一部分键保存在源节点里,
另一部分键值对保存在目标节点里面.

客户端向源节点发送一个与数据库键有关的命令,
且键落在被迁移的槽,
若键尚未迁移过去,直接执行,
若键已经迁移过去,返回ASK,指引客户端转向目标节点,向其发命令
- CLUSTER SETSLOT IMPORTING
typedef struct clusterState
{
    
    
	...
	// 若importing_slots_from[i]不为NULL
	// 则表示当前节点在从importing_slots_from[i]代表的结点
	// 导入槽i
	// CLUSTER SETSLOT <i> IMPORTING <source_id>
	clusterNode *importing_slots_from[16384];
	...
}clusterState;
- CLUSTER SETSLOT MIGRATING
typedef struct clusterState
{
    
    
	...
	// 当前节点正迁移至其他节点的槽
	// migrating_slots_to[i]不是NULL
	// 表示当前节点正将槽i迁移至migrating_slots_to[i]
	// 代表的节点
	// CLUSTER SETSLOT <i> MIGRATING <target-id>
	clusterNode *migrating_slots_to[16384];
};
- ASK错误
如节点收到一个关于键key的命令请求,
且键key所属的槽i正好指派给了此节点
则节点会尝试在自己的数据库里找键key,找到则直接执行.
找不到,检查槽i是否在迁移.
若是,发回ASK错误.
- ASKING
打开发送该命令的客户端的REDIS_ASKING标识.
如节点正在导入槽i,
且向节点发送命令的客户端带有REDIS_ASKING标识,
节点将破例执行这个关于槽i的命令一次.

复制与故障转移

Redis集群中的节点分为主节点和从节点,
主节点用于处理槽,
从节点用于复制某个主节点,
并在被复制的主节点下线时,
代替下线主节点继续处理命令请求.
- 设置从节点
CLUSTER REPLICATE <node_id>
可让接收命令的节点成为node_id所指定节点的从节点,
并开始对主节点进行复制.
struct clusterNode
{
    
    
	...
	// 如果这是一个从节点,则指向主节点
	struct clusterNode *slaveof;
	...
};
一个节点成为从节点,
并开始复制某个主节点这一信息会通过消息发给集群中的其他节点,
最终集群中的所有节点都会知道某个从节点正在复制某个主节点.
struct clusterNode
{
    
    
	...
	// 正复制此主节点的从节点数量
	int numslaves;
	// 数组,项为指向从节点对应的clusterNode对象的指针
	struct clusterNode **slaves;
};
- 故障检测
集群中的每个节点会定期向集群中其他节点发PING消息,
来检测对方是否在线.
如收到PING消息的节点,没在规定时间内发回PONG,
则发送PING消息的节点会将其标记为疑似下线.
在节点维护的目标节点的clusterNode的flags
中打开REDIS_NODE_PFAIL,即可.

集群中的各个节点会通过互相发消息,
来交换集群中各个节点的状态信息.
一个主节点A得知主节点B认为主节点C进入疑似下线时,
主节点A会在自己的clusterState.nodes找到主节点C对应的clusterNode
将主节点B的下线报告,添加到clusterNode的fail_reports链表里
struct clusterNode
{
    
    
	...
	list *fail_reports;
	...
};
每个下线报告由一个clusterNodeFailReport结构表示
struct clusterNodeFailReport
{
    
    
	struct clusterNode *nodes;
	mstime_t time;
};
如一个集群中,
半数以上负责处理槽的主节点都将某个主节点x报告为疑似下线,
则此主节点x将被标记为已下线.
将主节点x标记为已下线的节点
会向集群广播一条关于主节点x的FAIL消息.
所有收到此消息的节点会立即将主节点x标记为已下线.

- 故障转移
当一个从节点发现自己正在复制的主节点进入了已下线状态时,
从节点将开始对下线主节点进行故障转移
a.选出一个从节点
b.被选中节点执行SLAVEOF no one,成为新的主节点
c.新的主节点会撤销所有对已下线主节点的槽指派,
将这些槽指派给自己.
d.新的主节点向集群广播一条PONG消息
让集群中其他节点立即知道这个节点已经由从节点变为主节点,
且此主节点已经接管了老主节点的槽
e.新的主节点开始接收和负责槽的请求,转移完成.

- 选举新的主节点
a.集群的配置纪元,从0依次增加
b.集群的某个节点开始一次故障转移时,集群配置纪元+1
c.对每个配置纪元,集群里每个负责处理槽的主节点有一次投票机会
首个要求主节点投票的从节点获得要求主节点的票
d.从节点发现自己的主节点下线,
向集群广播
CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST
要求所有收到此消息
有投票权的主节点向其投票
e.主节点收到从节点要求投票信息,
且自己尚未投出,
即通过CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK
完成对其投票
f.每个从节点累计获得选票
g.某个从节点获得N/2+1票时,
此从节点当选为新的主节点
h.如一个配置纪元下来,
没产生获得半数以上票的从节点,
重复上述.

消息

集群中节点互发消息
节点发送的消息有五种
a. MEET消息
发送者接到客户端发的CLUSTER MEET时,
发送者会向接收者发MEET,
请求接收者加入到发送者当前所处集群
b. PING消息
集群中每个节点默认每隔一秒会从已知节点列表选五个节点,
对这五个节点中最长时间没发过PING消息的节点发PING,
来检测被选中节点是否在线.
如节点A最后一次收到节点B发的PONG消息时间,
距离当前已超过A的cluster-node-timeout一半,
则节点A也向B发PING消息
c. PONG消息
接收者收到发送者发来的MEET或PING时,
为向发送者确认此MEET或PING已到达
接收者会向发送者返回一条PONG
一个节点也可向集群广播自己的PONG
让集群其他节点立即刷新对此节点认识.
d. FAIL消息
一个主节点A判断另一主节点B已经进入FAIL状态时,
A向集群广播一条关于B的FAIL消息
e. PUBLISH消息
节点收到一个PUBLISH命令时,
节点会执行此命令,
向集群广播一条PUBLISH消息,
所有收到此PUBLISH消息节点会执行相同的PUBLISH命令

- 消息头
节点发的所有消息都由一个消息头包裹
typedef struct
{
    
    
	// 消息长度=消息头长+正文长
	uint32_t totlen;
	// 消息类型
	uint16_t type;
	// 消息正文包含的节点信息数量
	uint16_t count;
	// 发送者配置纪元
	uint64_t currentEpoch;
	// 主节点配置纪元
	uint64_t configEpoch;
	// 发送者名字
	char sender[REDIS_CLUSTER_NAMELEN];
	// 发送者目前的槽指派信息
	unsigned char myslots[REDIS_CLUSTER_SLOTS/8];
	// 隶属的主节点名字
	char slaveof[REDIS_CLUSTER_NAMELEN];
	// 发送者端口号
	uint16_t port;
	// 发送者标识
	uint16_t flags;
	// 发送者所处集群状态
	unsigned char state;
	// 消息正文
	union clusterMsgData data;	
} clusterMsg;

union clusterMsgData
{
    
    
	struct 
	{
    
    
		// 每条MEET/PING/PONG都含两个
		// clusterMsgDataGossip结构
		clusterMsgDataGossip gossip[1];
	}ping;
	
	struct
	{
    
    
		clusterMsgDataFail about;
	}fail;
	
	struct
	{
    
    
		clusterMsgDataPublish msg;	
	}publish;
	
	...
};
- MEET/PING/PONG
union clusterMsgData
{
    
    
	...
	struct
	{
    
    
		// 每条MEET/PING/PONG都含两个
		// clusterMsgDataGossip结构
		clusterMsgDataGossip gossip[1];
	}ping;
	
	...
};
每次发送MEET/PING/PONG消息时,
发送者都从自己的已知节点列表中随机选两个节点,
将此两个被选中节点的信息分别存到两个clusterMsgDataGossip里
typedef struct
{
    
    
	// 节点名字
	char nodename[REDIS_CLUSTER_NAMELEN];
	// 最后一次向其发PING时间戳
	uint32_t ping_sent;
	// 最后一次从其收到PONG时间戳
	uint32_t pong_received;
	// 节点IP
	char ip[16];
	// 节点端口
	uint16_t port;
	// 节点标识
	uint16_t flags;
} clusterMsgDataGossip;
接收者收到MEET/PING/PONG时,
接收者会访问消息正文中两个clusterMsgDataGossip
a.如被选中节点不存在于接收者的已知节点列表
则接收者首次接触到被选中节点,
接收者将与选中节点握手
b.如被选中节点已经存在于接收者的已知节点列表
对其clusterNode进行更新

- FAIL
typedef struct
{
    
    
	char nodename[REDIS_CLUSTER_NAMELEN];
}clusterMsgDataFail;
因为集群里的所有节点都有独一无二的名字,
所以FAIL只需存下线节点名字

- PUBLISH消息实现
当客户端集群中的某个节点发命令
PUBLISH <channel> <message>
时,收到PUBLISH命令的节点不仅会向channel频道发message
还会向集群广播一条PUBLISH
所有收到此PUBLISH的节点会向channel发message

向集群中某个节点发
PUBLISH <channel> <message>
将导致集群中的所有节点都向channel频道发message消息
typedef struct
{
    
    
	uint32_t channel_len;
	uint32_t message_len;
	// 保存了channel和message内容
	unsigned char bulk_data[8];
}clusterMsgDataPublish;

猜你喜欢

转载自blog.csdn.net/x13262608581/article/details/115058423