《Redis设计与实现》(16-21)个人学习总结

Posted 月亮的-影子

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《Redis设计与实现》(16-21)个人学习总结相关的知识,希望对你有一定的参考价值。

注明:《Redis设计与实现》的个人学习总结,这本书对redis的讲解清晰易懂,如果深入学习可以看看这本书

目录

第16章 Sentinel

  • Sentinel是redis高可用性的解决方案。由一个或者多个Sentinel实例组成的Sentinel系统监视多个主服务器,和下面的从服务器。
    • 双环是主服务器
    • 单环就是从服务器
    • sentinel监视这四个服务器。
  • 如果server1下线超时,sentinel就会对server1执行故障转移。
    • 选择一个从服务器升级为主服务器
    • 其它从服务器重新发送复制请求,认定新的主服务器。
    • 监控下线的server1,如果上线就设置为新主服务器的从服务器。

16.1 启动并初始化Sentinel

  • 启动命令
$ redis-sentinel /path/to/your/sentinel.conf
$ redis-server /path/to/your/sentinel.conf --sentinel
  • 启动的步骤
  1. 初始化服务器
  2. 普通redis服务器使用的代码替换成sentinel专用代码
  3. 初始化sentinel状态
  4. 根据配置文件,初始化sentinel监视主服务器列表
  5. 创建连向服务器的网络连接。

16.1.1 初始化服务器

  • sentinel本质就是运行在特殊模式下的redis服务器。
  • 一开始就要初始化普通redis服务器,但是redis普通服务器和sentinel有所不同。所以初始化也不同
  • 载入文件还原数据库这里的方式也是不同的。

16.1.2 使用Sentinel专用代码

  • 把redis服务器使用的代码转换成sentinel的专用代码。他们使用的服务器端口也是不同的。
#define REDIS_SERVERPORT 6379
#define REDIS_SENTINEL_PORT 26379
  • redis普通服务器使用redis.c/redisCommandTable作为命令表
  • Sentinel使用sentinel.c/sentinelcmds作为命令表。而且实现的函数也是有所不同。
  • 所以sentinel模式下无法执行set,dbsize,eval等命令。因为服务器的命令表没有这些。

16.1.3 初始化Sentinel状态

  • 接下来就会初始化sentinel.c/sentinelState结构保存了sentinel相关的状态。
struct sentinelState 
//
当前纪元,用于实现故障转移
uint64_t current_epoch;
//
保存了所有被这个sentinel
监视的主服务器
//
字典的键是主服务器的名字
//
字典的值则是一个指向sentinelRedisInstance
结构的指针
dict *masters;
//
是否进入了TILT
模式?
int tilt;
//
目前正在执行的脚本的数量
int running_scripts;
//
进入TILT
模式的时间
mstime_t tilt_start_time;
//
最后一次执行时间处理器的时间
mstime_t previous_time;
一个FIFO
队列,包含了所有需要执行的用户脚本
list *scripts_queue;
 sentinel;

16.1.4 初始化Sentinel状态的masters属性

  • master字典记录了所有被监视的主服务器相关信息
    • 字典的键是被监视的主服务器名字
    • 值就是sentinel.c/sentinelRedisInstance结构
  • sentinelRedisInstance结构代表一个被监视主服务器的实例,可以是主服务器,也可以是从服务器
  • 部分sentinelRedisInstance结构属性。
typedef struct sentinelRedisInstance 
//
标识值,记录了实例的类型,以及该实例的当前状态
int flags;
//
实例的名字
//
主服务器的名字由用户在配置文件中设置
//
从服务器以及Sentinel
的名字由Sentinel
自动设置
//
格式为ip:port
,例如"127.0.0.1:26379"
char *name;
//
实例的运行ID
char *runid;
//
配置纪元,用于实现故障转移
uint64_t config_epoch;
//
实例的地址
sentinelAddr *addr;
// SENTINEL down-after-milliseconds
选项设定的值
//
实例无响应多少毫秒之后才会被判断为主观下线(subjectively down
)
mstime_t down_after_period;
// SENTINEL monitor <master-name> <IP> <port> <quorum>
选项中的quorum
参数
//
判断这个实例为客观下线(objectively down
)所需的支持投票数量
int quorum;
// SENTINEL parallel-syncs <master-name> <number>
选项的值
//
在执行故障转移操作时,可以同时对新的主服务器进行同步的从服务器数量
int parallel_syncs;
// SENTINEL failover-timeout <master-name> <ms>
选项的值
//
刷新故障迁移状态的最大时限
mstime_t failover_timeout;
// ...
 sentinelRedisInstance;

  • sentinelRedisInstance.addr属性指向sentinel.c/sentinelAddr结构,保存实例的ip地址和端口号。
