The official account of redis (publish subscribe)

Keywords: Redis

1, Introduction

Official account is official account. Official account is like subscription. When the official account is released, the message will be sent to all users who are concerned about the public number. Users will be able to see the new articles.
The publish subscription of redis is similar. A user subscribes to a channel through subscribe. When a user publishes a message to the corresponding channel through publish, all users who subscribe to this channel will receive this message.

Source file pubsub.c, redis6.2.6

Two, subscribe to messages (official account)

Subscriptions are mainly set through the subscribe command. The redis server will use the subscribeCommand function for processing.

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

Once the client executes this command, it will enter the subscription mode. In the future, it can only execute subscribe, psubscribe, unsubscribe, punsubsribe, Ping, RESET and quit commands. Other commands cannot be executed. You can exit this mode through the RESET command, so clients who want a response to each command cannot execute this command.

2.1 first eliminate the blocked client

For clients_ DENY_ Clients with blocking flag. These clients expect each command to have response data, so they cannot execute this command. However, for backward compatibility, special processing is made for the MULTI command.

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, "SUBSCRIBE isn't allowed for a DENY BLOCKING client");
        return;
    }

2.2 subscription

Traverse each channel, perform the actual subscription operation, and reply to the client's subscription information.

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

2.2.1 add channel to client subscription

Insert the subscribed channel into the client's hash table PubSub_ In channels, if it already exists, the addition fails and returns DICT_ERR, continue the follow-up operation.

/* Add the channel to the client -> channels hash table */
    if (dictAdd(c->pubsub_channels,channel,NULL) == DICT_OK) {
    ...
    }

2.2.2 adding channels to global channels

When the channel is successfully added to the subscription hash of the client, it indicates that the channel is newly subscribed by the client, and then judge whether the channel exists in the hash of the global channel. If it does not exist, it is added, and the corresponding value of the channel object of the global hash is a linked list, which stores all clients subscribing to the current channel. You can see that the channel object added to the client is the same as the channel object in the global hash, saving space.

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

2.2.3 adding a client to the channel queue

All clients subscribing to the same channel are saved in a two-way linked list. When publishing messages, you can quickly find all clients according to the channel.

 listAddNodeTail(clients,c);

2.2.4 notify client

The commands used by the client, the subscribed channels, and the total number of channels subscribed by the current client will be replied.

 /* Notify the client */
    addReplyPubsubSubscribed(c,channel);
/* Send the pubsub subscription notification to the client. */
void addReplyPubsubSubscribed(client *c, robj *channel) {
    if (c->resp == 2)
        addReply(c,shared.mbulkhdr[3]);
    else
        addReplyPushLen(c,3);
    addReply(c,shared.subscribebulk);
    addReplyBulk(c,channel);
    addReplyLongLong(c,clientSubscriptionsCount(c));
}
/* Return the number of channels + patterns a client is subscribed to. */
int clientSubscriptionsCount(client *c) {
    return dictSize(c->pubsub_channels)+
           listLength(c->pubsub_patterns);
}

2.3 setting the client publish subscribe flag

c->flags |= CLIENT_PUBSUB;

2.3 actual operation

  • Connect using redis cli
    It can be seen that after directly using the subscribe command, it is blocked, waiting for the data of the channel to be read, and the operation cannot continue.
# ./redis-cli 
127.0.0.1:6379> subscribe mychannel
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "mychannel"
3) (integer) 1

  • Log in using telnet
    You can see that the client entering the publish subscribe mode can only execute those commands.
$ telnet 127.0.0.1 6379
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
subscribe mychannel mychannel2
*3
$9
subscribe
$9
mychannel
:1
*3
$9
subscribe
$10
mychannel2
:2
get name
-ERR Can't execute 'get': only (P)SUBSCRIBE / (P)UNSUBSCRIBE / PING / QUIT / RESET are allowed in this context

