redis之批量关注公众(发布订阅模式)

一、简介

上次说的subscribe只能订阅一个确定名称的channel,比如我记不清某个公众号的全称,只记得公众号名称中带有redis, 则可以使用模糊匹配的方式进行订阅,使用 *redis*则能订阅包含redis的所有公众号。
redis可以通过模糊匹配规则订阅多个channel。
模糊匹配规则:

  • h?llo subscribes to hello, hallo and hxllo
  • h*llo subscribes to hllo and heeeello
  • h[ae]llo subscribes to hello and hallo, but not hillo

这些规则和正则表达式类似,只是种类少,?匹配某一个字符,*匹配任意个字符,[list]匹配一个列表中的某一个字符。看https://redis.io/commands/psubscribe官方的命令介绍说的规则就这几种,但是看源代码中在psubscribe命令处理时并没有对这些规则进行判断,而匹配函数中有支持更多几种规则,不知道是官网命令没有更新还是我看代码时遗漏了一些细节。

二、批量订阅(批量订阅公众号)

使用psubscribe命令进行批量进行channel的订阅, redis服务器使用psubscribeCommand函数进行处理。
PSUBSCRIBE pattern [pattern ...]

struct redisCommand redisCommandTable[] = {
	...
	{"psubscribe",psubscribeCommand,-2,
	     "pub-sub no-script ok-loading ok-stale",
	     0,NULL,0,0,0,0,0,0},
	...
};

2.1 排除不能执行订阅的client

第一步依然是排除要求非阻塞的client, 但对multi做了特殊处理。

void psubscribeCommand(client *c) {
    int j;
    if ((c->flags & CLIENT_DENY_BLOCKING) && !(c->flags & CLIENT_MULTI)) {
        /**
         * A client that has CLIENT_DENY_BLOCKING flag on
         * expect a reply per command and so can not execute subscribe.
         *
         * Notice that we have a special treatment for multi because of
         * backword compatibility
         */
        addReplyError(c, "PSUBSCRIBE isn't allowed for a DENY BLOCKING client");
        return;
    }
    ...
}

2.2 订阅处理

逐一处理每一个pattern。

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

2.2.1 判断是否已经存在相同的pattern

int pubsubSubscribePattern(client *c, robj *pattern) {
    dictEntry *de;
    list *clients;
    int retval = 0;

	//从client的链表中搜索是否有相同的pattern
    if (listSearchKey(c->pubsub_patterns,pattern) == NULL) {
    ....

搜索过程是遍历链表中的每一项,回调match函数进行匹配,如果匹配上则返回对应的节点,否则继续下一项,如果都没有匹配上,则返回NULL,说明列表中没有相同的项。

listNode *listSearchKey(list *list, void *key)
{
    listIter iter;
    listNode *node;

    listRewind(list, &iter);
    while((node = listNext(&iter)) != NULL) {
        if (list->match) {
            if (list->match(node->value, key)) {
                return node;
            }
        } else {
            if (key == node->value) {
                return node;
            }
        }
    }
    return NULL;
}

而对于此match函数,在client第一次建立连接被创建时就设置好了。

client *createClient(connection *conn) {
	...
	c->pubsub_patterns = listCreate();
	...
 	listSetFreeMethod(c->pubsub_patterns,decrRefCountVoid);
    listSetMatchMethod(c->pubsub_patterns,listMatchObjects);
    ...
}

2.2.2 将pattern加入到当前client的链表中

如果此pattern没有在此client的链表中找到,则是第一次订阅,将它加入到链表中,增加引用计数是为了后续加入到全局hash表中共享使用。

 	retval = 1;
    listAddNodeTail(c->pubsub_patterns,pattern);
    incrRefCount(pattern);

2.2.3 将pattern加入到全局的hash表中

如果是第一次加入到client的链表中,则查询是否在全局hash表中是否存在,不存在则加入到全局hash表中。

 /* Add the client to the pattern -> list of clients hash table */
   de = dictFind(server.pubsub_patterns,pattern);
     if (de == NULL) {
         clients = listCreate();
         dictAdd(server.pubsub_patterns,pattern,clients);
         incrRefCount(pattern);
     } else {
         clients = dictGetVal(de);
     }

2.2.4 将client加入链表中

全局hash表中的每一项,key为pattern , 值即为client对象。

	listAddNodeTail(clients,c);

2.2.5 通知client

/* Notify the client */
    addReplyPubsubPatSubscribed(c,pattern);
/* Send the pubsub pattern subscription notification to the client. */
void addReplyPubsubPatSubscribed(client *c, robj *pattern) {
    if (c->resp == 2)
        addReply(c,shared.mbulkhdr[3]);
    else
        addReplyPushLen(c,3);
    addReply(c,shared.psubscribebulk);
    addReplyBulk(c,pattern);
    addReplyLongLong(c,clientSubscriptionsCount(c));
}

2.3 设置订阅模式

 c->flags |= CLIENT_PUBSUB;

三、发布消息(多账户同时更新文章)

PUBLISH channel message
发布消息依然使用publish命令,只是在发送消息的channel时,选取client时,需要遍历所有的pattern。

struct redisCommand redisCommandTable[] = {
	...
	 {"publish",publishCommand,3,
     "pub-sub ok-loading ok-stale fast may-replicate",
     0,NULL,0,0,0,0,0,0},
     ...
};

3.1 对channel完全相等的client发送消息

从server.pubsub_channels中获取的channel都是全名,即完全相等的channel,才会对这些client发送消息。

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++;
        }
    }