typedef struct sentinelAddr 
char *ip;
int port;
 sentinelAddr;

  • masters字典的初始化根据被载入的sentinel文件。
  • 下面展示master1的实例结构还有master2的实例结构。
#####################
# master1 configure #
#####################
sentinel monitor master1 127.0.0.1 6379 2
sentinel down-after-milliseconds master1 30000
sentinel parallel-syncs master1 1
sentinel failover-timeout master1 900000
#####################
# master2 configure #
#####################
sentinel monitor master2 127.0.0.1 12345 5
sentinel down-after-milliseconds master2 50000
sentinel parallel-syncs master2 5
sentinel failover-timeout master2 450000

16.1.5 创建连向主服务器的网络连接

  • 最后一步就是向被监视的主服务器进行网络连接。成为主服务器的客户端,发送命令并且获取对应的信息。

  • 但是需要两个连接

    • 命令连接:专门发送命令和接收回复
    • 订阅连接:订阅主服务器的__sentinel__:hello频道
  • 为什么有两个连接?

    • 因为redis订阅和发布功能中,被发送信息不会存于server,为了防止丢失,那么sentinel就需要一个订阅连接来特殊处理这些订阅接收信息。

    • 而且还需要发送命令和接收回复。

    • sentinel和多个实例进行网络连接所以sentinel使用的是异步连接。

16.2 获取主服务器信息

  • 每10s,sentinel都会发送info获取主服务器信息。
  • 获取下面的内容。
    • 主服务器的runid和服务器角色
    • 从服务器的ip和端口号
# Server
...
run_id:7611c59dc3a29aa6fa0609f841bb6a1019008a9c
...
# Replication
role:master
...
slave0:ip=127.0.0.1,port=11111,state=online,offset=43,lag=0
slave1:ip=127.0.0.1,port=22222,state=online,offset=43,lag=0
slave2:ip=127.0.0.1,port=33333,state=online,offset=43,lag=0
...
# Other sections
...

  • 如果发现runid和sentinel保存的那个主服务器实例结构不同那么就要进行更新。

  • 对于从服务器的信息用于更新主服务器实例的结构slaves字典,字典记录从服务器的名单。

    • 键是从服务器名字
    • 值就是对应的从服务器的实例结构。
  • sentinel还会检查从服务器的实例是不是存在于字典里面

    • 存在那么就sentinel就更新这个从服务器的实例
    • 否则就创建一个sentinel实例结构。
  • 下面就是主服务器和所有的从服务器的sentinelRedisInstance实例。保存服务器名字,ip,端口。

    • 主服务器的flags=SRI_MASTER,从服务器是SRI_SLAVE
    • 主服务器的name是sentinel配置文件设置,从服务器就是ip+端口号。

16.3 获取从服务器信息

  • 对于从服务器来说sentinel也会进行命令连接和订阅连接。
  • 每10s发送info获取从服务器的信息更新实例信息。
    • 从服务器的runid
    • 从服务器角色role
    • 主服务器的master_host
    • 主服务器的端口号master_port。
    • 主从服务器的连接状态master_link_status.
    • 从服务器的优先级slave_priority
    • 从服务器的复制偏移
# Server
...
run_id:32be0699dd27b410f7c90dada3a6fab17f97899f
...
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:up
slave_repl_offset:11887
slave_priority:100
# Other sections
...

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

  • 每2s,sentinel通过命令连接发送命令
PUBLISH __sentinel__:hello  信息
  • 命令向服务器的_sentinel__:hello频道发送一条信息
    • s_开头就是sentinel本身的信息
    • m_开头就是主服务器的信息。

  • 发送信息的实例