int processCommand(client *c) {
...
/* Only allow a subset of commands in the context of Pub/Sub if the
     * connection is in RESP2 mode. With RESP3 there are no limits. */
    if ((c->flags & CLIENT_PUBSUB && c->resp == 2) &&
        c->cmd->proc != pingCommand &&
        c->cmd->proc != subscribeCommand &&
        c->cmd->proc != unsubscribeCommand &&
        c->cmd->proc != psubscribeCommand &&
        c->cmd->proc != punsubscribeCommand &&
        c->cmd->proc != resetCommand) {
        rejectCommandFormat(c,
            "Can't execute '%s': only (P)SUBSCRIBE / "
            "(P)UNSUBSCRIBE / PING / QUIT / RESET are allowed in this context",
            c->cmd->name);
        return C_OK;
    }
    ...
}

Three, release the news (official account update)

Write data to a channel through the publish command. The redis server will process the data through the publishCommand function and send the data to the client s subscribed to the channel.

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 data to channels

/* PUBLISH <channel> <message> */
void publishCommand(client *c) {
    int receivers = pubsubPublishMessage(c->argv[1],c->argv[2]);
   	...
}

3.1.1 find the channel object from the global hash

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

3.1.2 traversing the client queue

If the channel object is found, get the client linked list from the object, then traverse the linked list and send messages one by one.

if (de) {
        list *list = dictGetVal(de);
        listNode *ln;
        listIter li;

        listRewind(list,&li);
        while ((ln = listNext(&li)) != NULL) {
          ...
        }
    }

3.1.3 send messages one by one

...		
while ((ln = listNext(&li)) != NULL) {
	client *c = ln->value;
	addReplyPubsubMessage(c,channel,message);
	receivers++;
}
...
void addReplyPubsubMessage(client *c, robj *channel, robj *msg) {
    if (c->resp == 2)
        addReply(c,shared.mbulkhdr[3]);
    else
        addReplyPushLen(c,3);
    addReply(c,shared.messagebulk);
    addReplyBulk(c,channel);
    if (msg) addReplyBulk(c,msg);
}

3.2 data dissemination

I haven't sorted out the details yet. I'll look at them later.

...
 if (server.cluster_enabled)
        clusterPropagatePublish(c->argv[1],c->argv[2]);
    else
        forceCommandPropagation(c,PROPAGATE_REPL);
...

3.3 client responding to published data

How many client s that subscribe to this channel have received data in response to the published data.

addReplyLongLong(c,receivers);

3.4 actual operation

Four, cancel subscription (cancel official account)

When you no longer care about channel data, you can unsubscribe. Unsubscribe through the unsubscribe command, and the redis server processes through the unsubscribeCommand function.

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

It can be seen from the processing function that when no cancelled channel is specified, all channels will be cancelled.

/* UNSUBSCRIBE [channel [channel ...]] */
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;
}

4.1 delete channel from hash in client

int pubsubUnsubscribeChannel(client *c, robj *channel, int notify) {
...
	if (dictDelete(c->pubsub_channels,channel) == DICT_OK) {
	...

4.2 delete the current client from the global linked list

Delete the current client from the linked list of global hash channel s. If the entire linked list is empty, this hash item will be deleted from the global hash table.

	retval = 1;
       /* Remove the client from the channel -> clients list hash table */
       de = dictFind(server.pubsub_channels,channel);
       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, so that it will be possible to abuse
            * Redis PUBSUB creating millions of channels. */
           dictDelete(server.pubsub_channels,channel);
       }

4.3 notify client

It is the same as notifying the client during subscription, except that the returned commands are different.

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

4.4 actual operation

If no cancellation channel is specified, all subscriptions are cancelled.

Specify to delete a channel. You can see that it has changed from 3 channels to 2 channels by returning.

5, Organizational structure

client1 and client2 subscribe to channel1 respectively, and client2 also subscribes to channel2. The whole organizational structure is shown in the figure below.

Posted by mariolopes on Thu, 02 Dec 2021 13:57:10 -0800