Redis的pub/sub 实现原理

4,428 阅读3分钟

channel的数据结构

Redis将所有订阅关系保持在服务器状态的pubsub_channels字典项中:

struct redisServer {
    ...
    dict *pubsub_channels;  /* Map channels to list of subscribed clients */
    ...
}

该字典项的key是被订阅的某个channel名称,字典项的value是一个链表,链表中保存了所有订阅这个channel的客户端: pub/sub字典示例

订阅频道

SUBSCRIBE channel [channel ...] 当客户端执行SUBSCRIBE命令,订阅某个或某些频道时,服务器会将客户端与被订阅的频道在pubsub_channels字典中进行关联。pubsub.c:

void subscribeCommand(client *c) {
    int j;

    for (j = 1; j < c->argc; j++)
        pubsubSubscribeChannel(c,c->argv[j]);
    c->flags |= CLIENT_PUBSUB;
}

根据频道是否存在于pubsub_channels字典中,关联操作分俩种情况:

  • 如果频道不存在,表示该频道还未有任何订阅者,程序首先要在pubsub_channels字典中创建该频道,键名即为该频道名,并将这个键的值设置为空链表,然后再将客户端添加到链表。
  • 如果频道存在,表示该频道已有其他订阅者,那么它在pubsub_channels字典中必然有相应的订阅者链表,程序唯一要做的就是将客户端添加到订阅者链表的末尾。
/* Subscribe a client to a channel. Returns 1 if the operation succeeded, or
 * 0 if the client was already subscribed to that channel. */
int pubsubSubscribeChannel(client *c, robj *channel) {
    ...
    /* Add the channel to the client -> channels hash table */
    if (dictAdd(c->pubsub_channels,channel,NULL) == DICT_OK) {
        /* Add the client to the channel -> list of clients hash table */
        de = dictFind(server.pubsub_channels,channel);
        if (de == NULL) {
            clients = listCreate();
            dictAdd(server.pubsub_channels,channel,clients);
            incrRefCount(channel);
        } else {
            clients = dictGetVal(de);
        }
        listAddNodeTail(clients,c);
    }
    ...
}

由代码可知,在进行关联操作之前,先将该频道添加到客户端状态的pubsub_channels字典中,留待客户端离线时使用。字典的键为该频道名,键的值为null。

// 将频道添加到客户端状态的pubsub_channels字典中,键为channel,键的值为NULL
if (dictAdd(c->pubsub_channels,channel,NULL) == DICT_OK) {}

假如执行下面操作:

client-9527> subscribe qiuxiang 

当某个频道有新消息时,Redis遍历该频道的客户端列表,依次给每个客户端发送消息。

取消订阅

UNSUBSCRIBE channel [channel ...] 取消订阅,就是将该客户端从pubsub_channels字典中移除:

client-9527> unsubscribe qiuxiang 

pub/sub字典示例

int pubsubUnsubscribeChannel(client *c, robj *channel, int notify) {
   ...
        // 找到要取消订阅频道对应的字典项
        de = dictFind(server.pubsub_channels,channel);
        ...
        // 订阅该频道的所有客户端列表
        clients = dictGetVal(de);
        // 找到要取消订阅的客户端
        ln = listSearchKey(clients,c);
        ...
        // 将该客户端从列表中移除
        listDelNode(clients,ln);
        // 如果列表项未空,表示没有客户端订阅该频道了,将该频道从pubsub_channels字典中移除
        if (listLength(clients) == 0) {
            /* Free the list and associated hash entry at all if this was
             * the latest client, so that it will be possible to abuse
             * Redis PUBSUB creating millions of channels. */
            dictDelete(server.pubsub_channels,channel);
        }
    ...
}

客户端离线

当客户端离线时,Redis会执行freeClient函数,

void freeClient(client *c) {
    ...
    // 取消所有频道的订阅
    pubsubUnsubscribeAllChannels(c,0);
    ...
}
int pubsubUnsubscribeAllChannels(client *c, int notify) {
    // 获取客户端已订阅频道的字典迭代器
    dictIterator *di = dictGetSafeIterator(c->pubsub_channels);
    ...
    // 迭代字典
    while((de = dictNext(di)) != NULL) {
        robj *channel = dictGetKey(de);

        // 依次取消订阅
        count += pubsubUnsubscribeChannel(c,channel,notify);
    }
}

假如客户端"Client 1"离线: Client 1将离线

"Client 1"离线后:

后记

Redis的pub/sub功能,只能实时获取订阅的频道消息,当客户端离线后,离线后的频道消息不会被保存起来,这点和MQ服务还是有区别的。