"127.0.0.1,26379,e955b4c85598ef5b5f055bc7ebfd5e828dbed4fa,0.."
  • sentinel的127.0.0.1端口号为26379,运行id是e955b4c85598ef5b5f055bc7ebfd5e828dbed4fa,纪元为0。
  • 后面就是一个对应的主服务器的相关信息。

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

  • sentinel的订阅连接,发送一个下面的请求给服务器
  • 这个订阅会持续到与服务器连接断开
  • 也就是可以向sentinel:hello频道发送信息,而且也能够接收信息
SUBSCRIBE __sentinel__:hello

  • 监视同一个服务器的多个sentinel,一个sentinel发送的信息,所有其他sentinel都可以共享。可以更新对被监视服务器的认知
  • 当sentinel接收信息,就会根据sentinel的id,端口,运行id进行一个解析
    • 运行id和自己的相同,信息丢弃
    • 不同的话,说明这是其他sentinel发的,可以更新一下sentinel对这个服务器的实例结构更新。

16.5.1 更新sentinels字典

  • Sentinel为主服务器创建的sentinels字典,包括了其它的sentinel的信息
    • 键就是其它sentinel的名字,格式ip:端口号
    • 值就是对应的sentinel实例。
  • sentinel接收信息会从信息提取
    • 与sentinel相关的参数
    • 与主服务器相关的参数。runid之类的。
  • 根据主服务器的参数可以在sentinel的masters字典找到主服务器结构
  • 根据sentinel参数可以在sentinels字典找到sentinel实例结构。
  • 下面sentinel 26379收到3条信息
    • 第一条忽略
    • 第二条发送者是26381,从sentinels字典获取这个sentinel并且更新信息
    • 第三条信息127.0.0.1:26380这个也是要更新
1) "message"
2) "__sentinel__:hello"
3) "127.0.0.1,26379,e955b4c85598ef5b5f055bc7ebfd5e828dbed4fa,0,mymaster,127.0.0.1,6379,0"
1) "message"
2) "__sentinel__:hello"
3) "127.0.0.1,26381,6241bf5cf9bfc8ecd15d6eb6cc3185edfbb24903,0,mymaster,127.0.0.1,6379,0"
1) "message"
2) "__sentinel__:hello"
3) "127.0.0.1,26380,a9b22fb79ae8fad28e4ea77d20398f77f6b89377,0,myma

16.5.2 创建连向其他Sentinel的命令连接

  • sentinel之间也是会建立命令连接的。

16.6 检测主观下线状态

  • Sentinel每秒发送ping验证连接实例是不是在线
  • ping回复
    • 有效回复+PONG、-LOADING、-MASTERDOWN
    • 无效回复+PONG、-LOADING、-MASTERDOWN这三个之外的回复。
  • down-after-milliseconds也就是sentinel配置文件的选项,指定判断实例已经下线的所需时间长度。比如在规定时间实例向sentinel返回无效回复,那么sentinel只能把实例的flags修改为SRI_S_DOWN
  • 而且每个sentinel的down-after-milliseconds都有可能是不同的。所以这个只是sentinel认为的主观下线。

16.7 检查客观下线状态

  • 一个sentinel不足以认为实例下线,所以可以询问其他sentinel,如果他们也这么认为那么就是下线了。就可以对主服务器进行故障转移。

16.7.1 发送SENTINEL is-master-down-by-addr命令

  • 询问其他sentinel是否同意主服务器下线
  • 下面是命令和各个参数。
SENTINEL is-master-down-by-addr
例子,主服务器IP为127.0.0.1,端口号为6379,纪元是0,询问其他sentinel问一下是不是下线了。
SENTINEL is-master-down-by-addr 127.0.0.1 6379 0 *

16.7.2 接收SENTINEL is-master-down-by-addr命令

  • 接收到命令后,立刻提取主服务器参数,并且检查是否已经下线。
  • 然后返回一条三个参数的Multi Bulk回复
1) <down_state>
2) <leader_runid>
3) <leader_epoch>

  • 比如,这个1就说明回复的是主服务器已经下线了。
1) 1
2) *
3) 0

16.7.3 接收SENTINEL is-master-down-by-addr命令的回复

  • 源sentinel统计其它sentinel认为主服务器下线的数量,如果达到客观下线数量,那么就会修改主服务器实例的flags=SRI_O_DOWN,说明现在已经是客观下线状态。
  • 如果其它sentinel认为主服务器下线的数量超过quorum参数那么就是客观下线。
  • 不同的sentinel设置的客观下线参数也是不同的。所以一个sentinel认为客观下线不代表其它的sentinel也符合这样的规则。

