一、简介
上次说的subscribe只能订阅一个确定名称的channel,比如我记不清某个公众号的全称,只记得公众号名称中带有redis, 则可以使用模糊匹配的方式进行订阅,使用 *redis*则能订阅包含redis的所有公众号。
redis可以通过模糊匹配规则订阅多个channel。
模糊匹配规则:
- h
?llo subscribes to hello, hallo and hxllo - h
*llo subscribes to hllo and heeeello - h
[ae]llo subscribes to hello and hallo, but not hillo
这些规则和正则表达式类似,只是种类少,?匹配某一个字符,*匹配任意个字符,[list]匹配一个列表中的某一个字符。看https://redis.io/commands/psubscribe官方的命令介绍说的规则就这几种,但是看源代码中在psubscribe命令处理时并没有对这些规则进行判断,而匹配函数中有支持更多几种规则,不知道是官网命令没有更新还是我看代码时遗漏了一些细节。
二、批量订阅(批量订阅公众号)
使用psubscribe命令进行批量进行channel的订阅, redis服务器使用psubscribeCommand函数进行处理。
PSUBSCRIBE pattern [pattern ...]
struct redisCommand redisCommandTable[] = {
...
{"psubscribe",psubscribeCommand,-2,
"pub-sub no-script ok-loading ok-stale",
0,NULL,0,0,0,0,0,0},
...
};
2.1 排除不能执行订阅的client
第一步依然是排除要求非阻塞的client, 但对multi做了特殊处理。
void psubscribeCommand(client *c) {
int j;
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, "PSUBSCRIBE isn't allowed for a DENY BLOCKING client");
return;
}
...
}
2.2 订阅处理
逐一处理每一个pattern。
for (j = 1; j < c->argc; j++)
pubsubSubscribePattern(c,c->argv[j]);
2.2.1 判断是否已经存在相同的pattern
int pubsubSubscribePattern(client *c, robj *pattern) {
dictEntry *de;
list *clients;
int retval = 0;
//从client的链表中搜索是否有相同的pattern
if (listSearchKey(c->pubsub_patterns,pattern) == NULL) {
....
搜索过程是遍历链表中的每一项,回调match函数进行匹配,如果匹配上则返回对应的节点,否则继续下一项,如果都没有匹配上,则返回NULL,说明列表中没有相同的项。
listNode *listSearchKey(list *list, void *key)
{
listIter iter;
listNode *node;
listRewind(list, &iter);
while((node = listNext(&iter)) != NULL) {
if (list->match) {
if (list->match(node->value, key)) {
return node;
}
} else {
if (key == node->value) {
return node;
}
}
}
return NULL;
}
而对于此match函数,在client第一次建立连接被创建时就设置好了。
client *createClient(connection *conn) {
...
c->pubsub_patterns = listCreate();
...
listSetFreeMethod(c->pubsub_patterns,decrRefCountVoid);
listSetMatchMethod(c->pubsub_patterns,listMatchObjects);
...
}
2.2.2 将pattern加入到当前client的链表中
如果此pattern没有在此client的链表中找到,则是第一次订阅,将它加入到链表中,增加引用计数是为了后续加入到全局hash表中共享使用。
retval = 1;
listAddNodeTail(c->pubsub_patterns,pattern);
incrRefCount(pattern);
2.2.3 将pattern加入到全局的hash表中
如果是第一次加入到client的链表中,则查询是否在全局hash表中是否存在,不存在则加入到全局hash表中。
/* Add the client to the pattern -> list of clients hash table */
de = dictFind(server.pubsub_patterns,pattern);
if (de == NULL) {
clients = listCreate();
dictAdd(server.pubsub_patterns,pattern,clients);
incrRefCount(pattern);
} else {
clients = dictGetVal(de);
}
2.2.4 将client加入链表中
全局hash表中的每一项,key为pattern , 值即为client对象。
listAddNodeTail(clients,c);
2.2.5 通知client
/* Notify the client */
addReplyPubsubPatSubscribed(c,pattern);
/* Send the pubsub pattern subscription notification to the client. */
void addReplyPubsubPatSubscribed(client *c, robj *pattern) {
if (c->resp == 2)
addReply(c,shared.mbulkhdr[3]);
else
addReplyPushLen(c,3);
addReply(c,shared.psubscribebulk);
addReplyBulk(c,pattern);
addReplyLongLong(c,clientSubscriptionsCount(c));
}
2.3 设置订阅模式
c->flags |= CLIENT_PUBSUB;
三、发布消息(多账户同时更新文章)
PUBLISH channel message
发布消息依然使用publish命令,只是在发送消息的channel时,选取client时,需要遍历所有的pattern。
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 对channel完全相等的client发送消息
从server.pubsub_channels中获取的channel都是全名,即完全相等的channel,才会对这些client发送消息。
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++;
}
}
3.2 对pattern满足的client发送消息
本文主要关注这种情况。
3.2.1 遍历全局的hash表
/* Send to clients listening to matching channels */
di = dictGetIterator(server.pubsub_patterns);
if (di) {
...
while((de = dictNext(di)) != NULL) {
...
}
...
dictReleaseIterator(di);
}
3.2.2 逐一使用pattern匹配channel
//遍历hash表项
while((de = dictNext(di)) != NULL) {
//获取pattern和client列表
robj *pattern = dictGetKey(de);
list *clients = dictGetVal(de);
//使用pattern匹配channel, 如果没有匹配上,则开始下一项的处理
if (!stringmatchlen((char*)pattern->ptr,
sdslen(pattern->ptr),
(char*)channel->ptr,
sdslen(channel->ptr),0)) continue;
...
}
如下为匹配的处理函数,当有*时会进行递归回溯操作,因此不要轻易的使用*进行模糊匹配,频繁的操作将会影响性能
int stringmatchlen(const char *pattern, int patternLen,
const char *string, int stringLen, int nocase)
{
while(patternLen && stringLen) {
switch(pattern[0]) {
case '*':
//跳过连续的*
while (patternLen && pattern[1] == '*') {
pattern++;
patternLen--;
}
//如果只有一个*, 则匹配上。
if (patternLen == 1)
return 1; /* match */
//开始递归操作
while(stringLen) {
if (stringmatchlen(pattern+1, patternLen-1,
string, stringLen, nocase))
return 1; /* match */
string++;
stringLen--;
}
return 0; /* no match */
break;
case '?': //匹配任意一个字符,所以直接跳过一个字符
string++;
stringLen--;
break;
case '[': //列表开始
{
int not, match;
pattern++;
patternLen--;
not = pattern[0] == '^'; //判断是否为排除列表中的字符
if (not) { //是排除列表中的字符,移动到pattern的下一个字符
pattern++;
patternLen--;
}
match = 0;
while(1) {
//转义处理
if (pattern[0] == '\\' && patternLen >= 2) {
pattern++;
patternLen--;
if (pattern[0] == string[0])
match = 1;
// 列表结束
} else if (pattern[0] == ']') {
break;
} else if (patternLen == 0) {//pattern结束,感觉是出错了
pattern--;
patternLen++;
break;
//范围
} else if (patternLen >= 3 && pattern[1] == '-') {
//获取起止范围
int start = pattern[0];
int end = pattern[2];
int c = string[0];
if (start > end) {
int t = start;
start = end;
end = t;
}
//如果不区分大小写,则都转换为小写
if (nocase) {
start = tolower(start);
end = tolower(end);
c = tolower(c);
}
pattern += 2;
patternLen -= 2;
//判断是否在范围内
if (c >= start && c <= end)
match = 1;
} else {
if (!nocase) {//大小写敏感,直接比较
if (pattern[0] == string[0])
match = 1;
} else { //大小写不敏感,都转成小写进行比较
if (tolower((int)pattern[0]) == tolower((int)string[0]))
match = 1;
}
}
pattern++;
patternLen--;
}
if (not) //排除列表中的
match = !match;
if (!match)
return 0; /* no match */
string++;
stringLen--;
break;
}
case '\\': //转义字符,跳过转义符
if (patternLen >= 2) {
pattern++;
patternLen--;
}
/* fall through */
default:
if (!nocase) {
if (pattern[0] != string[0])
return 0; /* no match */
} else {
if (tolower((int)pattern[0]) != tolower((int)string[0]))
return 0; /* no match */
}
string++;
stringLen--;
break;
}
pattern++;
patternLen--;
if (stringLen == 0) {
//如果string结束了,而pattern还有*,则跳过*
while(*pattern == '*') {
pattern++;
patternLen--;
}
break;
}
}
//pattern和string都结束,则匹配成功
if (patternLen == 0 && stringLen == 0)
return 1;
return 0;
}
3.2.3 对pattern匹配成功的client队列发送消息
匹配上的pattern存储的是订阅了这个pattern channel的所有client,所以遍历整个链表,对每个client发送消息。
//pattern匹配上,则遍历client链表,逐一发送消息
listRewind(clients,&li);
while ((ln = listNext(&li)) != NULL) {
client *c = listNodeValue(ln);
addReplyPubsubPatMessage(c,pattern,channel,message);
receivers++;
}
四、批量取消订阅(取消关注)
PUNSUBSCRIBE [pattern [pattern ...]]
struct redisCommand redisCommandTable[] = {
...
{"punsubscribe",punsubscribeCommand,-1,
"pub-sub no-script ok-loading ok-stale",
0,NULL,0,0,0,0,0,0},
...
};
根据参数个数进行不同的操作,当没有参数则将当前client订阅的所有的pattern的channel都取消,否则只处理参数指定的pattern的channel被取消。
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;
}
其中清除所有的pattern channel时还是调用的pubsubUnsubscribePattern函数,所以我们关注pubsubUnsubscribePattern函数。
/* Unsubscribe from all the patterns. Return the number of patterns the
* client was subscribed from. */
int pubsubUnsubscribeAllPatterns(client *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) addReplyPubsubPatUnsubscribed(c,NULL);
return count;
}
4.1 从client的链表中删除此pattern channel
如果能从此client的pubsub_patterns链表中找到此channel,则从链表中删除。
int pubsubUnsubscribePattern(client *c, robj *pattern, int notify) {
...
if ((ln = listSearchKey(c->pubsub_patterns,pattern)) != NULL) {
retval = 1;
listDelNode(c->pubsub_patterns,ln);
4.2 从全局的hash的pattern channel的链表中删除client
将此client从hash中的client链表中删除,当client链表为空时,将此hash项也从hash表中删除。
/* Remove the client from the pattern -> clients list hash table */
de = dictFind(server.pubsub_patterns,pattern);
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. */
dictDelete(server.pubsub_patterns,pattern);
}
4.3 通知client
/* Notify the client */
if (notify) addReplyPubsubPatUnsubscribed(c,pattern);
void addReplyPubsubPatUnsubscribed(client *c, robj *pattern) {
if (c->resp == 2)
addReply(c,shared.mbulkhdr[3]);
else
addReplyPushLen(c,3);
addReply(c,shared.punsubscribebulk);
if (pattern)
addReplyBulk(c,pattern);
else
addReplyNull(c);
addReplyLongLong(c,clientSubscriptionsCount(c));
}
五、对比
psubscribe,punsubscribe和subscribe ,unsubscribe实际上都是相同的逻辑,只是将channel存放的地方不同,去重方式不同。
| 命令 | 功能 | client存储方式 | client去重方式 | 去重/查找时间复杂度 |
|---|---|---|---|---|
| SUBSCRIBE channel [channel …] | 订阅消息 | hash | hash | O(1) |
| UNSUBSCRIBE [channel [channel …]] | 取消订阅 | O(1) | ||
| PSUBSCRIBE pattern [pattern …] | pattern订阅 | list | 遍历比较 | O(N) |
| PUNSUBSCRIBE [pattern [pattern …]] | 取消订阅 | O(N) |
而且pattern只是在publish命令中有作用,其他时候都是当作一个简单的字符串对待,比如去重等。
而server中保存的channel只是不同的变量,但是都是相同的hash类型,结构完全相同。
5.1 结构图
比如client1订阅了pattern1, client2订阅了pattern1,pattern2。

六、获取channel的一些信息
在redis 2.8.0增加了pubsub命令,获取一些channel的一些信息。
struct redisCommand redisCommandTable[] = {
...
{"pubsub",pubsubCommand,-2,
"pub-sub ok-loading ok-stale random",
0,NULL,0,0,0,0,0,0},
...
};
其中有四个子命令:
- CHANNELS
- NUMPAT
- NUMSUB
- HELP
6.1 子命令 CHANNELS
PUBSUB CHANNELS [pattern]
返回满足pattern规则的channel,其中channel是pubsub_channels哈希中实际的channel,不是pattern的channel,而且如果未指定pattern,则返回了所有channel。
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);
}
6.2 子命令 NUMPAT
返回系统中不重复的pattern channel的总个数。
直接获取hash中元素的个数,因此时间复杂度O(1)。
else if (!strcasecmp(c->argv[1]->ptr,"numpat") && c->argc == 2) {
/* PUBSUB NUMPAT */
addReplyLongLong(c,dictSize(server.pubsub_patterns));
}
6.3 子命令 NUMSUB
获取订阅了某个channel的client个数。
PUBSUB NUMSUB [channel [channel ...]]
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);
}
}
可以看出,没有指定channel是合法的请求,返回一个空数组。
根据参数指定的channel,从全局的pubsub_channels中查找channel,其中channel的值存储的是一个client的链表,所以直接获取链表的大小即可,如果没找到则返回0。每次的获取时间复杂度是O(1),但是这里有N个channel需要查找,所以命令执行的总时间复杂度O(N)。
6.4 子命令 HELP
此子命令是在6.2.0版本中加入的,只是输出子命令的用法信息。
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: '*').",
"NUMPAT",
" Return number of subscriptions to patterns.",
"NUMSUB [<channel> ...]",
" Return the number of subscribers for the specified channels, excluding",
" pattern subscriptions(default: no channels).",
NULL
};
addReplyHelp(c, help);
}
6.5实际操作

688

被折叠的 条评论
为什么被折叠?



