redis's mass attention to the public (publish and subscribe mode)

Posted by teguh123 on Sun, 05 Dec 2021 06:29:42 +0100

1, Introduction

The last official account official subscribe only subscribes to a channel with a name, for example, I can not remember the full name of a official account. I only remember that redis in the official account name can be subscribed by fuzzy matching. *redis* can subscribe to all public numbers including redis.
redis can subscribe to multiple channel s through fuzzy matching rules.
Fuzzy matching rules:

  • 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

These rules are similar to regular expressions, but there are few kinds,? Match a character, * match any character, and [list] match a character in a list. see https://redis.io/commands/psubscribe These are the rules introduced by the official command. However, in the source code, these rules are not judged during the processing of the psubscribe command, and more rules are supported in the matching function. I don't know whether the official website command is not updated or I missed some details when looking at the code.

Two, bulk subscription (bulk subscription to official account).

Use the psubscribe command to subscribe to channel s in batches, and the redis server uses the psubscribeCommand function to process.
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 exclude client s that cannot execute subscription

The first step is still to exclude non blocking client s, but special treatment is made for 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 subscription processing

Process each pattern one by one.

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

2.2.1 judge whether the same pattern already exists

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

	//Search for the same pattern from the linked list of the client
    if (listSearchKey(c->pubsub_patterns,pattern) == NULL) {
    ....

The search process is to traverse each item in the linked list and call back the match function for matching. If it matches, the corresponding node will be returned. Otherwise, continue to the next item. If there is no match, NULL will be returned, indicating that there are no same items in the list.

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

For this match function, it is set when the client establishes the connection for the first time.

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

2.2.2 add pattern to the linked list of the current client

If this pattern is not found in the linked list of this client, it is the first subscription. It is added to the linked list. Increasing the reference count is for subsequent sharing in the global hash table.

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

2.2.3 add pattern to the global hash table

If it is the first time to add to the client's linked list, query whether it exists in the global hash table. If it does not exist, add it to the global hash table.

 /* 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 add client to linked list

For each item in the global hash table, the key is pattern, and the value is the client object.

	listAddNodeTail(clients,c);

2.2.5 notify 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 setting subscription mode

 c->flags |= CLIENT_PUBSUB;

3, Publish news (update articles simultaneously for multiple accounts)

PUBLISH channel message
The publish command is still used to publish messages, but when selecting the client when sending the message channel, you need to traverse all pattern s.

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 sending messages to client s whose channel s are completely equal

From server.pubsub_ The channels obtained in channels are all full names, that is, completely equal channels, before sending messages to these client s.

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 send a message to the client that meets the pattern

This paper focuses on this situation.

3.2.1 traverse the global hash table

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

3.2.2 use pattern to match channel s one by one

//Traversing hash table entries
 while((de = dictNext(di)) != NULL) {
	//Get the list of pattern and client
    robj *pattern = dictGetKey(de);
    list *clients = dictGetVal(de);

	//Use pattern to match the channel. If there is no match, start the processing of the next item
    if (!stringmatchlen((char*)pattern->ptr,
                        sdslen(pattern->ptr),
                        (char*)channel->ptr,
                        sdslen(channel->ptr),0)) continue;
	
	...
}

The following is the matching processing function. When there is a *, the recursive backtracking operation will be carried out. Therefore, do not easily use * for fuzzy matching. Frequent operations will affect the performance

int stringmatchlen(const char *pattern, int patternLen,
        const char *string, int stringLen, int nocase)
{
    while(patternLen && stringLen) {
        switch(pattern[0]) {
        case '*':
        	//Skip continuous*
            while (patternLen && pattern[1] == '*') {
                pattern++;
                patternLen--;
            }
            //If there is only one *, the match is on.
            if (patternLen == 1)
                return 1; /* match */
			//Start recursive operation
            while(stringLen) {
                if (stringmatchlen(pattern+1, patternLen-1,
                            string, stringLen, nocase))
                    return 1; /* match */
                string++;
                stringLen--;
            }
            return 0; /* no match */
            break;
        case '?': //Match any character, so skip one character directly
            string++;
            stringLen--;
            break;
        case '[': //List start
        {
            int not, match;

            pattern++;
            patternLen--;
            not = pattern[0] == '^'; //Judge whether it is a character in the exclusion list
            if (not) { //Is the character in the exclusion list and moves to the next character of pattern
                pattern++;
                patternLen--;
            }
            match = 0;
            while(1) {
            	//Escape processing
                if (pattern[0] == '\\' && patternLen >= 2) {
                    pattern++;
                    patternLen--;
                    if (pattern[0] == string[0])
                        match = 1;
                 
                 // End of list      
                } else if (pattern[0] == ']') {
                    break;
                } else if (patternLen == 0) {//When the pattern ends, it feels like an error
                    pattern--;
                    patternLen++;
                    break;

				//Range
                } else if (patternLen >= 3 && pattern[1] == '-') {
                	//Get start stop range
                    int start = pattern[0];
                    int end = pattern[2];
                    int c = string[0];
                    if (start > end) {
                        int t = start;
                        start = end;
                        end = t;
                    }
                    //If case insensitive, they are converted to lowercase
                    if (nocase) {
                        start = tolower(start);
                        end = tolower(end);
                        c = tolower(c);
                    }
                    pattern += 2;
                    patternLen -= 2;
                    //Judge whether it is within the range
                    if (c >= start && c <= end)
                        match = 1;
                } else {
                    if (!nocase) {//Case sensitive, direct comparison
                        if (pattern[0] == string[0])
                            match = 1;
                    } else { //Case insensitive, all converted to lowercase for comparison
                        if (tolower((int)pattern[0]) == tolower((int)string[0]))
                            match = 1;
                    }
                }
                pattern++;
                patternLen--;
            }
            if (not) //Exclude from list
                match = !match;
            if (!match)
                return 0; /* no match */
            string++;
            stringLen--;
            break;
        }
        case '\\': //Escape character, skip escape character
            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) {
        	//If the string ends and the pattern has *, skip*
            while(*pattern == '*') {
                pattern++;
                patternLen--;
            }
            break;
        }
    }
    //If both pattern and string end, the matching is successful
    if (patternLen == 0 && stringLen == 0)
        return 1;
    return 0;
}