16.8 选举领头Sentinel

  • 如果主服务器下线,那么这个时候需要选举出一个sentinel的领头,并且让领头处理故障转移操作。
    • 谁都可能是领头
    • 不管是不是选举成功纪元都+1
    • 领头设置之后,领头的纪元不能被修改
    • 每个发现主服务器进入客观下线的sentinel都会要求其他sentinel将自己设置为领头Sentinel。
    • 源sentinel向目标sentinel发送SENTINEL is-master-down-by-addr,如果runid不是*而是源sentinel的id那么表示源sentinel要求目标sentinel设置后者为局部领头
    • 设置局部领头先到先得,最先向目标发送信息要求的那个源sentinel才会成为这个目标的sentinel的局部领头。
    • 目标sentinel接收命令后回复leader_runid参数和leader_runid参数记录了领头id和纪元
    • 源sentinel收到回复查看纪元leader_epoch是不是和自己的相同,如果是取出leader_runid,如果runid也和sentinel的runid相同,表示说明目标已经设置它为局部领头
    • 如果一个sentinel获得半数其它sentinel支持那么就能成为领头。
    • 如果没有选出那么就会重新再来选,直到成功。

16.9 故障转移

  • 选出领头之后的操作
  1. 挑出一个从服务器转换为主服务器
  2. 已下线的主服务器的从服务器改复制新的主服务器
  3. 已下线的主服务器设置为新主服务器的从服务器。

16.9.1 选出新的主服务器

  • 从状态比较好的从服务器中选出一个新的主服务器。发送 slave of no one
  • 新的主服务器是怎样挑选出来的
  1. 删除列表中下线的从服务器。
  2. 删除最近5s没有恢复sentinel的info命令的从服务器
  3. 删除所有和主服务器断开连接超过down-after-milliseconds*10毫秒的从服务器。这个参数指定了判断主服务器下线所需要的时间,删除断开连接超过down-after-milliseconds * 10能够保证剩余从服务器没有过早和主服务器断开连接,保存的数据比较新。
  4. 然后根据优先级和复制偏移量大小选出最好的。

  • 选出来之后领头sentinel就会每秒一直发送info,直到被升级的从服务器的role已经变了master。那么说明升级成功。

16.9.2 修改从服务器的复制目标

  • 实际上下一步就是修改其他从服务器的复制对象,使用slaveof命令就可以进行处理。

16.9.3 将旧的主服务器变为从服务器

  • 以前的主服务器上线的时候就会设置为新主服务器的从服务器。

16.10 重点回顾

  • Sentinel是特殊的redis服务器,和普通的服务器不同在于,使用不同的命令表,命令表的函数也是不一样的。
  • Sentinel会创建两个连接一个是命令,一个是订阅,订阅连接主要是处理获取其它Sentinel和服务器的最新信息,命令连接就是为了处理好主服务器发送的命令请求。
  • Sentinel可以通过Info命令获取主服务器的信息,和所有从服务器的信息,并且为这些服务器创建实例结构。它也会和其它从服务器进行一个订阅和命令连接
  • 每10s发送一次info命令,如果发现主服务器处于下线或者是处于故障转移的时候,那么就会改成每秒发送一次,主要是监测主服务器是否真的下线,还有就是故障转移是否成功。
  • 监视同一个服务器的Sentinel可以通过发送消息到这个频道告知自己的存在
  • 也可以接受频道的信息,并且为他们创建实例结构
  • Sentinel之间只会创建命令连接
  • Sentinel每秒向所有服务器和Sentinel发送ping,如果没有在规定时间做出有效的应答,那么就会认为是主观下线,并且询问其他的Sentinel是不是他们也认为该服务器主观下线,要是多个Sentinel超过半数认为是主观下线,那么就会给该服务器标记为客观下线
  • 这个时候就会开始争夺领头,通过向其他Sentinel发送请求,投票自己,成为领头之后就可以进行故障转移。选出新的主服务器,改变从服务器的复制目标,最后就是把以前的主服务器设置为新服务器的从服务器。

