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