Redis设计与实现 笔记 第十八章 发布与订阅

发布与订阅

Redis 的发布与订阅功能由 PUBLISH, SUBSCRIBE,PSUBSCRIBE 组成

当客户端监听某个频道时,有人发布消息至该频道时,所有的订阅者都会收到这条信息.

18.1 频道的订阅与退订

当一个客户端执行了 SUBSCRIBE ,这个客户端将与被订阅频道之间就建立起了一种订阅关系.

以键值对模式建立相关链接,存入下面结构中, 键为字符串对象, 值为链表对象.

struct redisServer{
    
    dict* pubsub_channels;
};
18.1.1 订阅频道

主要规则就是判断当前是否有对应键,如果没有直接创建添加,如果有就加入末尾

* Subscribe a client to a channel. Returns 1 if the operation succeeded, or
 * 0 if the client was already subscribed to that channel. 
 *
 * 设置客户端 c 订阅频道 channel 。
 *
 * 订阅成功返回 1 ,如果客户端已经订阅了该频道,那么返回 0 。
 */
int pubsubSubscribeChannel(redisClient *c, robj *channel) {
    dictEntry *de;
    list *clients = NULL;
    int retval = 0;

    /* Add the channel to the client -> channels hash table */
    // 将 channels 填接到 c->pubsub_channels 的集合中(值为 NULL 的字典视为集合)
    if (dictAdd(c->pubsub_channels,channel,NULL) == DICT_OK) {
        retval = 1;
        incrRefCount(channel);

        // 关联示意图
        // {
        //  频道名        订阅频道的客户端
        //  'channel-a' : [c1, c2, c3],
        //  'channel-b' : [c5, c2, c1],
        //  'channel-c' : [c10, c2, c1]
        // }
        /* Add the client to the channel -> list of clients hash table */
        // 从 pubsub_channels 字典中取出保存着所有订阅了 channel 的客户端的链表
        // 如果 channel 不存在于字典,那么添加进去
        de = dictFind(server.pubsub_channels,channel);
        if (de == NULL) {
            clients = listCreate();
            dictAdd(server.pubsub_channels,channel,clients);
            incrRefCount(channel);
        } else {
            clients = dictGetVal(de);
        }

        // before:
        // 'channel' : [c1, c2]
        // after:
        // 'channel' : [c1, c2, c3]
        // 将客户端添加到链表的末尾
        listAddNodeTail(clients,c);
    }

    /* Notify the client */
    // 回复客户端。
    // 示例:
    // redis 127.0.0.1:6379> SUBSCRIBE xxx
    // Reading messages... (press Ctrl-C to quit)
    // 1) "subscribe"
    // 2) "xxx"
    // 3) (integer) 1
    addReply(c,shared.mbulkhdr[3]);
    // "subscribe\n" 字符串
    addReply(c,shared.subscribebulk);
    // 被订阅的客户端
    addReplyBulk(c,channel);
    // 客户端订阅的频道和模式总数
    addReplyLongLong(c,dictSize(c->pubsub_channels)+listLength(c->pubsub_patterns));

    return retval;
}
18.1.2 退订频道

简单的来说,从dict 中链表中删除对应的客户端节点.

void unsubscribeCommand(redisClient *c) {
    if (c->argc == 1) {
        pubsubUnsubscribeAllChannels(c,1);
    } else {
        int j;

        for (j = 1; j < c->argc; j++)
            pubsubUnsubscribeChannel(c,c->argv[j],1);
    }
}


/* Unsubscribe a client from a channel. Returns 1 if the operation succeeded, or
 * 0 if the client was not subscribed to the specified channel. 
 *
 * 客户端 c 退订频道 channel 。
 *
 * 如果取消成功返回 1 ,如果因为客户端未订阅频道,而造成取消失败,返回 0 。
 */