第17章 集群

17.1 节点

  • redis集群由多个节点组成。
  • 下面就是集群工作命令
  • 发送命令到指定的node,可以进行握手,握手成功就会加入到本node的集群中
  • 下面就是7000邀请7001来到自己的集群。然后再邀请7002。
CLUSTER MEET ip port

17.1.1 启动节点

  • 一个节点就是运行在集群模式下的redis服务器。会根据cluster-enabled配置选项决定不是开启集群。
  • 节点会使用单机模式的所有服务器组件
    • 节点会使用文件事件处理器完成请求和回复
    • 时间事件处理器完成serverCron函数,serverCron又会调用clusterCron主要处理集群下的常规操作
    • 还是会使用数据库
    • 持久化组件
    • 复制等

17.1.2 集群数据结构

  • clusterNode结构保存节点的状态。
struct clusterNode 
//
创建节点的时间
mstime_t ctime;
//
节点的名字,由40
个十六进制字符组成
//
例如68eef66df23420a5862208ef5b1a7005b806f2ff
char name[REDIS_CLUSTER_NAMELEN];
//
节点标识
//
使用各种不同的标识值记录节点的角色(比如主节点或者从节点),
//
以及节点目前所处的状态(比如在线或者下线)。
int flags;
//
节点当前的配置纪元,用于实现故障转移
uint64_t configEpoch;
//
节点的IP
地址
char ip[REDIS_IP_STR_LEN];
//
节点的端口号
int port;
//
保存连接节点所需的有关信息
clusterLink *link;
// ...
;
  • clusterNode的link对应一个clusterLink,保存套接字描述符,输入输出缓冲区
typedef struct clusterLink 
//
连接的创建时间
mstime_t ctime;
// TCP
套接字描述符
int fd;
//
输出缓冲区,保存着等待发送给其他节点的消息(message
)。
sds sndbuf;
//
输入缓冲区,保存着从其他节点接收到的消息。
sds rcvbuf;
//
与这个连接相关联的节点,如果没有的话就为NULL
struct clusterNode *node;
 clusterLink;

redisClient结构和clusterLink结构不同之处?

  • 都有描述符和输入输出缓冲区

  • redisClient套接字和缓冲区是用于连接客户端的

  • clusterLink的套接字和缓冲区是用于连接节点的。

  • 最后还有一个clusterState用于保存节点的状态。每个节点都有这个东西。

typedef struct clusterState 
//
指向当前节点的指针
clusterNode *myself;
//
集群当前的配置纪元,用于实现故障转移
uint64_t currentEpoch;
//
集群当前的状态:是在线还是下线
int state;
//
集群中至少处理着一个槽的节点的数量
int size;
//
集群节点名单(包括myself
节点)
//
字典的键为节点的名字,字典的值为节点对应的clusterNode
结构
dict *nodes;
// ...
 clusterState;
  • 用这是三个节点为例子。从7000的角度来说记录集群和三个节点的状态(上面的代码)
    • 结构的currentEpoch属性是0,表示集群的纪元就是0。
    • size为0说明没有任何节点处理槽,所以state的值是REDIS_CLUSTER_FAIL,集群处于下线
    • nodes字典保存了2个clusterNode结构,分别保存7001和7002。myself指向了7000
    • 三个节点的clusterNode的flags是REDIS_NODE_MASTER说明都是主节点。
    • 在7001创建的clusterState上面myself就是指向7001

17.1.3 CLUSTER MEET命令的实现

  • 可以向节点A发送cluster meet命令来把节点B加入到自己的集群。

整个过程

  1. A会为B创建一个clusterNode,并且加入到自己的clusterState.nodes字典中
  2. 根据命令A会发送给节点B一条meet命令
  3. B接收,并且B为A创建一个clusterNode添加到cluster.nodes字典中
  4. B返回一条pong给A
  5. A接收到那么就知道B已经成功接收到meet
  6. A向B发送一条ping
  7. 如果B返回pong说明连接成功。

相当于就是发送命令给A去加入B,A发送meet,B回应,A发送ping,B回应那么就握手成功。

  • 最后通过Goosip协议传播给集群的其它节点。

