Pub/Sub在主从故障切换时是如何发挥作用的

44 阅读6分钟

发布订阅通信方法的实现

发布订阅通信方法的基本模型是包含发布者、频道和订阅者。发布者把消息发布到频道上,而订阅者会订阅频道,一旦频道上有消息,频道就会把消息发送给订阅者。一个频道可以有多个订阅者,而对于一个订阅者来说,它也可以订阅多个频道,从而获得多个发布者发布的消息。

频道的实现

Redis 的全局变量 server 使用了一个成员变量 pubsub_channels 来保存频道,pubsub_channels 的初始化是在 initServer 函数(在server.c文件中)中完成的。initServer 函数会调用 dictCreate 创建一个 keylistDictType 类型的哈希表,然后用这个哈希表来保存频道的信息,如下所示:

void initServer(void) {
…
server.pubsub_channels = dictCreate(&keylistDictType,NULL);
…
}

Redis 把频道的名称作为哈希项的 key,而把订阅频道的订阅者作为哈希项的 value。

发布命令的实现

publish 命令,它对应的实现函数是 publishCommand(在pubsub.c文件中)

struct redisCommand redisCommandTable[] = {
…
{"publish",publishCommand,3,"pltF",0,NULL,0,0,0,0,0},
…
}

publishCommand 函数,它是调用 pubsubPublishMessage 函数(在 pubsub.c 文件中)来完成消息的实际发送,然后,再返回接收消息的订阅者数量的,如下所示:

void publishCommand(client *c) {
    //调用pubsubPublishMessage发布消息
    int receivers = pubsubPublishMessage(c->argv[1],c->argv[2]);
    … //如果Redis启用了cluster,那么在集群中发送publish命令
    addReplyLongLong(c,receivers); //返回接收消息的订阅者数量
}

pubsubPublishMessage 函数来说,它的原型如下。你可以看到,它的两个参数分别是要发布消息的频道,以及要发布的具体消息。

int pubsubPublishMessage(robj *channel, robj *message)

pubsubPublishMessage 函数会在 server.pubsub_channels 哈希表中,查找要发布的频道。如果找见了,它就会遍历这个 channel 对应的订阅者列表,然后依次向每个订阅者发送要发布的消息。这样一来,只要订阅者订阅了这个频道,那么发布者发布消息时,它就能收到了。

//查找频道是否存在
de = dictFind(server.pubsub_channels,channel);
    if (de) { //频道存在
        …
        //遍历频道对应的订阅者,向订阅者发送要发布的消息
        while ((ln = listNext(&li)) != NULL) {
            client *c = ln->value;
            …
            addReplyBulk(c,channel);
            addReplyBulk(c,message);
            receivers++;
        }
    }

订阅命令的实现

subscribe 对应的实现函数是 subscribeCommand(在 pubsub.c 文件中)。

subscribeCommand 函数的逻辑比较简单,它会直接调用 pubsubSubscribeChannel 函数(在 pubsub.c 文件中)来完成订阅操作,如下所示:

void subscribeCommand(client *c) {
    int j;
    for (j = 1; j < c->argc; j++)
        pubsubSubscribeChannel(c,c->argv[j]);
    c->flags |= CLIENT_PUBSUB;
}

subscribeCommand 函数的参数是 client 类型的变量,而它会根据 client 的 argc 成员变量执行一个循环,并把 client 的每个 argv 成员变量传给 pubsubSubscribeChannel 函数执行。

对于 client 的 argc 和 argv 来说,它们分别代表了要执行命令的参数个数和具体参数值。

pubsubSubscribeChannel 函数的原型:

int pubsubSubscribeChannel(client *c, robj *channel)

pubsubSubscribeChannel 函数的参数除了 client 变量外,还会接收频道的信息,这也就是说,subscribeCommand 会按照 subscribe 执行时附带的频道名称,来逐个订阅频道。

pubsubSubscribeChannel 函数的实现。这个函数的逻辑也比较清晰,主要可以分成三步:

  • 首先,它把要订阅的频道加入到 server 记录的 pubsub_channels 中。
  • 然后,pubsubSubscribeChannel 函数把执行 subscribe 命令的订阅者,加入到订阅者列表中。
  • 最后,pubsubSubscribeChannel 函数会把成功订阅的频道个数返回给订阅者。