3.2 对pattern满足的client发送消息

本文主要关注这种情况。

3.2.1 遍历全局的hash表

 /* Send to clients listening to matching channels */
    di = dictGetIterator(server.pubsub_patterns);
    if (di) {
    	...
        while((de = dictNext(di)) != NULL) {
           ...
        }
    	...
        dictReleaseIterator(di);
    }

3.2.2 逐一使用pattern匹配channel

//遍历hash表项
 while((de = dictNext(di)) != NULL) {
	//获取pattern和client列表
    robj *pattern = dictGetKey(de);
    list *clients = dictGetVal(de);

	//使用pattern匹配channel, 如果没有匹配上,则开始下一项的处理
    if (!stringmatchlen((char*)pattern->ptr,
                        sdslen(pattern->ptr),
                        (char*)channel->ptr,
                        sdslen(channel->ptr),0)) continue;
	
	...
}

如下为匹配的处理函数,当有*时会进行递归回溯操作,因此不要轻易的使用*进行模糊匹配,频繁的操作将会影响性能

int stringmatchlen(const char *pattern, int patternLen,
        const char *string, int stringLen, int nocase)
{
    while(patternLen && stringLen) {
        switch(pattern[0]) {
        case '*':
        	//跳过连续的*
            while (patternLen && pattern[1] == '*') {
                pattern++;
                patternLen--;
            }
            //如果只有一个*, 则匹配上。
            if (patternLen == 1)
                return 1; /* match */
			//开始递归操作
            while(stringLen) {
                if (stringmatchlen(pattern+1, patternLen-1,
                            string, stringLen, nocase))
                    return 1; /* match */
                string++;
                stringLen--;
            }
            return 0; /* no match */
            break;
        case '?': //匹配任意一个字符,所以直接跳过一个字符
            string++;
            stringLen--;
            break;
        case '[': //列表开始
        {
            int not, match;

            pattern++;
            patternLen--;
            not = pattern[0] == '^'; //判断是否为排除列表中的字符
            if (not) { //是排除列表中的字符,移动到pattern的下一个字符
                pattern++;
                patternLen--;
            }
            match = 0;
            while(1) {
            	//转义处理
                if (pattern[0] == '\\' && patternLen >= 2) {
                    pattern++;
                    patternLen--;
                    if (pattern[0] == string[0])
                        match = 1;
                 
                 // 列表结束      
                } else if (pattern[0] == ']') {
                    break;
                } else if (patternLen == 0) {//pattern结束,感觉是出错了
                    pattern--;
                    patternLen++;
                    break;

				//范围
                } else if (patternLen >= 3 && pattern[1] == '-') {
                	//获取起止范围
                    int start = pattern[0];
                    int end = pattern[2];
                    int c = string[0];
                    if (start > end) {
                        int t = start;
                        start = end;
                        end = t;
                    }
                    //如果不区分大小写,则都转换为小写
                    if (nocase) {
                        start = tolower(start);
                        end = tolower(end);
                        c = tolower(c);
                    }
                    pattern += 2;
                    patternLen -= 2;
                    //判断是否在范围内
                    if (c >= start && c <= end)
                        match = 1;
                } else {
                    if (!nocase) {//大小写敏感,直接比较
                        if (pattern[0] == string[0])
                            match = 1;
                    } else { //大小写不敏感,都转成小写进行比较
                        if (tolower((int)pattern[0]) == tolower((int)string[0]))
                            match = 1;
                    }
                }
                pattern++;
                patternLen--;
            }
            if (not) //排除列表中的
                match = !match;
            if (!match)
                return 0; /* no match */
            string++;
            stringLen--;
            break;
        }
        case '\\': //转义字符,跳过转义符
            if (patternLen >= 2) {
                pattern++;
                patternLen--;
            }
            /* fall through */
        default:
            if (!nocase) {
                if (pattern[0] != string[0])
                    return 0; /* no match */
            } else {
                if (tolower((int)pattern[0]) != tolower((int)string[0]))
                    return 0; /* no match */
            }
            string++;
            stringLen--;
            break;
        }
        pattern++;
        patternLen--;
        if (stringLen == 0) {
        	//如果string结束了,而pattern还有*,则跳过*
            while(*pattern == '*') {
                pattern++;
                patternLen--;
            }
            break;
        }
    }
    //pattern和string都结束,则匹配成功
    if (patternLen == 0 && stringLen == 0)
        return 1;
    return 0;
}