17.2 槽指派

  • 集群通过分片存储数据库的键值对,集群被分为16384个槽。
  • 集群每个节点可以处理0或者16384个槽
  • 所有槽都有节点处理说明集群上线。如果其中一个没有处理说明下线。
  • 可以通过命令来分配槽给对应的节点处理。
CLUSTER ADDSLOTS <slot> [slot ...]
  • 把0-5000的槽交给7000节点来处理。
127.0.0.1:7000> CLUSTER ADDSLOTS 0 1 2 3 4 ... 5000
OK
127.0.0.1:7000> CLUSTER NODES
9dfb4c4e016e627d9769e4c9bb0d4fa208e65c26 127.0.0.1:7002 master - 0 1388316664849 0 connected
68eef66df23420a5862208ef5b1a7005b806f2ff 127.0.0.1:7001 master - 0 1388316665850 0 connected
51549e625cfda318ad27423a31e7476fe3cd2939 :0 myself,master - 0 0 0 co
  • 把5001-10000交给7001
127.0.0.1:7001> CLUSTER ADDSLOTS 5001 5002 5003 5004 ... 10000
OK
  • 剩下的交给7002。
127.0.0.1:7002> CLUSTER ADDSLOTS 10001 10002 10003 10004 ... 16383
OK
  • 现在的状态就是ok也就是上线的状态。
127.0.0.1:7000> CLUSTER INFO
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:3
cluster_size:3
cluster_current_epoch:0
cluster_stats_messages_sent:2699
cluster_stats_messages_received:2617
127.0.0.1:7000> CLUSTER NODES
9dfb4c4e016e627d9769e4c9bb0d4fa208e65c26 127.0.0.1:7002 master - 0 1388317426165 0 connected 10001-16383
68eef66df23420a5862208ef5b1a7005b806f2ff 127.0.0.1:7001 master - 0 1388317427167 0 connected 5001-10000
51549e625cfda318ad27423a31e7476fe3cd2939 :0 myself,master - 0 0 0 c

17.2.1 记录节点的槽指派信息

  • 节点的slots和numslots记录了节点负责处理哪些槽。
    • slots是一个二进制数组长度是2048个字节包含了16384个bit
      • 数位的二进制如果是1那么就是负责
      • 数位的二进制0那么就是不负责
    • numslots就是记录有多少个需要处理的槽。

struct clusterNode 
// ...
unsigned char slots[16384/8];
int numslots;
// ...
;

17.2.2 传播节点的槽指派信息

  • 除了处理自己的槽,还需要告诉别的节点自己处理什么槽

  • 当节点A接收到B的slots的时候,它会找到B对应的clusterNode进行更新。

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

  • slots数组记录了每个槽的指向信息。每个指向都是一个clusterNode指针。
    • null说明槽没有指派给其它节点
    • 如果指向一个clusterNode那么说明槽已经委派给这个节点了。
  • 如果指派信息是放到clusterNode上面那么就会导致每次查看槽的指派对象都需要进行遍历所有node。
  • 但是在state上做一个这样的slots就很好地解决了指派问题。
  • 但是对于clusterNode的slot也是必要的
    • 每次通知其它节点的时候只需要把数组发出去
    • 如果没有clusterNode的slot,那么每次发送A的被分配的槽的时候就需要遍历clusterState的slot获取所有的A的槽位置然后才能发送出去。
typedef struct clusterState 
// ...
clusterNode *slots[16384];
// ...
 clusterState;

17.2.4 CLUSTER ADDSLOTS命令的实现

  • 可以通过命令来把槽委派给节点处理。
  • 如果有其中一个槽被委派那么返回错误,如果都是没有被委派,修改clusterState的slot,然后再修改节点clusterNode的slot。
CLUSTER ADDSLOTS <slot> [slot ...]
def CLUSTER_ADDSLOTS(*all_input_slots):
#
遍历所有输入槽,检查它们是否都是未指派槽
for i in all_input_slots:
#
如果有哪怕一个槽已经被指派给了某个节点
#
那么向客户端返回错误,并终止命令执行
if clusterState.slots[i] != NULL:
reply_error()
return
如果所有输入槽都是未指派槽
#
那么再次遍历所有输入槽,将这些槽指派给当前节点
for i in all_input_slots:
#
设置clusterState
结构的slots
数组
#
将slots[i]
的指针指向代表当前节点的clusterNode
结构
clusterState.slots[i] = clusterState.myself
#
访问代表当前节点的clusterNode
结构的slots
数组
#
将数组在索引i
上的二进制位设置为1
setSlotBit(clusterState.myself.slots, i)

  • 做了一些委派CLUSTER ADDSLOTS 1 2