3.2.3 send a message to the client queue whose pattern matches successfully

The matching pattern stores all clients subscribed to the pattern channel, so traverse the entire linked list and send messages to each client.

//If the pattern matches, traverse the client linked list and send messages one by one
    listRewind(clients,&li);
    while ((ln = listNext(&li)) != NULL) {
        client *c = listNodeValue(ln);
        addReplyPubsubPatMessage(c,pattern,channel,message);
        receivers++;
    }

4, Bulk unsubscribe (unsubscribe)

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

Different operations are performed according to the number of parameters. If there are no parameters, the channels of all patterns subscribed by the current client will be cancelled. Otherwise, the channels that only process the patterns specified by the parameters will be cancelled.

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

The pubsubUnsubscribePattern function is still called when all pattern channel s are cleared, so we focus on the pubsubUnsubscribePattern function.

/* 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 delete this pattern channel from the linked list of the client

If you can use PubSub from this client_ If this channel is found in the patterns linked list, it will be deleted from the linked list.

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 delete the client from the linked list of the pattern channel of the global hash

Delete this client from the client linked list in the hash. When the client linked list is empty, delete this hash item from the hash table.

/* 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 notify 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));
}

5, Contrast

psubscribe, pununsubscribe, subscribe and unsubscribe are actually the same logic, except for different storage places of channel s and different de duplication methods.

commandfunctionclient storage modeclient de duplication methodDe duplication / lookup time complexity
SUBSCRIBE channel [channel ...]Subscription messagehashhashO(1)
UNSUBSCRIBE [channel [channel ...]]UnsubscribeO(1)
PSUBSCRIBE pattern [pattern ...]pattern subscriptionlistErgodic comparisonO(N)
PUNSUBSCRIBE [pattern [pattern ...]]UnsubscribeO(N)

Moreover, pattern only plays a role in the publish command. Other times, it is treated as a simple string, such as de duplication.
The channel s stored in the server are only different variables, but they are of the same hash type and have the same structure.

5.1 structural drawing

For example, client1 subscribes to pattern1 and client2 subscribes to pattern1 and pattern2.

6, Get some information about channel

In redis 2.8.0, the pubsub command is added to obtain some channel information.

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

There are four subcommands:

  • CHANNELS
  • NUMPAT
  • NUMSUB
  • HELP

6.1 subcommand CHANNELS

PUBSUB CHANNELS [pattern]
Returns the channel that satisfies the pattern rule, where channel is PubSub_ The actual channel in the channels hash is not the channel of the pattern, and if the pattern is not specified, all channels are returned.

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 subcommand NUMPAT

Returns the total number of non duplicate pattern channel s in the system.
Directly obtain the number of elements in the hash, so the time complexity is O(1).

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

6.3 subcommand NUMSUB

Gets the number of client s subscribed to a channel.
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);
        }
    } 

It can be seen that no channel specified is a legal request, and an empty array is returned.
According to the channel specified by the parameter, from the global PubSub_ Find channel in channels. The value of channel stores a linked list of client s, so you can directly obtain the size of the linked list. If it is not found, it returns 0. The time complexity of each acquisition is O(1), but there are N channels to find, so the total time complexity of command execution is O(N).

6.4 subcommand HELP

This subcommand is added in version 6.2.0. It only outputs the usage information of the subcommand.

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 actual operation

Topics: Redis