if (dictAdd(c->pubsub_channels,channel,NULL) == DICT_OK) {
   …
   de = dictFind(server.pubsub_channels,channel); //在pubsub_channels哈希表中查找频道
   if (de == NULL) { //如果频道不存在
      clients = listCreate();  //创建订阅者对应的列表
      dictAdd(server.pubsub_channels,channel,clients); //新插入频道对应的哈希项
      …
    } else {
      clients = dictGetVal(de); //频道已存在,获取订阅者列表
    }
    listAddNodeTail(clients,c); //将订阅者加入到订阅者列表
}
 
…
addReplyLongLong(c,clientSubscriptionsCount(c)); //给订阅者返回成功订阅的频道数量

发布订阅方法在哨兵中的应用

哨兵用来发布消息的函数是 sentinelEvent。

sentinelEvent 函数与消息生成

这个函数的原型如下所示:

void sentinelEvent(int level, char *type, sentinelRedisInstance *ri, const char *fmt, ...)

这个函数最终是通过调用刚才我提到的 pubsubPublishMessage 函数,来实现向某一个频道发布消息的。那么,当我们要发布一条消息时,需要确定两个方面的内容:一个是要发布的频道,另一个是要发布的消息。

sentinelEvent 函数的第二个参数 type,表示的就是要发布的频道,而要发布的消息,就是由这个函数第四个参数 fmt 后面的省略号来表示的。这里的省略号表示的是可变参数。

哨兵订阅与 hello 频道

每个哨兵会订阅它所监听的主节点的"sentinel:hello"频道。

哨兵在周期性执行 sentinelTimer 函数时,会调用 sentinelHandleRedisInstance 函数,进而调用 sentinelReconnectInstance 函数。而在 sentinelReconnectInstance 函数中,哨兵会调用 redisAsyncCommand 函数,向主节点发送 subscribe 命令,订阅的频道由宏定义 SENTINEL_HELLO_CHANNEL(在 sentinel.c 文件中)指定,也就是"sentinel:hello"频道。这部分的代码如下所示:

retval = redisAsyncCommand(link->pc,
                sentinelReceiveHelloMessages, ri, "%s %s",
                sentinelInstanceMapCommand(ri,"SUBSCRIBE"),
                SENTINEL_HELLO_CHANNEL);

当在"sentinel:hello"频道上收到 hello 消息后,哨兵会回调 sentinelReceiveHelloMessages 函数来进行处理。而 sentinelReceiveHelloMessages 函数,实际是通过调用 sentinelProcessHelloMessage 函数,来完成 hello 消息的处理的。

哨兵在 sentinelTimer 函数中,调用 sentinelSendPeriodicCommands 函数时,由 sentinelSendPeriodicCommands 函数调用 sentinelSendHello 函数来完成发布 hello 消息的。

//hello消息包含的内容
snprintf(payload,sizeof(payload),
        "%s,%d,%s,%llu," //当前哨兵实例的信息,包括ip、端口号、ID和当前纪元
        "%s,%s,%d,%llu", //当前主节点的信息,包括名称、IP、端口号和纪元
        announce_ip, announce_port, sentinel.myid,
        (unsigned long long) sentinel.current_epoch,
        master->name,master_addr->ip,master_addr->port,
        (unsigned long long) master->config_epoch);
//向主节点的hello频道发布hello消息
retval = redisAsyncCommand(ri->link->cc,
        sentinelPublishReplyCallback, ri, "%s %s %s",
        sentinelInstanceMapCommand(ri,"PUBLISH"),
        SENTINEL_HELLO_CHANNEL,payload);

这样,当哨兵通过 sentinelSendHello,向自己监听的主节点的"sentinel:hello"频道发布 hello 消息时,和该哨兵监听同一个主节点的其他哨兵,也会订阅主节点的"sentinel:hello"频道,从而就可以获得该频道上的 hello 消息了。

通过这样的通信方式,监听同一主节点的哨兵就能相互知道彼此的访问信息了。如此一来,哨兵就可以基于这些访问信息,执行主节点状态共同判断,以及进行 Leader 选举等操作了。


此文章为10月Day25学习笔记,内容来源于极客时间《Redis 源码剖析与实战》