一、订阅频道/模式
1.1 命令
Redis的订阅功能由SUBSCREBE、PSUBSCRIBE等命令组成。
SUBSCRIBE < channel >:客户端订阅一个或多个频道,成为频道订阅者subscriber,当有其他客户端向频道发送消息message时,频道的所有订阅者都会收到消息。
PSUBSCRIBE < patterns >:客户端订阅一个或多个模式,成为模式订阅者,模式可以匹配多个频道。
1.2 订阅关系的存储结构
Redis将所有频道和模式的订阅关系分别保存在pubsub_channels和pubsub_patterns中。
这两个字典的键是某个被订阅的频道/模式,键的值是链表,链表的节点时所有订阅该key中频道/模式的客户端
见server.h的redisServer
struct redisServer {
// ...
/* Pubsub */
dict *pubsub_channels; /* Map channels to list of subscribed clients */
list *pubsub_patterns; /* A list of pubsub_patterns */
// ...
}
1.3 订阅流程
每当客户端执行SUBSCRIBE/PSUBSCRIBE时,服务器都会将客户端与被订阅的频道在pubsub_channels/pubsub_patterns字段中进行关联。
如果频道已有其他订阅者,则在链表尾部增加一个订阅者;
如果频道无其他订阅者,将在字典中创建一个键,设置值为空链表,再将客户端添加到链表。
见pubsub.c的pubsubSubscribeChannel和pubsubSubscribePattern
void subscribeCommand(client *c) {
int j;
for (j = 1; j < c->argc; j++)
pubsubSubscribeChannel(c,c->argv[j]);
c->flags |= CLIENT_PUBSUB;
}
/* Subscribe a client to a channel. Returns 1 if the operation succeeded, or
* 0 if the client was already subscribed to that channel. */
int pubsubSubscribeChannel(client *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 */
addReplyPubsubSubscribed(c,channel);
return retval;
}
/* Subscribe a client to a pattern. Returns 1 if the operation succeeded, or 0 if the client was already subscribed to that pattern. */
int pubsubSubscribePattern(client *c, robj *pattern) {
dictEntry *de;
list *clients;
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);
/* Add the client to the pattern -> list of clients hash table */
de = dictFind(server.pubsub_patterns_dict,pattern);
if (de == NULL) {
clients = listCreate();
dictAdd(server.pubsub_patterns_dict,pattern,clients);
incrRefCount(pattern);
} else {
clients = dictGetVal(de);
}
listAddNodeTail(clients,c);
}
/* Notify the client */
addReplyPubsubPatSubscribed(c,pattern);
return retval;
}
1.4 退订流程
退订流程就是订阅流程的逆向操作。
每当客户端执行UNSUBSCRIBE/PUNSUBSCRIBE时,服务器都会将客户端与被订阅的频道在pubsub_channels/pubsub_patterns字段中进行关联。
如果频道已有其他订阅者,则从链表尾部删除该订阅者;
如果频道无其他订阅者,将字典中该频道/模式 设置值为空链表,删除该键。
见pubsub.c的unsubscribeCommand和punsubscribeCommand,根据参数判断全退订还是退订一个
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;
}
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;
}
二、发布消息
2.1 命令
PUBLISH < channel > < message >:向指定频道channel发布消息message。
2.1 发布流程
服务器会执行以下两个动作:
1)将消息message发送给频道channel所有的订阅者subscriber;
2)如果有一个或多个模式pattern与频道channel匹配,将消息发送给对应模式patterns下的订阅者subscriber。
见pubsub.c的publishCommand、pubsubPublishMessage
void publishCommand(client *c) {
int receivers = pubsubPublishMessage(c->argv[1],c->argv[2]);
if (server.cluster_enabled)
clusterPropagatePublish(c->argv[1],c->argv[2]);
else
forceCommandPropagation(c,PROPAGATE_REPL);
addReplyLongLong(c,receivers);
}
/* 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);
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++;
}
}
/* Send to clients listening to matching channels */
di = dictGetIterator(server.pubsub_patterns_dict);
if (di) {
channel = getDecodedObject(channel);
while((de = dictNext(di)) != NULL) {
robj *pattern = dictGetKey(de);
list *clients = dictGetVal(de);
if (!stringmatchlen((char*)pattern->ptr,
sdslen(pattern->ptr),
(char*)channel->ptr,
sdslen(channel->ptr),0)) continue;
listRewind(clients,&li);
while ((ln = listNext(&li)) != NULL) {
client *c = listNodeValue(ln);
addReplyPubsubPatMessage(c,pattern,channel,message);
receivers++;
}
}
decrRefCount(channel);
dictReleaseIterator(di);
}
return receivers;
}
三、查看订阅信息
3.1 命令
PUBSUB CHANNELS [pattern]:返回服务器当前被订阅的频道,pattern参数可选,选择时会返回与输入模式相匹配的频道
PUBSUB NUMSUB [channel-1 channel*2 ... channel-n]:接受任意多个频道作为输入参数,并返回这些频道的订阅者数量。
PUBSUB NUMPAT:返回服务器当前被订阅模式数量。
3.2 查看流程
见pubsub.c中的pubsubCommand,可见根据命令CHANNELS、NUMPAT、NUMSUB执行不同的逻辑,在字典中统计出订阅信息
/* PUBSUB command for Pub/Sub introspection. */
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: all).",
"NUMPAT -- Return number of subscriptions to patterns.",
"NUMSUB [channel-1 .. channel-N] -- Returns the number of subscribers for the specified channels (excluding patterns, default: none).",
NULL
};
addReplyHelp(c, help);
} 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);
} 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);
}
} else if (!strcasecmp(c->argv[1]->ptr,"numpat") && c->argc == 2) {
/* PUBSUB NUMPAT */
addReplyLongLong(c,listLength(server.pubsub_patterns));
} else {
addReplySubcommandSyntaxError(c);
}
}
总结
redis在pubsub_channels和pubsub_patterns 两个订阅关系字典中,分别保存了所有频道和模式的订阅关系,SUBSCRIBE/PSUBSCRIBE将订阅关系存到字典的链表中,UNSUBSCRIBE/PUNSUBSCRIBE将订阅关系从字典的链表中移除。
PUBLISH命令通过访问订阅关系字典,向命中的频道下,链表中的所有客户端发送消息。
PUBSUB命令的三个子命令都是通过读取订阅关系字典来实现的。
redis版本:redis-6.2.9
参考:《redis设计与实现(第二版)》
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/93727.html