17.3 在集群中执行命令

  • 都委派之后就可以发送命令了。
  • 客户端发送命令,集群计算出数据库的键值对在什么槽,然后委派给节点来处理。
  • 步骤
    • 如果槽刚好在客户端发送命令的接收的节点上,直接执行
    • 不在就会返回moved错误,并且引导客户端转向正确的节点。再次发送命令

  • 如果在7000中执行命令且槽也在7000中
127.0.0.1:7000> SET date "2013-12-31"
OK
  • 如果7000中执行命令但不在7000中,最后转到7001执行。
127.0.0.1:7000> SET msg "happy new year!"
-> Redirected to slot [6257] located at 127.0.0.1:7001
OK
127.0.0.1:7001> GET msg
"happy new year!"

17.3.1 计算键属于哪个槽

  • crc16是计算key的校验和,然后和16383相与计算出一个在0-16383之内的槽号
  • cluster keyslot就能够计算出槽位。
def slot_number(key):
return CRC16(key) & 16383
127.0.0.1:7000> CLUSTER KEYSLOT "date"
(integer) 2022
127.0.0.1:7000> CLUSTER KEYSLOT "msg"
(integer) 6257
127.0.0.1:7000> CLUSTER KEYSLOT "name"
(integer) 5798
127.0.0.1:7000> CLUSTER KEYSLOT "fruits"
(integer) 14943
  • 命令调用的实际上就是上面的算法
def CLUSTER_KEYSLOT(key):
#
计算槽号
slot = slot_number(key)
#
将槽号返回给客户端
reply_client(slot)

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

  • 检查槽是不是自己处理只需要检查clusterState.slots就可以了。
  1. 如果是直接执行
  2. 否则转移到正确的节点(ip和端口)执行。

17.3.3 MOVED错误

  • 如果发现不是自己执行那么就会返回一个moved错误。
  • 下面是格式。
  • 有点类似于重定向的过程,通知,然后重定向到其它节点。
  • 正常是看不到这个moved错误的,因为他是自动转向的,并且被隐藏。
MOVED <slot> <ip>:<port>

17.3.4 节点数据库的实现

  • 集群只能使用0号数据库。
  • 键值对保存到数据库,但是还会使用跳表来保存槽和键之间的关系。
typedef struct clusterState 
// ...
zskiplist *slots_to_keys;
// ...
 clusterState;

  • slots_to_keys的分值都是槽号,成员就是数据库键。

    • 添加键值对,就会把键和槽号关联到跳跃表
    • 删除的时候就会删除跳跃表和键的关联。
  • 节点可以很方便对这些数据库键进行批量操作。

  • 比如,返回最多count个属于slot的数据库键。

CLUSTER GETKEYSINSLOT<slot><count>

17.4 重新分片

  • 重新分片就是指把任意数量已经指派给某个节点,但是可以修改这些分片的指派节点。

重新分片的实现原理

通过redis集群管理软件redis-trib负责执行。下面就是redis-trib对单个槽的分片处理

  1. redis-trib对目标节点发送下面的命令,让目标节点准备好从源节点导入属于槽slot的键值对。
CLUSTER
SETSLOT<slot>IMPORTING<source_id>
  1. redis-trib对源节点发送下面命令,把目标槽的键值对进行迁移到目标节点
CLUSTER
SETSLOT<slot>MIGRATING<target_id>
  1. 然后向源节点发送CLUSTER GETKEYSINSLOT (slot) (count)命令,获得最多count个的属于槽slot的键值对的键名。
  2. 对于每个键名&

    以上是关于《Redis设计与实现》(16-21)个人学习总结的主要内容,如果未能解决你的问题,请参考以下文章

    八月阶段计划

    学习笔记《Redis设计与实现》笔记

    学习笔记《Redis设计与实现》笔记

    Redis学习与总结

    Redis设计思路学习与总结

    redis学习记录:字典(dict)源码分析