Redis的发布与订阅功能,由SUBSCRIBE,PSUBSCRIBE,UNSUBSCRIBE,PUNSUBSCRIBE,以及PUBLISH等命令实现。
通过执行SUBSCRIBE命令,客户端可以订阅一个或多个频道。当有客户端通过PUBLISH命令向某个频道发布消息时,频道的所有订阅者都会收到这条消息。
除了订阅具体的频道之外,客户端还可以通过执行PSUBSCRIBE命令订阅一个或多个频道模式。当有客户端通过PUBLISH命令向某个频道发布消息时,消息不仅会被发送给这个频道的所有订阅者,它还会发送给所有与这个频道相匹配的频道模式的订阅者。
UNSUBSCRIBE和PUNSUBSCRIBE命令,主要用于退订频道和退订频道模式。
一:订阅
1:数据结构
SUBSCRIBE命令主要使用字典结构。
在表示Redis服务器的redisServer结构体中,使用字典pubsub_channels记录某个频道都由哪些客户端订阅。
struct redisServer {
...
/* Pubsub */
dict *pubsub_channels; /* Map channels to list of subscribed clients */
...
}
在字典pubsub_channels中,以具体的频道名为key,而value是一个列表,该列表中记录了订阅该频道的所有客户端。
当向某频道发布消息时,就是通过查询该字典,将消息发送给订阅该频道的所有客户端。
在表示客户端的结构体redisClient中,使用字典pubsub_channels记录该客户端都订阅了哪些频道:
typedef struct redisClient {
...
dict *pubsub_channels; /* channels a client is interested in (SUBSCRIBE) */
...
} redisClient;
在字典pubsub_channels中,以具体的频道名为key,而value为NULL。因此使用该字典能够快速判断客户端是否订阅了某频道。
PSUBSCRIBE命令主要使用列表结构。
在表示Redis服务器的redisServer结构体中,使用列表pubsub_patterns记录客户端及其订阅的频道模式。列表中的元素都是pubsubPattern结构:
struct redisServer {
...
/* Pubsub */
list *pubsub_patterns; /* A list of pubsub_patterns */
...
};
typedef struct pubsubPattern {
redisClient *client;
robj *pattern;
} pubsubPattern;
当向某频道发布消息时,就是通过查询该列表,将消息发送给订阅了与该频道相匹配的频道模式的所有客户端。
在表示客户端的结构体redisClient中,使用列表pubsub_patterns记录该客户端都订阅了哪些频道模式,列表中的元素就是频道模式名:
typedef struct redisClient {
...
list *pubsub_patterns; /* patterns a client is interested in (PSUBSCRIBE) */
...
} redisClient;
2:SUBSCRIBE命令
当客户端发来SUBSCRIBE命令之后,该命令的处理函数是subscribeCommand,代码如下:
void subscribeCommand(redisClient *c) {
int j;
for (j = 1; j < c->argc; j++)
pubsubSubscribeChannel(c,c->argv[j]);
c->flags |= REDIS_PUBSUB;
}
代码很简单,针对命令参数中的每一个channel,调用函数pubsubSubscribeChannel,将客户端c和该channel,记录到相应数据结构中。
最后,向客户端标志位中增加REDIS_PUBSUB标记,表示该客户端进入订阅模式;
函数pubsubSubscribeChannel的代码如下:
int pubsubSubscribeChannel(redisClient *c, robj *channel) {
dictEntry *de;
list *clients = NULL;
int retval = 0;
/* Add the channel to the client -> channels hash table */
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) {
clients = listCreate();
dictAdd(server.pubsub_channels,channel,clients);
incrRefCount(channel);
} else {
clients = dictGetVal(de);
}
listAddNodeTail(clients,c);
}
/* Notify the client */
addReply(c,shared.mbulkhdr[3]);
addReply(c,shared.subscribebulk);
addReplyBulk(c,channel);
addReplyLongLong(c,clientSubscriptionsCount(c));
return retval;
}
首先向字典c->pubsub_channels中添加以该channel为key的键值对,键值对中的value为NULL。因此,该字典仅用于记录当前客户端订阅了哪些频道;
然后,在字典server.pubsub_channels中,以channel为key寻找字典项de,如果de为NULL,则创建列表clients,以channel为key,以列表clients为value,将该键值对添加到字典server.pubsub_channels中;如果de不为NULL,则从中取出当前已订阅该channel的客户端列表clients;然后,将客户端c添加到列表clients中;
最后,回复客户端c相应的订阅信息;
3:PSUBSCRIBE命令
当客户端发来PSUBSCRIBE命令之后,该命令的处理函数是psubscribeCommand,代码如下:
void psubscribeCommand(redisClient *c) {
int j;
for (j = 1; j < c->argc; j++)
pubsubSubscribePattern(c,c->argv[j]);
c->flags |= REDIS_PUBSUB;
}
代码很简单,针对命令参数中的每一个频道模式,调用函数pubsubSubscribePattern,将客户端c和该频道模式,记录到相应数据结构中。
最后,向客户端标志位中增加REDIS_PUBSUB标记,表示该客户端进入订阅模式;
函数pubsubSubscribePattern的代码如下:
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,clientSubscriptionsCount(c));
return retval;
}
根据pattern,从列表c->pubsub_patterns中寻找相应的节点,如果找不到,说明该客户端未订阅该模式,因此:首先将pattern追加到列表c->pubsub_patterns中;然后根据c和pattern,创建pubsubPattern结构的pat,并将pat追加到列表server.pubsub_patterns中;
最后,回复客户端相应信息;
4:订阅模式
当客户端标志位中设置了REDIS_PUBSUB标记后,表示该客户端进入订阅模式。
在处理客户端命令的processCommand函数中,有下面的逻辑:
/* Only allow SUBSCRIBE and UNSUBSCRIBE in the context of Pub/Sub */
if (c->flags & REDIS_PUBSUB &&
c->cmd->proc != pingCommand &&
c->cmd->proc != subscribeCommand &&
c->cmd->proc != unsubscribeCommand &&
c->cmd->proc != psubscribeCommand &&
c->cmd->proc != punsubscribeCommand) {
addReplyError(c,"only (P)SUBSCRIBE / (P)UNSUBSCRIBE / QUIT allowed in this context");
return REDIS_OK;
}
因此,处于订阅模式下的客户端,只能向Redis服务器发送PING、SUBSCRIBE、UNSUBSCRIBE、PSUBSCRIBE、PUNSUBSCRIBE命令。
二:发布
当客户端向Redis服务器发送”PUBLISH <channel> <message>”命令后,Redis服务器会将消息<message>发送给所有订阅了<channel>的客户端,以及那些订阅了与<channel>相匹配的频道模式的客户端。
PUBLISH命令的处理函数是publishCommand,代码如下:
void publishCommand(redisClient *c) {
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);
addReplyLongLong(c,receivers);
}
函数中,首先调用pubsubPublishMessage函数,将message发布到相应的频道;
然后,如果当前Redis处于集群模式下,则调用clusterPropagatePublish函数,向所有集群节点广播该消息;否则,调用forceCommandPropagation函数,向客户端c的标志位中增加REDIS_FORCE_REPL标记,以便后续能将该PUBLISH命令传递给从节点;
最后,将接收消息的客户端个数回复给客户端c;
发布消息的功能,主要是通过pubsubPublishMessage函数实现的,该函数的代码如下:
int pubsubPublishMessage(robj *channel, robj *message) {
int receivers = 0;
dictEntry *de;
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) {
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;
}
代码很简单,首先根据channel,在字典server.pubsub_channels中查找订阅了该频道的客户端列表list;针对列表中的每个客户端,向其发送message消息;
然后,轮训列表server.pubsub_patterns,针对列表中的每一个pat,如果channel与pat->pattern相匹配,则向pat->client发送message消息;
三:退订
1:UNSUBSCRIBE命令
客户端发送UNSUBSCRIBE命令,用于退订之前通过SUBSCRIBE命令订阅的频道。UNSUBSCRIBE命令的处理函数是unsubscribeCommand,代码如下:
void unsubscribeCommand(redisClient *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 &= ~REDIS_PUBSUB;
}
如果该命令没有任何参数,则调用pubsubUnsubscribeAllChannels函数,设置该客户端c退订所有频道;
否则,针对命令参数中的每一个channel,调用pubsubUnsubscribeChannel,使客户端退订相应频道;
最后,如果当前该客户端c订阅的频道和频道模式数为0,则从客户端标志位中删除REDIS_PUBSUB标记,表示客户端退出订阅模式;
pubsubUnsubscribeAllChannels函数用于客户端退订所有频道,该函数的代码如下:
int pubsubUnsubscribeAllChannels(redisClient *c, int notify) {
dictIterator *di = dictGetSafeIterator(c->pubsub_channels);
dictEntry *de;
int count = 0;
while((de = dictNext(di)) != NULL) {
robj *channel = dictGetKey(de);
count += pubsubUnsubscribeChannel(c,channel,notify);
}
/* We were subscribed to nothing? Still reply to the client. */
if (notify && count == 0) {
addReply(c,shared.mbulkhdr[3]);
addReply(c,shared.unsubscribebulk);
addReply(c,shared.nullbulk);
addReplyLongLong(c,dictSize(c->pubsub_channels)+
listLength(c->pubsub_patterns));
}
dictReleaseIterator(di);
return count;
}
轮训字典c->pubsub_channels,针对其中的每一个channel,调用函数pubsubUnsubscribeChannel,使客户端退订该频道,并将退订的频道数记录到count中;
最后,如果notify非0,并且count为0,则回复客户端相应信息;
pubsubUnsubscribeChannel函数用于客户端退订单个频道,该函数的代码如下:
int pubsubUnsubscribeChannel(redisClient *c, robj *channel, int notify) {
dictEntry *de;
list *clients;
listNode *ln;
int retval = 0;
/* Remove the channel from the client -> channels hash table */
incrRefCount(channel); /* channel may be just a pointer to the same object
we have in the hash tables. Protect it... */
if (dictDelete(c->pubsub_channels,channel) == DICT_OK) {
retval = 1;
/* Remove the client from the channel -> clients list hash table */
de = dictFind(server.pubsub_channels,channel);
redisAssertWithInfo(c,NULL,de != NULL);
clients = dictGetVal(de);
ln = listSearchKey(clients,c);
redisAssertWithInfo(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);
}
}
/* Notify the client */
if (notify) {
addReply(c,shared.mbulkhdr[3]);
addReply(c,shared.unsubscribebulk);
addReplyBulk(c,channel);
addReplyLongLong(c,dictSize(c->pubsub_channels)+
listLength(c->pubsub_patterns));
}
decrRefCount(channel); /* it is finally safe to release it */
return retval;
}
首先从字典c->pubsub_channels中,删除以channel为key的字典项;
然后在字典server.pubsub_channels中,寻找以channel为key的字典项de,并从de中得到订阅该channel的客户端列表clients,然后在列表clients中找到客户端c对应的元素ln,将ln从clients中删除;如果clients长度为0,则从字典server.pubsub_channels中删除该channel;
最后,如果参数notify非0,则回复客户端相应信息;
2:PUNSUBSCRIBE命令
客户端发送PUNSUBSCRIBE命令,用于退订之前通过PSUBSCRIBE命令订阅的频道模式。PUNSUBSCRIBE命令的处理函数是punsubscribeCommand,代码如下:
void punsubscribeCommand(redisClient *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 &= ~REDIS_PUBSUB;
}
如果该命令没有任何参数,则调用pubsubUnsubscribeAllPatterns函数,设置该客户端c退订所有频道模式;
否则,针对命令参数中的每一个pattern,调用pubsubUnsubscribePattern,使客户端退订相应频道模式;
最后,如果该客户端c订阅的频道和频道模式数为0,则从客户端标志位中删除REDIS_PUBSUB标记,表示客户端退出订阅模式;
pubsubUnsubscribeAllPatterns函数用于客户端退订所有频道模式,该函数的代码如下:
int pubsubUnsubscribeAllPatterns(redisClient *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) {
/* We were subscribed to nothing? Still reply to the client. */
addReply(c,shared.mbulkhdr[3]);
addReply(c,shared.punsubscribebulk);
addReply(c,shared.nullbulk);
addReplyLongLong(c,dictSize(c->pubsub_channels)+
listLength(c->pubsub_patterns));
}
return count;
}
轮训列表c->pubsub_patterns,针对其中的每一个pattern,调用函数pubsubUnsubscribePattern使客户端退订该频道模式,并将退订的频道模式数记录到count中;
最后,如果notify非0,并且count为0,则回复客户端相应信息;
pubsubUnsubscribePattern函数用于客户端退订单个频道模式,该函数的代码如下:
int pubsubUnsubscribePattern(redisClient *c, robj *pattern, int notify) {
listNode *ln;
pubsubPattern pat;
int retval = 0;
incrRefCount(pattern); /* Protect the object. May be the same we remove */
if ((ln = listSearchKey(c->pubsub_patterns,pattern)) != NULL) {
retval = 1;
listDelNode(c->pubsub_patterns,ln);
pat.client = c;
pat.pattern = pattern;
ln = listSearchKey(server.pubsub_patterns,&pat);
listDelNode(server.pubsub_patterns,ln);
}
/* Notify the client */
if (notify) {
addReply(c,shared.mbulkhdr[3]);
addReply(c,shared.punsubscribebulk);
addReplyBulk(c,pattern);
addReplyLongLong(c,dictSize(c->pubsub_channels)+
listLength(c->pubsub_patterns));
}
decrRefCount(pattern);
return retval;
}
首先,根据pattern,在列表c->pubsub_patterns中寻找相应的节点ln;然后将ln从列表c->pubsub_patterns中删除;
然后,根据c和pattern,在列表server.pubsub_patterns中寻找相应的节点ln,然后将ln从列表server.pubsub_patterns中删除;
最后,如果参数notify非0,回复客户端相应信息;