int pubsubUnsubscribeChannel(redisClient *c, robj *channel, int notify) {
    dictEntry *de;
    list *clients;
    listNode *ln;
    int retval = 0;

    /* Remove the channel from the client -> channels hash table */
    // 将频道 channel 从 client->channels 字典中移除
    incrRefCount(channel); /* channel may be just a pointer to the same object
                            we have in the hash tables. Protect it... */
    // 示意图:
    // before:
    // {
    //  'channel-x': NULL,
    //  'channel-y': NULL,
    //  'channel-z': NULL,
    // }
    // after unsubscribe channel-y :
    // {
    //  'channel-x': NULL,
    //  'channel-z': NULL,
    // }
    if (dictDelete(c->pubsub_channels,channel) == DICT_OK) {

        // channel 移除成功,表示客户端订阅了这个频道,执行以下代码

        retval = 1;
        /* Remove the client from the channel -> clients list hash table */
        // 从 channel->clients 的 clients 链表中,移除 client 
        // 示意图:
        // before:
        // {
        //  'channel-x' : [c1, c2, c3],
        // }
        // after c2 unsubscribe channel-x:
        // {
        //  'channel-x' : [c1, c3]
        // }
        de = dictFind(server.pubsub_channels,channel);
        redisAssertWithInfo(c,NULL,de != NULL);
        clients = dictGetVal(de);
        ln = listSearchKey(clients,c);
        redisAssertWithInfo(c,NULL,ln != NULL);
        listDelNode(clients,ln);

        // 如果移除 client 之后链表为空,那么删除这个 channel 键
        // 示意图:
        // before
        // {
        //  'channel-x' : [c1]
        // }
        // after c1 ubsubscribe channel-x
        // then also delete 'channel-x' key in dict
        // {
        //  // nothing here
        // }
        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);
        }
    }

    /* Notify the client */
    // 回复客户端
    if (notify) {
        addReply(c,shared.mbulkhdr[3]);
        // "ubsubscribe" 字符串
        addReply(c,shared.unsubscribebulk);
        // 被退订的频道
        addReplyBulk(c,channel);
        // 退订频道之后客户端仍在订阅的频道和模式的总数
        addReplyLongLong(c,dictSize(c->pubsub_channels)+
                       listLength(c->pubsub_patterns));

    }

    decrRefCount(channel); /* it is finally safe to release it */

    return retval;
}

18.2 模式的订阅与退订

类似于频道的订阅与退订,模式的订阅与退订.

struct redisServer{
    
    list* pubsub_patterns;
    
};


/*
 * 记录订阅模式的结构
 */
typedef struct pubsubPattern {

    // 订阅模式的客户端
    redisClient *client;

    // 被订阅的模式
    robj *pattern;

} pubsubPattern;

18.2.1 订阅模式

通过遍历查找来判断客户端有没有进行订阅,如果没有订阅则加入链表表尾.

void psubscribeCommand(redisClient *c) {
    int j;

    for (j = 1; j < c->argc; j++)
        pubsubSubscribePattern(c,c->argv[j]);
}

/* Subscribe a client to a pattern. Returns 1 if the operation succeeded, or 0 if the client was already subscribed to that pattern. 
 *
 * 设置客户端 c 订阅模式 pattern 。
 *
 * 订阅成功返回 1 ,如果客户端已经订阅了该模式,那么返回 0 。
 */
int pubsubSubscribePattern(redisClient *c, robj *pattern) {
    int retval = 0;

    // 在链表中查找模式,看客户端是否已经订阅了这个模式
    // 这里为什么不像 channel 那样,用字典来进行检测呢?
    // 虽然 pattern 的数量一般来说并不多
    if (listSearchKey(c->pubsub_patterns,pattern) == NULL) {
        
        // 如果没有的话,执行以下代码

        retval = 1;

        pubsubPattern *pat;

        // 将 pattern 添加到 c->pubsub_patterns 链表中
        listAddNodeTail(c->pubsub_patterns,pattern);

        incrRefCount(pattern);

        // 创建并设置新的 pubsubPattern 结构
        pat = zmalloc(sizeof(*pat));
        pat->pattern = getDecodedObject(pattern);
        pat->client = c;

        // 添加到末尾
        listAddNodeTail(server.pubsub_patterns,pat);
    }

    /* Notify the client */
    // 回复客户端。
    // 示例:
    // redis 127.0.0.1:6379> PSUBSCRIBE xxx*
    // Reading messages... (press Ctrl-C to quit)
    // 1) "psubscribe"
    // 2) "xxx*"
    // 3) (integer) 1
    addReply(c,shared.mbulkhdr[3]);
    // 回复 "psubscribe" 字符串
    addReply(c,shared.psubscribebulk);
    // 回复被订阅的模式
    addReplyBulk(c,pattern);
    // 回复客户端订阅的频道和模式的总数
    addReplyLongLong(c,dictSize(c->pubsub_channels)+listLength(c->pubsub_patterns));

    return retval;
}
18.2.2 退订模式

PUNSUBSCRIBECOMMAND
查找链表中是否有对应的客户端,进行一个删除完成退订

