redis简单地实现了订阅发布功能。
pubsub涉及到的结构主要是下面两个:
typedef struct redisClient {
...
dict *pubsub_channels; //该client订阅的channels,以channel为key用dict的方式组织
list *pubsub_patterns; //该client订阅的pattern,以list的方式组织
...
} redisClient;
struct redisServer {
...
dict *pubsub_channels; //redis server进程中维护的channel dict,它以channel为key,订阅channel的client list为value
list *pubsub_patterns; //redis server进程中维护的pattern list
int notify_keyspace_events;
...
};
没搞懂的是在redisClient中,为什么channel和pattern一个用dict一个用list?
对应的command:
struct redisCommand redisCommandTable[] = {
...
{"subscribe", subscribeCommand, -2,"rpslt",0,NULL,0,0,0,0,0}, //channel订阅命令
{"unsubscribe", unsubscribeCommand, -1,"rpslt",0,NULL,0,0,0,0,0}, //channel退订命令
{"psubscribe", psubscribeCommand, -2,"rpslt",0,NULL,0,0,0,0,0}, //pattern订阅命令
{"punsubscribe", punsubscribeCommand, -1,"rpslt",0,NULL,0,0,0,0,0}, //pattern退订命令
{"publish", publishCommand, 3,"pltrF",0,NULL,0,0,0,0,0}, //消息发布命令
{"pubsub", pubsubCommand, -2,"pltrR",0,NULL,0,0,0,0,0}, //pubsub命令,用于输出channel相关的统计信息
...
}
pattern的匹配,里面调用的equalStringObjects就是redis实现的正则匹配:
int listMatchPubsubPattern(void *a, void *b) {
pubsubPattern *pa = a, *pb = b;
return (pa->client == pb->client) &&
(equalStringObjects(pa->pattern,pb->pattern));
}
订阅某个channel的核心操作
int pubsubSubscribeChannel(redisClient *c, robj *channel) {
dictEntry *de;
list *clients = NULL;
int retval = 0;
/* Add the channel to the client -> channels hash table */
/* 以channel为key加到c->pubsub_channels当中 */
if (dictAdd(c->pubsub_channels,channel,NULL) == DICT_OK) {
retval = 1;
incrRefCount(channel);
/* Add the client to the channel -> list of clients hash table */
/* server.pubsub_channels记录了所有被订阅的channel以及订阅特定channel的clients, 这里把c加到该channel对应的列表当中 */
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;
}
退订某个channel的核心操作
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 */
/* 为什么需要保护起来?
* 考虑到redis本身是单进程单线程的,所以这里不是为了防止在别的地方被free掉
*/
incrRefCount(channel); /* channel may be just a pointer to the same object
we have in the hash tables. Protect it... */
/* 从该client的订阅dict中移除该channel */
if (dictDelete(c->pubsub_channels,channel) == DICT_OK) {
retval = 1;
/* Remove the client from the channel -> clients list hash table */
/* 从server.pubsub_channels中该channel维护的client列表上删除该client */
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);
/* 如果该channel上没有其它订阅者,则从server.pubsub_channels上删除该channel */
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;
}
订阅/退订pattern的操作也很类似,就不贴代码了。
它还提供了pubsubUnsubscribeAllChannels和pubsubUnsubscribeAllPatterns,用于一次性退订所有的channels/patters。实现上就是循环调用相应的退订函数。
发布消息的核心操作
int pubsubPublishMessage(robj *channel, robj *message) {
int receivers = 0;
dictEntry *de;
listNode *ln;
listIter li;
/* Send to clients listening for that channel */
/* 先找到该channel */
de = dictFind(server.pubsub_channels,channel);
if (de) {
list *list = dictGetVal(de);
listNode *ln;
listIter li;
listRewind(list,&li);
/* 对所有订阅该频道的client发送消息 */
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 */
/* 如果pattern也有client在订阅,那么还要进行模式的匹配并发送消息给相应的client */
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++;
}
}
/* 释放getDecodedObject返回的对象 */
decrRefCount(channel);
}
return receivers;
}
有了上面的几个核心操作,subscribe/unsubscribe, psubscribe/punsubscribe, publish几个操作基本上就是直接调用上面的函数,这里就不贴代码了。
有一点提一下:发布/订阅跟具体的db无关,只跟client和具体的channel/pattern有关。
redis还提供一个pubsub命令,用于输出当前订阅的总体情况:
void pubsubCommand(redisClient *c) {
if (!strcasecmp(c->argv[1]->ptr,"channels") &&
(c->argc == 2 || c->argc ==3))
{
/* PUBSUB CHANNELS [<pattern>] */
/* 如果没有指定pattern,则输出所有channel的信息
* 如果指定了pattern,则输出跟pattern匹配的所有channel信息
*/
sds pat = (c->argc == 2) ? NULL : c->argv[2]->ptr;
dictIterator *di = dictGetIterator(server.pubsub_channels);
dictEntry *de;
long mblen = 0;
void *replylen;
replylen = addDeferredMultiBulkLength(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);
setDeferredMultiBulkLength(c,replylen,mblen);
} else if (!strcasecmp(c->argv[1]->ptr,"numsub") && c->argc >= 2) {
/* PUBSUB NUMSUB [Channel_1 ... Channel_N] */
int j;
/* 输出指定channel上client的数量 */
addReplyMultiBulkLen(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);
}
} else if (!strcasecmp(c->argv[1]->ptr,"numpat") && c->argc == 2) {
/* PUBSUB NUMPAT */
/* 输出pattern的数量 */
addReplyLongLong(c,listLength(server.pubsub_patterns));
} else {
addReplyErrorFormat(c,
"Unknown PUBSUB subcommand or wrong number of arguments for '%s'",
(char*)c->argv[1]->ptr);
}
}