Redis解析之订阅与发布

Redis订阅命令包括subscribe(订阅指定频道)和psubscribe(订阅符合指定模式的频道)两种,这些命令被广泛用于实时通信应用,如实时广播、实时提醒等。现在,我们就来研究一下redis-server订阅与发布的工作机制。

1、订阅

1.1、数据结构

//服务端信息
struct redisServer 
{
    //省略...

    dict *pubsub_channels;  //字典,标记所有频道和订阅客户端,其中key为频道,value是一个双向链表,链表的每一个结点代表订阅该频道的一个客户端。(用于subscribe命令)

    list *pubsub_patterns;  //链表,标记所有模式,每一个结点包括一个模式和订阅该模式的一个客户端,即在单个结点中,模式与客户端属于一一对应关系。(用于psubscribe命令)

    //  省略...
};

struct redisServer server; 
//客户端信息
typedef struct redisClient 
{
    //  省略...

    dict *pubsub_channels;  //字典,标记该客户端订阅的所有频道,key为频道,value为空(不使用)。(用于subscribe命令)

    list *pubsub_patterns;  //链表,标记该客户端订阅的所有模式。(用于psubscribe命令)

    //  省略...
} redisClient;

1.2、subscribe

redis-server接收到客户端的subscribe命令之后,首先会将命令的参数个数存入redisClient.argc,各命令参数存入指针数组redisClient.argv,然后调用pubsubSubscribeChannel(),依次订阅单个频道。

void subscribeCommand(redisClient *c) 
{
    int j;

    //依次订阅单个频道
    for (j = 1; j < c->argc; j++)
        pubsubSubscribeChannel(c,c->argv[j]);
    c->flags |= REDIS_PUBSUB;
}
//功能:为一个客户端订阅一个频道
//将客户端添加至字典redisServer.pubsub_channels,即:将客户端添加至指定频道所对应的客户端双向链表中。
//返回值:1(订阅成功),0(该客户端已经订阅过该频道)
int pubsubSubscribeChannel(redisClient *c, robj *channel) 
{
    dictEntry *de;
    list *clients = NULL;
    int retval = 0;

    //dictAdd():订阅频道
    //功能:将频道channel添加至字典redisClient.pubsub_channels,登记该客户端所订阅的频道
    //返回值:DICT_OK,订阅成功
    if (dictAdd(c->pubsub_channels,channel,NULL) == DICT_OK) 
    {
        retval = 1;

        //对象引用计数+1,便于后续释放对象
        incrRefCount(channel);  

        //1、检索字典redisServer.pubsub_channels,检查key=频道channel是否已存在
        //返回值:key存在时,直接返回key所对应的记录,否则返回NULL
        de = dictFind(server.pubsub_channels,channel);
        if (de == NULL) 
        {
            //主键不存在时
            //2.1、首先创建一个空链表clients,用于后续存储订阅该频道的所有客户端
            clients = listCreate();

            //2.2、往字典redisServer.pubsub_channels中新增一条记录,新增记录的key为频道channel,value是一个空链表clients
            dictAdd(server.pubsub_channels,channel,clients);

            incrRefCount(channel);  
        } 
        else 
        {
            //3、主键已存在时,直接获取key所对应的value,即订阅该频道的客户端双向链表
            clients = dictGetVal(de);
        }

        //4、将客户端c添加至双向链表clients
        listAddNodeTail(clients,c);
    }

    //5、设置响应消息
    //以下四个函数功能大体相似:
    //函数首先会检查本次响应是否已创建IO事件,若没有,则创建,并指定文件描述符fd、监听的事件类型mask为2(读:1,写:2)、事件函数为sendReplyToClient(读:readQueryFromClient,写:sendReplyToClient)。
    //(注意:针对单个客户端的一条subscribe命令,即使同时订阅多个频道,IO事件只会创建一次。)
    //然后将响应消息以Redis协议的形式添加至客户端的响应缓冲区
    //若一次订阅多个频道,则待所有频道订阅完毕后,再执行sendReplyToClient,将缓冲区内容一次性发送给客户端。

    //返回的参数个数
    //如:
    //*3\r\n
    addReply(c,shared.mbulkhdr[3]);

    //返回参数“subscribe”的长度和参数数据
    //如:
    //$9\r\n
    //subscribe\r\n
    addReply(c,shared.subscribebulk);

    //返回订阅的频道(包括频道名称的长度和频道名称)
    //如: 
    //$7\r\n
    //redis01\r\n
    addReplyBulk(c,channel);

    //返回客户端当前已订阅的频道总和
    //如:
    //:1\r\n    
    addReplyLongLong(c,clientSubscriptionsCount(c));

    return retval;
}

假设客户端执行:subscribe redis01 redis02,则pubsubSubscribeChannel()分三次依次订阅各频道,并分别往相应缓冲区中追加:

    *3\r\n
    $9\r\n
    subscribe\r\n
    $7\r\n
    redis01\r\n
    :1\r\n

    *3\r\n
    $9\r\n
    subscribe\r\n
    $7\r\n
    redis02\r\n
    :1\r\n

redis-cli收到的响应为:

    1) "subscribe"
    2) "redis01"
    3) (integer) 1
    1) "subscribe"
    2) "redis02"
    3) (integer) 2

1.3、psubscribe

redis-server接收到客户端的psubscribe命令之后,首先会将命令的参数个数存入redisClient.argc,各命令参数存入指针数组redisClient.argv,然后调用pubsubSubscribePattern(),依次订阅单个模式。

