Redis pub/sub 发布订阅 原理&源码解析

一、订阅频道/模式

1.1 命令

Redis的订阅功能由SUBSCREBE、PSUBSCRIBE等命令组成。

​ SUBSCRIBE < channel >:客户端订阅一个或多个频道,成为频道订阅者subscriber,当有其他客户端向频道发送消息message时,频道的所有订阅者都会收到消息。

​ PSUBSCRIBE < patterns >:客户端订阅一个或多个模式,成为模式订阅者,模式可以匹配多个频道。

1.2 订阅关系的存储结构

​ Redis将所有频道和模式的订阅关系分别保存在pubsub_channels和pubsub_patterns中。

​ 这两个字典的键是某个被订阅的频道/模式,键的值是链表,链表的节点时所有订阅该key中频道/模式的客户端

​ 见server.h的redisServer

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

1.3 订阅流程

​ 每当客户端执行SUBSCRIBE/PSUBSCRIBE时,服务器都会将客户端与被订阅的频道在pubsub_channels/pubsub_patterns字段中进行关联。

​ 如果频道已有其他订阅者,则在链表尾部增加一个订阅者;

​ 如果频道无其他订阅者,将在字典中创建一个键,设置值为空链表,再将客户端添加到链表。

​ 见pubsub.c的pubsubSubscribeChannel和pubsubSubscribePattern

void subscribeCommand(client *c) {
    
    
    int j;

    for (j = 1; j < c->argc; j++)
        pubsubSubscribeChannel(c,c->argv[j]);
    c->flags |= CLIENT_PUBSUB;
}
/* 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) {
    
    
    dictEntry *de;
    list *clients = NULL;
    int retval = 0;

    /* Add the channel to the client -> channels hash table */
    if (dictAdd(c->pubsub_channels,channel,NULL) == DICT_OK) {
    
    
        retval = 1;
        incrRefCount(channel);
        /* 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);
    }
    /* Notify the client */
    addReplyPubsubSubscribed(c,channel);
    return retval;
}
/* Subscribe a client to a pattern. Returns 1 if the operation succeeded, or 0 if the client was already subscribed to that pattern. */
int pubsubSubscribePattern(client *c, robj *pattern) {
    
    
    dictEntry *de;
    list *clients;
    int retval = 0;

    if (listSearchKey(c->pubsub_patterns,pattern) == NULL) {
    
    
        retval = 1;
        pubsubPattern *pat;
        listAddNodeTail(c->pubsub_patterns,pattern);
        incrRefCount(pattern);
        pat = zmalloc(sizeof(*pat));
        pat->pattern = getDecodedObject(pattern);
        pat->client = c;
        listAddNodeTail(server.pubsub_patterns,pat);
        /* Add the client to the pattern -> list of clients hash table */
        de = dictFind(server.pubsub_patterns_dict,pattern);
        if (de == NULL) {
    
    
            clients = listCreate();
            dictAdd(server.pubsub_patterns_dict,pattern,clients);
            incrRefCount(pattern);
        } else {
    
    
            clients = dictGetVal(de);
        }
        listAddNodeTail(clients,c);
    }
    /* Notify the client */
    addReplyPubsubPatSubscribed(c,pattern);
    return retval;
}

1.4 退订流程

​ 退订流程就是订阅流程的逆向操作。

​ 每当客户端执行UNSUBSCRIBE/PUNSUBSCRIBE时,服务器都会将客户端与被订阅的频道在pubsub_channels/pubsub_patterns字段中进行关联。

​ 如果频道已有其他订阅者,则从链表尾部删除该订阅者;

​ 如果频道无其他订阅者,将字典中该频道/模式 设置值为空链表,删除该键。

​ 见pubsub.c的unsubscribeCommand和punsubscribeCommand,根据参数判断全退订还是退订一个

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

        for (j = 1; j < c->argc; j++)
            pubsubUnsubscribeChannel(c,c->argv[j],1);
    }
    if (clientSubscriptionsCount(c) == 0) c->flags &= ~CLIENT_PUBSUB;
}

void punsubscribeCommand(client *c) {
    
    
    if (c->argc == 1) {
    
    
        pubsubUnsubscribeAllPatterns(c,1);
    } else {
    
    
        int j;

        for (j = 1; j < c->argc; j++)
            pubsubUnsubscribePattern(c,c->argv[j],1);
    }
    if (clientSubscriptionsCount(c) == 0) c->flags &= ~CLIENT_PUBSUB;
}

二、发布消息

2.1 命令

​ PUBLISH < channel > < message >:向指定频道channel发布消息message。

2.1 发布流程

​ 服务器会执行以下两个动作:

​ 1)将消息message发送给频道channel所有的订阅者subscriber;

​ 2)如果有一个或多个模式pattern与频道channel匹配,将消息发送给对应模式patterns下的订阅者subscriber。

​ 见pubsub.c的publishCommand、pubsubPublishMessage

void publishCommand(client *c) {
    
    
    int receivers = pubsubPublishMessage(c->argv[1],c->argv[2]);
    if (server.cluster_enabled)
        clusterPropagatePublish(c->argv[1],c->argv[2]);
    else
        forceCommandPropagation(c,PROPAGATE_REPL);
    addReplyLongLong(c,receivers);
}