/* Unsubscribe a client from a channel. Returns 1 if the operation succeeded, or
 * 0 if the client was not subscribed to the specified channel. 
 *
 * 取消客户端 c 对模式 pattern 的订阅。
 *
 * 取消成功返回 1 ,因为客户端未订阅 pattern 而造成取消失败,返回 0 。
 */
int pubsubUnsubscribePattern(redisClient *c, robj *pattern, int notify) {
    listNode *ln;
    pubsubPattern pat;
    int retval = 0;

    incrRefCount(pattern); /* Protect the object. May be the same we remove */

    // 先确认一下,客户端是否订阅了这个模式
    if ((ln = listSearchKey(c->pubsub_patterns,pattern)) != NULL) {

        retval = 1;

        // 将模式从客户端的订阅列表中删除
        listDelNode(c->pubsub_patterns,ln);

        // 设置 pubsubPattern 结构
        pat.client = c;
        pat.pattern = pattern;

        // 在服务器中查找
        ln = listSearchKey(server.pubsub_patterns,&pat);
        listDelNode(server.pubsub_patterns,ln);
    }

    /* Notify the client */
    // 回复客户端
    if (notify) {
        addReply(c,shared.mbulkhdr[3]);
        // "punsubscribe" 字符串
        addReply(c,shared.punsubscribebulk);
        // 被退订的模式
        addReplyBulk(c,pattern);
        // 退订频道之后客户端仍在订阅的频道和模式的总数
        addReplyLongLong(c,dictSize(c->pubsub_channels)+
                       listLength(c->pubsub_patterns));
    }

    decrRefCount(pattern);

    return retval;
}
18.3 发送消息

Redis 客户端通过执行

PUBLISH <channel> <message> 

来发送对应的消息

通过查找 channel 频道的订阅者,来进行对应消息的转播

18.3.1 将消息发送给频道订阅者

大致逻辑是通过查找订阅记录结构中是否有对应的频道,对订阅者进行一个遍历,来达到消息转播的目的

void publishCommand(redisClient *c) {

    int receivers = pubsubPublishMessage(c->argv[1],c->argv[2]);
    if (server.cluster_enabled)
        clusterPropagatePublish(c->argv[1],c->argv[2]);
    else
        forceCommandPropagation(c,REDIS_PROPAGATE_REPL);
    addReplyLongLong(c,receivers);
}

/* Publish a message 
 *
 * 将 message 发送到所有订阅频道 channel 的客户端,
 * 以及所有订阅了和 channel 频道匹配的模式的客户端。
 */
int pubsubPublishMessage(robj *channel, robj *message) {
    int receivers = 0;
    dictEntry *de;
    listNode *ln;
    listIter li;

    /* Send to clients listening for that channel */
    // 取出包含所有订阅频道 channel 的客户端的链表
    // 并将消息发送给它们
    de = dictFind(server.pubsub_channels,channel);
    if (de) {
        list *list = dictGetVal(de);
        listNode *ln;
        listIter li;

        // 遍历客户端链表,将 message 发送给它们
        listRewind(list,&li);
        while ((ln = listNext(&li)) != NULL) {
            redisClient *c = ln->value;

            // 回复客户端。
            // 示例:
            // 1) "message"
            // 2) "xxx"
            // 3) "hello"
            addReply(c,shared.mbulkhdr[3]);
            // "message" 字符串
            addReply(c,shared.messagebulk);
            // 消息的来源频道
            addReplyBulk(c,channel);
            // 消息内容
            addReplyBulk(c,message);

            // 接收客户端计数
            receivers++;
        }
    }

    /* Send to clients listening to matching channels */
    // 将消息也发送给那些和频道匹配的模式
    if (listLength(server.pubsub_patterns)) {

        // 遍历模式链表
        listRewind(server.pubsub_patterns,&li);
        channel = getDecodedObject(channel);
        while ((ln = listNext(&li)) != NULL) {

            // 取出 pubsubPattern
            pubsubPattern *pat = ln->value;

            // 如果 channel 和 pattern 匹配
            // 就给所有订阅该 pattern 的客户端发送消息
            if (stringmatchlen((char*)pat->pattern->ptr,
                                sdslen(pat->pattern->ptr),
                                (char*)channel->ptr,
                                sdslen(channel->ptr),0)) {

                // 回复客户端
                // 示例:
                // 1) "pmessage"
                // 2) "*"
                // 3) "xxx"
                // 4) "hello"
                addReply(pat->client,shared.mbulkhdr[4]);
                addReply(pat->client,shared.pmessagebulk);
                addReplyBulk(pat->client,pat->pattern);
                addReplyBulk(pat->client,channel);
                addReplyBulk(pat->client,message);

                // 对接收消息的客户端进行计数
                receivers++;
            }
        }

        decrRefCount(channel);
    }

    // 返回计数
    return receivers;
}