3.2.3 对pattern匹配成功的client队列发送消息

匹配上的pattern存储的是订阅了这个pattern channel的所有client,所以遍历整个链表,对每个client发送消息。

//pattern匹配上,则遍历client链表,逐一发送消息
    listRewind(clients,&li);
    while ((ln = listNext(&li)) != NULL) {
        client *c = listNodeValue(ln);
        addReplyPubsubPatMessage(c,pattern,channel,message);
        receivers++;
    }

四、批量取消订阅(取消关注)

PUNSUBSCRIBE [pattern [pattern ...]]

struct redisCommand redisCommandTable[] = {
...
	{"punsubscribe",punsubscribeCommand,-1,
     "pub-sub no-script ok-loading ok-stale",
     0,NULL,0,0,0,0,0,0},
...
};

根据参数个数进行不同的操作,当没有参数则将当前client订阅的所有的pattern的channel都取消,否则只处理参数指定的pattern的channel被取消。

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;
}

其中清除所有的pattern channel时还是调用的pubsubUnsubscribePattern函数,所以我们关注pubsubUnsubscribePattern函数。

/* Unsubscribe from all the patterns. Return the number of patterns the
 * client was subscribed from. */
int pubsubUnsubscribeAllPatterns(client *c, int notify) {
    listNode *ln;
    listIter li;
    int count = 0;

    listRewind(c->pubsub_patterns,&li);
    while ((ln = listNext(&li)) != NULL) {
        robj *pattern = ln->value;

        count += pubsubUnsubscribePattern(c,pattern,notify);
    }
    if (notify && count == 0) addReplyPubsubPatUnsubscribed(c,NULL);
    return count;
}

4.1 从client的链表中删除此pattern channel

如果能从此client的pubsub_patterns链表中找到此channel,则从链表中删除。