/* Publish a message */
int pubsubPublishMessage(robj *channel, robj *message) {
    
    
    int receivers = 0;
    dictEntry *de;
    dictIterator *di;
    listNode *ln;
    listIter li;

    /* Send to clients listening for that channel */
    de = dictFind(server.pubsub_channels,channel);
    if (de) {
    
    
        list *list = dictGetVal(de);
        listNode *ln;
        listIter li;

        listRewind(list,&li);
        while ((ln = listNext(&li)) != NULL) {
    
    
            client *c = ln->value;
            addReplyPubsubMessage(c,channel,message);
            receivers++;
        }
    }
    /* Send to clients listening to matching channels */
    di = dictGetIterator(server.pubsub_patterns_dict);
    if (di) {
    
    
        channel = getDecodedObject(channel);
        while((de = dictNext(di)) != NULL) {
    
    
            robj *pattern = dictGetKey(de);
            list *clients = dictGetVal(de);
            if (!stringmatchlen((char*)pattern->ptr,
                                sdslen(pattern->ptr),
                                (char*)channel->ptr,
                                sdslen(channel->ptr),0)) continue;

            listRewind(clients,&li);
            while ((ln = listNext(&li)) != NULL) {
    
    
                client *c = listNodeValue(ln);
                addReplyPubsubPatMessage(c,pattern,channel,message);
                receivers++;
            }
        }
        decrRefCount(channel);
        dictReleaseIterator(di);
    }
    return receivers;
}

三、查看订阅信息

3.1 命令

​ PUBSUB CHANNELS [pattern]:返回服务器当前被订阅的频道,pattern参数可选,选择时会返回与输入模式相匹配的频道

PUBSUB NUMSUB [channel-1 channel*2 ... channel-n]:接受任意多个频道作为输入参数,并返回这些频道的订阅者数量。

​ PUBSUB NUMPAT:返回服务器当前被订阅模式数量。

3.2 查看流程

​ 见pubsub.c中的pubsubCommand,可见根据命令CHANNELS、NUMPAT、NUMSUB执行不同的逻辑,在字典中统计出订阅信息

/* PUBSUB command for Pub/Sub introspection. */
void pubsubCommand(client *c) {
    
    
    if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"help")) {
    
    
        const char *help[] = {
    
    
"CHANNELS [<pattern>] -- Return the currently active channels matching a pattern (default: all).",
"NUMPAT -- Return number of subscriptions to patterns.",
"NUMSUB [channel-1 .. channel-N] -- Returns the number of subscribers for the specified channels (excluding patterns, default: none).",
NULL
        };
        addReplyHelp(c, help);
    } else if (!strcasecmp(c->argv[1]->ptr,"channels") &&
        (c->argc == 2 || c->argc == 3))
    {
    
    
        /* PUBSUB CHANNELS [<pattern>] */
        sds pat = (c->argc == 2) ? NULL : c->argv[2]->ptr;
        dictIterator *di = dictGetIterator(server.pubsub_channels);
        dictEntry *de;
        long mblen = 0;
        void *replylen;

        replylen = addReplyDeferredLen(c);
        while((de = dictNext(di)) != NULL) {
    
    
            robj *cobj = dictGetKey(de);
            sds channel = cobj->ptr;

            if (!pat || stringmatchlen(pat, sdslen(pat),
                                       channel, sdslen(channel),0))
            {
    
    
                addReplyBulk(c,cobj);
                mblen++;
            }
        }
        dictReleaseIterator(di);
        setDeferredArrayLen(c,replylen,mblen);
    } else if (!strcasecmp(c->argv[1]->ptr,"numsub") && c->argc >= 2) {
    
    
        /* PUBSUB NUMSUB [Channel_1 ... Channel_N] */
        int j;

        addReplyArrayLen(c,(c->argc-2)*2);
        for (j = 2; j < c->argc; j++) {
    
    
            list *l = dictFetchValue(server.pubsub_channels,c->argv[j]);

            addReplyBulk(c,c->argv[j]);
            addReplyLongLong(c,l ? listLength(l) : 0);
        }
    } else if (!strcasecmp(c->argv[1]->ptr,"numpat") && c->argc == 2) {
    
    
        /* PUBSUB NUMPAT */
        addReplyLongLong(c,listLength(server.pubsub_patterns));
    } else {
    
    
        addReplySubcommandSyntaxError(c);
    }
}

总结

​ redis在pubsub_channels和pubsub_patterns 两个订阅关系字典中,分别保存了所有频道和模式的订阅关系,SUBSCRIBE/PSUBSCRIBE将订阅关系存到字典的链表中,UNSUBSCRIBE/PUNSUBSCRIBE将订阅关系从字典的链表中移除。

​ PUBLISH命令通过访问订阅关系字典,向命中的频道下,链表中的所有客户端发送消息。

​ PUBSUB命令的三个子命令都是通过读取订阅关系字典来实现的。


redis版本:redis-6.2.9

参考:《redis设计与实现(第二版)》

猜你喜欢

转载自blog.csdn.net/weixin_43859729/article/details/111562168