18.3.2 将消息发送模式订阅者

先进行频道的发送,再遍历模式检查是否有对应的客户端,再进行发送

18.4 查看订阅信息

PUBSUB 命令是 Redis 2.8 新增加的命令之一.
可以查看频道或者模式的相关信息,比如某个频道目前有多少订阅者.

18.4.1 PUBSUB CHANNELS

通过

PUBSUB CHANNELS [pattern]

来进行对应频道的信息查找,根据是否给定了 pattern 来确定返回那个平道的信息.

if (!strcasecmp(c->argv[1]->ptr,"channels") &&
    (c->argc == 2 || c->argc ==3))
{
    /* PUBSUB CHANNELS [<pattern>] */
    // 检查命令请求是否给定了 pattern 参数
    // 如果没有给定的话,就设为 NULL
    sds pat = (c->argc == 2) ? NULL : c->argv[2]->ptr;

    // 创建 pubsub_channels 的字典迭代器
    // 该字典的键为频道,值为链表
    // 链表中保存了所有订阅键所对应的频道的客户端
    dictIterator *di = dictGetIterator(server.pubsub_channels);
    dictEntry *de;
    long mblen = 0;
    void *replylen;

    replylen = addDeferredMultiBulkLength(c);
    // 从迭代器中获取一个客户端
    while((de = dictNext(di)) != NULL) {

        // 从字典中取出客户端所订阅的频道
        robj *cobj = dictGetKey(de);
        sds channel = cobj->ptr;

        // 顺带一提
        // 因为 Redis 的字典实现只能遍历字典的值(客户端)
        // 所以这里才会有遍历字典值然后通过字典值取出字典键(频道)的蹩脚用法

        // 如果没有给定 pattern 参数,那么打印所有找到的频道
        // 如果给定了 pattern 参数,那么只打印和 pattern 相匹配的频道
        if (!pat || stringmatchlen(pat, sdslen(pat),
                                   channel, sdslen(channel),0))
        {
            // 向客户端输出频道
            addReplyBulk(c,cobj);
            mblen++;
        }
    }
    // 释放字典迭代器
    dictReleaseIterator(di);
    setDeferredMultiBulkLength(c,replylen,mblen);

// PUBSUB NUMSUB [channel-1 channel-2 ... channel-N] 子命令
} 
18.4.2 PUBSUB NUMSUB

返回对应频道的订阅数量

if (!strcasecmp(c->argv[1]->ptr,"numsub") && c->argc >= 2) {
    /* PUBSUB NUMSUB [Channel_1 ... Channel_N] */
    int j;

    addReplyMultiBulkLen(c,(c->argc-2)*2);
    for (j = 2; j < c->argc; j++) {

        // c->argv[j] 也即是客户端输入的第 N 个频道名字
        // pubsub_channels 的字典为频道名字
        // 而值则是保存了 c->argv[j] 频道所有订阅者的链表
        // 而调用 dictFetchValue 也就是取出所有订阅给定频道的客户端
        list *l = dictFetchValue(server.pubsub_channels,c->argv[j]);

        addReplyBulk(c,c->argv[j]);
        // 向客户端返回链表的长度属性
        // 这个属性就是某个频道的订阅者数量
        // 例如:如果一个频道有三个订阅者,那么链表的长度就是 3
        // 而返回给客户端的数字也是三
        addReplyBulkLongLong(c,l ? listLength(l) : 0);
    }

// PUBSUB NUMPAT 子命令
} 
18.4.3 PUBSUB NUMPAT

用于返回当前服务器被订阅模式的数量

 if (!strcasecmp(c->argv[1]->ptr,"numpat") && c->argc == 2) {
    /* PUBSUB NUMPAT */

    // pubsub_patterns 链表保存了服务器中所有被订阅的模式
    // pubsub_patterns 的长度就是服务器中被订阅模式的数量
    addReplyLongLong(c,listLength(server.pubsub_patterns));

// 错误处理
}
总结

本章介绍了订阅与发布,其的运作原理.

在看之前,想象过各种高大上的实现方式,但是,在源代码以及书籍面前,我深刻的意识到一句话.

程序本质是算法+数据结构

数据结构的简单组合就可以完成各种看似复杂的逻辑,且简单没有复杂度.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值