Redis源码分析--发布与订阅

发布与订阅:

一、介绍:

​ Redis提供了一个轻量的订阅机制:

命令 功能
PUBLISH channel message 将消息发送到指定的频道
SUBSCRIBE channel [channel …] 订阅给定的一个或多个频道的信息
UNSUBSCRIBE [channel [channel …]] 退订给定的频道
PSUBSCRIBE pattern [pattern …] 订阅一个或多个符合给定模式的频道
PUNSUBSCRIBE [pattern [pattern …]] 退订所有给定模式的频道

下面将介绍SUBCRIBE、PSUBSCRIBE以及PUBLISH,退订实现与订阅相似,不赘述;


二、SUBSCRIBE:

​ 为了实现订阅关系,redisServerredisClient都有对应的结构:

struct redisServer {
    // ...
    // 保存所有频道的订阅关系
	dict *pubsub_channels; 
    // ...
};

struct redisClient {
	// ...
    dict *pubsub_channels;
    // ...
};
  • L4:保存所有频道的订阅关系,key:channel,val:list of clients;
  • L10:保存该客户端订阅的频道,key:channel,val:NULL;

​ 订阅命令的实现:

void subscribeCommand(redisClient *c) {
    int j;

    /* 命令:SUBSCRIBE channel [channel …],对所有channel参数进行subscribe*/
    for (j = 1; j < c->argc; j++)
        pubsubSubscribeChannel(c,c->argv[j]);
}
/* 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(redisClient *c, robj *channel) {
    struct dictEntry *de;
    list *clients = NULL;
    int retval = 0;

    /* Add the channel to the client -> channels hash table */
    /*
     * client端:保存所有本客户端订阅的频道
     * pubsub_channels: {
     *  "chan1": NULL,
     *  "chan2": NULL,
     *  ...
     * }
     * server端:保存所有的订阅信息,key:channel,val:list of clients
     * pubsub_channels: {
     *  "chan1": client3->client2,
     *  "chan2": client2->client1->client3,
     *  ...
     * }
     */
    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) {
            /* 如果server保存的订阅信息找不到该频道,新建一个空client链表并插入channel对应的key中 */
            clients = listCreate();
            dictAdd(server.pubsub_channels,channel,clients);
            incrRefCount(channel);
        } else {
            clients = dictGetVal(de);
        }
        /* 新客户端插到链表的尾部 */
        listAddNodeTail(clients,c);
    }
    /* Notify the client */
    /* 大概说一下,添加"*3\r\n",下标3就是数字3,redis使用这些共享的sds来减少内存消耗 */
    addReply(c,shared.mbulkhdr[3]);
    /* "$9\r\nsubscribe\r\n" */
    addReply(c,shared.subscribebulk);
    addReplyBulk(c,channel);
    addReplyLongLong(c,dictSize(c->pubsub_channels)+listLength(c->pubsub_patterns));
    return retval;
}
  • L9~L21: redisServerredisClient结构体中都有 dict *pubsub_channels字典,但是保存的val不同;
  • L40~L46:执行结果返回个客户端;

三、PSUBSCRIBE:

​ 为了实现订阅关系,redisServerredisClient都有对应的结构:

typedef struct pubsubPattern {
    redisClient *client;
    robj *pattern;
} pubsubPattern;

struct redisServer {
    // ...
	list *pubsub_patterns;
    // ...
};

struct redisClient {
	// ...
    list *pubsub_patterns;
    // ...
};
  • L8:保存所有模式以及订阅的频道,val:pubsubPattern;
  • L14:保存该客户端订阅的频道,val:pattern字符串;
void psubscribeCommand(redisClient *c) {
    int j;
    /* 命令:PSUBSCRIBE pattern [pattern …] */
    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. */
int pubsubSubscribePattern(redisClient *c, robj *pattern) {
    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);
    }
    /* Notify the client */
    addReply(c,shared.mbulkhdr[3]);
    addReply(c,shared.psubscribebulk);
    addReplyBulk(c,pattern);
    addReplyLongLong(c,dictSize(c->pubsub_channels)+listLength(c->pubsub_patterns));
    return retval;
}
  • L6:pattern必须在中模式链表找到完全匹配的模式;

四、PUBLISH:

void publishCommand(redisClient *c) {
    int receivers = pubsubPublishMessage(c->argv[1],c->argv[2]);
    /* publish发送的消息要和从服务器一致,所以进行命令传播 */
    forceCommandPropagation(c,REDIS_PROPAGATE_REPL);
    addReplyLongLong(c,receivers);
}
  • L4:publish发送的消息要和从服务器一致,所以进行命令传播;
/* Publish a message */
int pubsubPublishMessage(robj *channel, robj *message) {
    int receivers = 0;
    struct dictEntry *de;
    listNode *ln;
    listIter li;

    /* Send to clients listening for that channel */
    de = dictFind(server.pubsub_channels,channel);
    if (de) {
        /* 保存了所有订阅channel的client的链表 */
        list *list = dictGetVal(de);
        listNode *ln;
        listIter li;
        /* 从list->head开始遍历,将消息发送给订阅的client */
        listRewind(list,&li);
        while ((ln = listNext(&li)) != NULL) {
            redisClient *c = ln->value;

            addReply(c,shared.mbulkhdr[3]);
            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 *pat = ln->value;
            /* 正则匹配 */
            if (stringmatchlen((char*)pat->pattern->ptr,
                                sdslen(pat->pattern->ptr),
                                (char*)channel->ptr,
                                sdslen(channel->ptr),0)) {
                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;
}

五、键空间通知:

​ 2.8版本开始,Redis引入了键空间通知(keyspace notifications),户端可以通过订阅/发布(Pub/Sub)机制,接收那些以某种方式改变了Redis数据空间的事件通知。具体看Redis键空间通知[1][2],具体实现也是用 pubsubPublishMessage来发布消息的。


参考:

  1. Redis源码解析(7) 发布订阅机制_李兆龙的博客-CSDN博客

  2. 10Redis键空间通知(keyspace notifications)_程序员的自我修养-CSDN博客_redis键空间通知

posted @ 2022-02-07 20:35  macguz  阅读(74)  评论(0编辑  收藏  举报