int pubsubUnsubscribePattern(client *c, robj *pattern, int notify) {
    ...
    
    if ((ln = listSearchKey(c->pubsub_patterns,pattern)) != NULL) {
        retval = 1;
        listDelNode(c->pubsub_patterns,ln);

4.2 从全局的hash的pattern channel的链表中删除client

将此client从hash中的client链表中删除,当client链表为空时,将此hash项也从hash表中删除。

/* Remove the client from the pattern -> clients list hash table */
        de = dictFind(server.pubsub_patterns,pattern);
        serverAssertWithInfo(c,NULL,de != NULL);
        clients = dictGetVal(de);
        ln = listSearchKey(clients,c);
        serverAssertWithInfo(c,NULL,ln != NULL);
        listDelNode(clients,ln);
        if (listLength(clients) == 0) {
            /* Free the list and associated hash entry at all if this was
             * the latest client. */
            dictDelete(server.pubsub_patterns,pattern);
        }

4.3 通知client

 /* Notify the client */
    if (notify) addReplyPubsubPatUnsubscribed(c,pattern);
void addReplyPubsubPatUnsubscribed(client *c, robj *pattern) {
    if (c->resp == 2)
        addReply(c,shared.mbulkhdr[3]);
    else
        addReplyPushLen(c,3);
    addReply(c,shared.punsubscribebulk);
    if (pattern)
        addReplyBulk(c,pattern);
    else
        addReplyNull(c);
    addReplyLongLong(c,clientSubscriptionsCount(c));
}

五、对比

psubscribe,punsubscribe和subscribe ,unsubscribe实际上都是相同的逻辑,只是将channel存放的地方不同,去重方式不同。

命令功能client存储方式client去重方式去重/查找时间复杂度
SUBSCRIBE channel [channel …]订阅消息hashhashO(1)
UNSUBSCRIBE [channel [channel …]]取消订阅O(1)
PSUBSCRIBE pattern [pattern …]pattern订阅list遍历比较O(N)
PUNSUBSCRIBE [pattern [pattern …]]取消订阅O(N)

而且pattern只是在publish命令中有作用,其他时候都是当作一个简单的字符串对待,比如去重等。
而server中保存的channel只是不同的变量,但是都是相同的hash类型,结构完全相同。

5.1 结构图

比如client1订阅了pattern1, client2订阅了pattern1,pattern2。
请添加图片描述

六、获取channel的一些信息

在redis 2.8.0增加了pubsub命令,获取一些channel的一些信息。

struct redisCommand redisCommandTable[] = {
...
  {"pubsub",pubsubCommand,-2,
     "pub-sub ok-loading ok-stale random",
     0,NULL,0,0,0,0,0,0},
...
};

其中有四个子命令:

  • CHANNELS
  • NUMPAT
  • NUMSUB
  • HELP

6.1 子命令 CHANNELS

PUBSUB CHANNELS [pattern]
返回满足pattern规则的channel,其中channel是pubsub_channels哈希中实际的channel,不是pattern的channel,而且如果未指定pattern,则返回了所有channel。

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);
    } 

6.2 子命令 NUMPAT

返回系统中不重复的pattern channel的总个数。
直接获取hash中元素的个数,因此时间复杂度O(1)。

else if (!strcasecmp(c->argv[1]->ptr,"numpat") && c->argc == 2) {
        /* PUBSUB NUMPAT */
        addReplyLongLong(c,dictSize(server.pubsub_patterns));
    } 

6.3 子命令 NUMSUB

获取订阅了某个channel的client个数。
PUBSUB NUMSUB [channel [channel ...]]

 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);
        }
    } 

可以看出,没有指定channel是合法的请求,返回一个空数组。
根据参数指定的channel,从全局的pubsub_channels中查找channel,其中channel的值存储的是一个client的链表,所以直接获取链表的大小即可,如果没找到则返回0。每次的获取时间复杂度是O(1),但是这里有N个channel需要查找,所以命令执行的总时间复杂度O(N)。

6.4 子命令 HELP

此子命令是在6.2.0版本中加入的,只是输出子命令的用法信息。

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: '*').",
"NUMPAT",
"    Return number of subscriptions to patterns.",
"NUMSUB [<channel> ...]",
"    Return the number of subscribers for the specified channels, excluding",
"    pattern subscriptions(default: no channels).",
NULL
        };
        addReplyHelp(c, help);
    } 

6.5实际操作

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值