注明:《Redis设计与实现》的个人学习总结,这本书对redis的讲解清晰易懂,如果深入学习可以看看这本书
$ redis-sentinel /path/to/your/sentinel.conf
$ redis-server /path/to/your/sentinel.conf --sentinel
#define REDIS_SERVERPORT 6379
#define REDIS_SENTINEL_PORT 26379
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;
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
选项中的quorum
参数
//
判断这个实例为客观下线(objectively down
)所需的支持投票数量
int quorum;
// SENTINEL parallel-syncs
选项的值
//
在执行故障转移操作时,可以同时对新的主服务器进行同步的从服务器数量
int parallel_syncs;
// SENTINEL failover-timeout
选项的值
//
刷新故障迁移状态的最大时限
mstime_t failover_timeout;
// ...
} sentinelRedisInstance;
typedef struct sentinelAddr {
char *ip;
int port;
} sentinelAddr;
#####################
# 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
最后一步就是向被监视的主服务器进行网络连接。成为主服务器的客户端,发送命令并且获取对应的信息。
但是需要两个连接
为什么有两个连接?
因为redis订阅和发布功能中,被发送信息不会存于server,为了防止丢失,那么sentinel就需要一个订阅连接来特殊处理这些订阅接收信息。
而且还需要发送命令和接收回复。
sentinel和多个实例进行网络连接所以sentinel使用的是异步连接。
# 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还会检查从服务器的实例是不是存在于字典里面
下面就是主服务器和所有的从服务器的sentinelRedisInstance实例。保存服务器名字,ip,端口。
# 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
...
PUBLISH __sentinel__:hello 信息
"127.0.0.1,26379,e955b4c85598ef5b5f055bc7ebfd5e828dbed4fa,0.."
SUBSCRIBE __sentinel__:hello
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
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 *
1)
2)
3)
1) 1
2) *
3) 0
CLUSTER MEET ip port
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;
// ...
};
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;
整个过程
相当于就是发送命令给A去加入B,A发送meet,B回应,A发送ping,B回应那么就握手成功。
CLUSTER ADDSLOTS [slot ...]
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
127.0.0.1:7001> CLUSTER ADDSLOTS 5001 5002 5003 5004 ... 10000
OK
127.0.0.1:7002> CLUSTER ADDSLOTS 10001 10002 10003 10004 ... 16383
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
struct clusterNode {
// ...
unsigned char slots[16384/8];
int numslots;
// ...
};
typedef struct clusterState {
// ...
clusterNode *slots[16384];
// ...
} clusterState;
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)
127.0.0.1:7000> SET date "2013-12-31"
OK
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!"
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)
MOVED <slot> <ip>:<port>
typedef struct clusterState {
// ...
zskiplist *slots_to_keys;
// ...
} clusterState;
slots_to_keys的分值都是槽号,成员就是数据库键。
节点可以很方便对这些数据库键进行批量操作。
比如,返回最多count个属于slot的数据库键。
CLUSTER GETKEYSINSLOT
通过redis集群管理软件redis-trib负责执行。下面就是redis-trib对单个槽的分片处理
CLUSTER
SETSLOTIMPORTING
CLUSTER
SETSLOTMIGRATING
MIGRATE0
重复执行3和4。
redis-trib向任意节点发送下面的命令,将槽slot委派给目标节点。并且把信息发送给整个集群。
CLUSTER
SETSLOTNODE
总结:执行cluster setslot (slot) importing (source_id)让目标节点准备好导入,CLUSTER SETSLOT (slot) MIGRATING (target_id)这个就是让源节点准备好迁移。再通过CLUSTER GETKEYSINSLOT来获取槽的键值对的键名,然后针对每个键名发送MIGRATE命令进行迁移。最后就是通知整个集群的迁移信息。
typedef struct clusterState {
// ...
clusterNode *importing_slots_from[16384];
// ...
} clusterState;
# 9dfb...
是节点7002
的ID
127.0.0.1:7003> CLUSTER SETSLOT 16198 IMPORTING 9dfb4c4e016e627d9769e4c9bb0d4fa208e65c26
OK
typedef struct clusterState {
// ...
clusterNode *migrating_slots_to[16384];
// ...
} clusterState;
# 0457...
是节点7003
的ID
127.0.0.1:7002> CLUSTER SETSLOT 16198 MIGRATING 04579925484ce537d3410d7ce97bd2e260c459a2
OK
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yUAscAWA-1637333363309)(C:/Users/11914/AppData/Roaming/Typora/typora-user-images/image-20211119142329843.png)]
def ASKING():
#
打开标识
client.flags |= REDIS_ASKING
#
向客户端返回OK
回复
reply("OK")
$ ./redis-cli -p 7003
127.0.0.1:7003> GET "love"
(error) MOVED 16198 127.0.0.1:7002
CLUSTER REPLICATE
struct clusterNode {
// ...
//
如果这是一个从节点,那么指向主节点
struct clusterNode *slaveof;
// ...
};
struct clusterNode {
// ...
//
正在复制这个主节点的从节点数量
int numslaves;
//
一个数组
//
每个数组项指向一个正在复制这个主节点的从节点的clusterNode
结构
struct clusterNode **slaves;
// ...
};
struct clusterNode {
// ...
//
一个链表,记录了所有其他节点对该节点的下线报告
list *fail_reports;
// ...
};
struct clusterNodeFailReport {
//
报告目标节点已经下线的节点
struct clusterNode *node;
//
最后一次从node
节点收到下线报告的时间
//
程序使用这个时间戳来检查下线报告是否过期
//
(与当前时间相差太久的下线报告会被删除)
mstime_t time;
} typedef clusterNodeFailReport;
消息通过两个部分组成,包括消息头和消息正文。
typedef struct {
//
消息的长度(包括这个消息头的长度和消息正文的长度)
uint32_t totlen;
//
消息的类型
uint16_t type;
//
消息正文包含的节点信息数量
//
只在发送MEET
、PING
、PONG
这三种Gossip
协议消息时使用
uint16_t count;
//
发送者所处的配置纪元
uint64_t currentEpoch;
//
如果发送者是一个主节点,那么这里记录的是发送者的配置纪元
//
如果发送者是一个从节点,那么这里记录的是发送者正在复制的主节点的配置纪元
uint64_t configEpoch;
//
发送者的名字(ID
)
char sender[REDIS_CLUSTER_NAMELEN];
//
发送者目前的槽指派信息
unsigned char myslots[REDIS_CLUSTER_SLOTS/8];
//
如果发送者是一个从节点,那么这里记录的是发送者正在复制的主节点的名字
//
如果发送者是一个主节点,那么这里记录的是REDIS_NODE_NULL_NAME
//
(一个40
字节长,值全为0
的字节数组)
char slaveof[REDIS_CLUSTER_NAMELEN];
//
发送者的端口号
uint16_t port;
//
发送者的标识值
uint16_t flags;
//
发送者所处集群的状态
unsigned char state;
//
消息的正文(或者说,内容)
union clusterMsgData data;
} clusterMsg;
union clusterMsgData {
// MEET
、PING
、PONG
消息的正文
struct {
//
每条MEET
、PING
、PONG
消息都包含两个
// clusterMsgDataGossip
结构
clusterMsgDataGossip gossip[1];
} ping;
// FAIL
消息的正文
struct {
clusterMsgDataFail about;
} fail;
// PUBLISH
消息的正文
struct {
clusterMsgDataPublish msg;
} publish;
//
其他消息的正文...
};
union clusterMsgData {
// ...
// MEET
、PING
和PONG
消息的正文
struct {
//
每条MEET
、PING
、PONG
消息都包含两个
// clusterMsgDataGossip
结构
clusterMsgDataGossip gossip[1];
} ping;
//
其他消息的正文...
};
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;
typedef struct {
char nodename[REDIS_CLUSTER_NAMELEN];
} clusterMsgDataFail;
typedef struct {
uint32_t channel_len;
uint32_t message_len;
//
定义为8
字节只是为了对齐其他消息结构
//
实际的长度由保存的内容决定
unsigned char bulk_data[8];
} clusterMsgDataPublish;
除了订阅频道。客户端还能够通过psubscribe来订阅一个或者是多个模式,成为模式的订阅者。这个模式其实也可以说是多个频道的抽象,只要频道符合规范,那么就能够加入到这个模式
下面这个例子的模式就是news.[ie]t,也就是频道的名字里面只要是it或者ie都能够匹配成功模式,订阅模式的客户端相当于订阅了多个频道。
struct redisServer {
// ...
//
保存所有频道的订阅关系
dict *pubsub_channels;
// ...
};
当客户端执行subscribe的时候就会在pubsub_channels字典上面进行关联,根据是否存在其他订阅者进行操作
比如下面的例子客户端执行了命令SUBSCRIBE “news.sport” “news.movie”,这个sport有链表直接加入尾巴,movie没有所以需要创建键,然后创建链表,把客户端加入到链表头。
def subscribe(*all_input_channels):
#
遍历输入的所有频道
for channel in all_input_channels:
#
如果channel
不存在于pubsub_channels
字典(没有任何订阅者)
#
那么在字典中添加channel
键,并设置它的值为空链表
if channel not in server.pubsub_channels:
server.pubsub_channels[channel] = []
#
将订阅者添加到频道所对应的链表的末尾
server.pubsub_channels[channel].append(client)
struct redisServer {
// ...
//
保存所有模式订阅关系
list *pubsub_patterns;
// ...
};
typedef struct pubsubPattern {
//
订阅模式的客户端
redisClient *client;
//
被订阅的模式
robj *pattern;
} pubsubPattern;
publish信息的时候
def channel_publish(channel, message):
#
如果channel
键不存在于pubsub_channels
字典中
#
那么说明channel
频道没有任何订阅者
#
程序不做发送动作,直接返回
if channel not in server.pubsub_channels:
return
#
运行到这里,说明channel
频道至少有一个订阅者
#
程序遍历channel
频道的订阅者链表
#
将消息发送给所有订阅者
for subscriber in server.pubsub_channels[channel]:
send_message(subscriber, message)
def pubsub_channels(pattern=None):
#
一个列表,用于记录所有符合条件的频道
channel_list = []
#
遍历服务器中的所有频道
#
(也即是pubsub_channels
字典的所有键)
for channel in server.pubsub_channels:
#
当以下两个条件的任意一个满足时,将频道添加到链表里面:
#1
)用户没有指定pattern
参数
#2
)用户指定了pattern
参数,并且channel
和pattern
匹配
if (pattern is None) or match(channel, pattern):
channel_list.append(channel)
#
向客户端返回频道列表
return channel_list
redis> PUBSUB CHANNELS
1) "news.it"
2) "news.sport"
3) "news.business"
4) "news.movie"
redis> PUBSUB NUMSUB news.it news.sport news.business news.movie
1) "news.it"
2) "3"
3) "news.sport"
4) "2"
5) "news.business"
6) "2"
7) "news.movie"
8) "1"
def pubsub_numsub(*all_input_channels):
#
遍历输入的所有频道
for channel in all_input_channels:
#
如果pubsub_channels
字典中没有channel
这个键
#
那么说明channel
频道没有任何订阅者
if channel not in server.pubsub_channels:
#
返回频道名
reply_channel_name(channel)
#
订阅者数量为0
reply_subscribe_count(0)
#
如果pubsub_channels
字典中存在channel
键
#
那么说明channel
频道至少有一个订阅者
else:
#
返回频道名
reply_channel_name(channel)
#
订阅者链表的长度就是订阅者数量
reply_subscribe_count(len(server.pubsub_channels[channel]))
redis> MULTI
OK
def MULTI():
打开事务标识
client.flags |= REDIS_MULTI
#
返回OK
回复
replyOK()
redis> SET "name" "Practical Common Lisp"
OK
redis> GET "name"
"Practical Common Lisp"
redis> SET "author" "Peter Seibel"
OK
redis> GET "author"
"Peter Seibel"
typedef struct redisClient {
// ...
//
事务状态
multiState mstate; /* MULTI/EXEC state */
// ...
} redisClient;
typedef struct multiState {
//
事务队列,FIFO
顺序
multiCmd *commands;
//
已入队命令计数
int count;
} multiState;
typedef struct multiCmd {
//
参数
robj **argv;
//
参数数量
int argc;
//
命令指针
struct redisCommand *cmd;
} multiCmd;
redis> MULTI
OK
redis> SET "name" "Practical Common Lisp"
QUEUED
redis> GET "name"
QUEUED
redis> SET "author" "Peter Seibel"
QUEUED
redis> GET "author"
QUEUED
redis> WATCH "name"
OK
redis> MULTI
OK
redis> SET "name" "peter"
QUEUED
redis> EXEC
(nil)
typedef struct redisDb {
// ...
//
正在被WATCH
命令监视的键
dict *watched_keys;
// ...
} redisDb;
def touchWatchKey(db, key):
#
如果键key
存在于数据库的watched_keys
字典中
#
那么说明至少有一个客户端在监视这个key
if key in db.watched_keys:
#
遍历所有监视键key
的客户端
for client in db.watched_keys[key]:
#
打开标识
client.flags |= REDIS_DIRTY_CAS
命令不存在或者是命令格式不对,那么redis就会拒绝执行这个命令。
事务的执行错误
redis> RPUSH numbers 5 3 1 4 2
(integer) 5
#
按插入顺序排列的列表元素
redis> LRANGE numbers 0 -1
1) "5"
2) "3"
3) "1"
4) "4"
5) "2"
#
按值从小到大有序排列的列表元素
redis> SORT numbers
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
redis> SADD alphabet a b c d e f g
(integer) 7
#
乱序排列的集合元素
redis> SMEMBERS alphabet
1) "d"
2) "a"
3) "f"
4) "e"
5) "b"
6) "g"
7) "c"
#
排序后的集合元素
redis> SORT alphabet ALPHA
1) "a"
2) "b"
3) "c"
4) "d"
5) "e"
6) "f"
7) "g"
redis> ZADD test-result 3.0 jack 3.5 peter 4.0 tom
(integer) 3
#
按元素的分值排列
redis> ZRANGE test-result 0 -1
1) "jack"
2) "peter"
3) "tom"
#
为各个元素设置序号
redis> MSET peter_number 1 tom_number 2 jack_number 3
OK
#
以序号为权重,对有序集合中的元素进行排序
redis> SORT test-result BY *_number
1) "peter"
2) "tom"
3) "jack"
sort numbers的全部步骤
排序后的数组。
redis> RPUSH numbers 3 1 2
(integer) 3
redis> SORT numbers
1) "1"
2) "2"
3) "3"
typedef struct _redisSortObject {
//
被排序键的值
robj *obj;
//
权重
union {
//
排序数字值时使用
double score;
//
排序带有BY
选项的字符串值时使用
robj *cmpobj;
} u;
} redisSortObject;
redis> SADD fruits apple banana cherry
(integer) 3
#
元素在集合中是乱序存放的
redis> SMEMBERS fruits
1) "apple"
2) "cherry"
3) "banana"
#
对fruits
键进行字符串排序
redis> SORT fruits ALPHA
1) "apple"
2) "banana"
3) "cherry"
SORT
SORT ASC
SORT DESC
redis> SADD fruits "apple" "banana" "cherry"
(integer) 3
redis> SORT fruits ALPHA
1) "apple"
2) "banana"
3) "cherry"
redis> MSET apple-price 8 banana-price 5.5 cherry-price 7
OK
redis> SORT fruits BY *-price
1) "banana"
2) "cherry"
3) "apple"
步骤
redis> SADD fruits "apple" "banana" "cherry"
(integer) 3
redis> MSET apple-id "FRUIT-25" banana-id "FRUIT-79" cherry-id "FRUIT-13"
OK
redis> SORT fruits BY *-id ALPHA
1)"cherry"
2)"apple"
3)"banana"
实现步骤
还没排序
排好序的数组
redis> SORT alphabet ALPHA LIMIT 0 4
1) "a"
2) "b"
3) "c"
4) "d"
步骤