void psubscribeCommand(redisClient *c) 
{
    int j;

    //依次订阅单个模式
    for (j = 1; j < c->argc; j++)
        pubsubSubscribePattern(c,c->argv[j]);
    c->flags |= REDIS_PUBSUB;
}
//功能:为一个客户端订阅一个模式
//将客户端添加至链表redisServer.pubsub_patterns,即:往链表中追加单个结点,结点模式和客户端信息。单个结点中,模式和客户端属于一一对应关系。
//返回值:1(订阅成功),0(该客户端已经订阅过该模式)
int pubsubSubscribePattern(redisClient *c, robj *pattern) {
    int retval = 0;

    //1、检查客户端是否已经订阅该模式
    //返回值:NULL,说明该客户端未曾订阅过该模式
    if (listSearchKey(c->pubsub_patterns,pattern) == NULL) 
    {
        retval = 1;
        pubsubPattern *pat;

        //2.1、将模式pattern添加至链表redisClient.pubsub_patterns,登记该客户端所订阅的模式。
        listAddNodeTail(c->pubsub_patterns,pattern);
        incrRefCount(pattern);

        //2.2、创建一个结点pubsubPattern,用于后续存入链表redisServer.pubsub_patterns。
        pat = zmalloc(sizeof(*pat));

        //2.3、设置结点信息
        //getDecodedObject():获取编码对象的解码版本,并返回解码版本对象
        //若对象已经是原始编码,则对象引用计数+1,并返回原始对象
        //此处pattern为原始编码
        pat->pattern = getDecodedObject(pattern);
        pat->client = c;

        //2.4、将模式添加至链表redisServer.pubsub_patterns
        listAddNodeTail(server.pubsub_patterns,pat);
    }

    addReply(c,shared.mbulkhdr[3]);
    addReply(c,shared.psubscribebulk);
    addReplyBulk(c,pattern);
    addReplyLongLong(c,clientSubscriptionsCount(c));
    return retval;
}

2.1、publish

redis-server接收到客户端的publish命令之后,首先会将命令的参数个数存入redisClient.argc,各命令参数存入指针数组redisClient.argv,然后调用pubsubPublishMessage(),将信息推送到各订阅客户端。

void publishCommand(redisClient *c) 
{
    //1、发布消息
    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);

    //2、向publish客户端返回频道订阅者的个数
    addReplyLongLong(c,receivers);
}
//功能:发布消息
//函数首先向频道的订阅者发送消息,然后向频道所符合模式的订阅者发送消息
//返回值:订阅者的个数
int pubsubPublishMessage(robj *channel, robj *message) 
{
    int receivers = 0;
    dictEntry *de;
    listNode *ln;
    listIter li;

    //向频道的订阅者发送消息
    //1、检索字典redisServer.pubsub_channels,返回订阅该频道的客户端链表
    de = dictFind(server.pubsub_channels,channel);
    if (de) 
    {
        list *list = dictGetVal(de);
        listNode *ln;
        listIter li;

        //2、创建一个迭代器,用于遍历客户端链表
        listRewind(list,&li);

        //3、遍历客户端链表
        while ((ln = listNext(&li)) != NULL) 
        {
            redisClient *c = ln->value;

            //4、设置客户端响应缓冲区
            //返回的参数个数
            //如:
            //*3\r\n
            addReply(c,shared.mbulkhdr[3]);

            //返回参数“message”的长度和参数数据
            //如:
            //$9\r\n
            //message\r\n
            addReply(c,shared.messagebulk);

            //返回频道名称
            //如:
            //$7\r\n
            //redis02\r\n
            addReplyBulk(c,channel);

            //返回消息内容
            //如:
            //$10\r\n
            //helloworld\r\n
            addReplyBulk(c,message);

            receivers++;
        }
    }

    //向频道所符合模式的订阅者发送消息
    if (listLength(server.pubsub_patterns)) 
    {
        listRewind(server.pubsub_patterns,&li);
        channel = getDecodedObject(channel);

        //5、遍历链表pubsub_patterns.redisServer
        while ((ln = listNext(&li)) != NULL) 
        {
            pubsubPattern *pat = ln->value;

            //6、检查频道是否符合指定模式
            if (stringmatchlen((char*)pat->pattern->ptr,
                                (int)sdslen(pat->pattern->ptr),                 WIN_PORT_FIX /* cast (int) */
                                (char*)channel->ptr,
                                (int)sdslen(channel->ptr),0)) {                 WIN_PORT_FIX /* cast (int) */

                //7、设置客户端响应缓冲区
                //返回的参数个数
                //如:
                //*4\r\n                
                addReply(pat->client,shared.mbulkhdr[4]);

                //返回参数“pmessage”的长度和参数数据
                //如:
                //$8\r\n
                //pmessage\r\n
                addReply(pat->client,shared.pmessagebulk);

                //返回模式
                //如:
                //$6\r\n
                //redis*\r\n
                addReplyBulk(pat->client,pat->pattern);

                //返回频道名称
                //如:
                //$7\r\n
                //redis02\r\n
                addReplyBulk(pat->client,channel);

                //返回消息内容
                //如:
                //$10\r\n
                //helloworld\r\n
                addReplyBulk(pat->client,message);

                receivers++;
            }
        }
        decrRefCount(channel);
    }
    return receivers;
}
发布了112 篇原创文章 · 获赞 22 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/u010601662/article/details/77427038