Redis
可以执行发布/订阅模式(publish
/subscribe
), 该模式可以解耦消息的发送者和接收者,使程序具有更好的扩展性.从宏观上来讲,Redis的发布/订阅模式具有如下特点:
SUBSCRIBE
或者PSUBSCRIBE
),取消订阅(UNSUBSCRIBE
或者PUNSUBSCRIBE
), PING
命令和结束连接(QUIT
)外, 不能执行其他操作,客户端将阻塞直到订阅通道上发布消息的到来.glob
模式匹配.如果客户端同时订阅了glob模式的通道和非glob模式的通道,并且名称存在交集,则对于一个发布的消息,该执行订阅的客户端接收到两个消息.pub/sub API
Redis
的发布/订阅设计模式相关的命令有六个:
SUBSCRIBE
命令执行订阅.客户端可以多次执行该命令, 也可以一次订阅多个通道. 多个客户端可以订阅相同的通道.该命令的响应包括三部分, 依次是:命令名称(字符串subscribe
),订阅的通道名称,总共订阅的通道数(包含glob
通道).PSUBSCRIBE
命令执行glob
模式订阅.客户端可以多次执行该命令, 也可以一次订阅多个glob通道. 多个客户端可以订阅相同的glob通道.该命令的响应包括三部分, 依次是:命令名称(字符串psubscribe
),订阅的glob
通道名称,总共订阅的通道数(包含非glob
通道).UNSUBSCRIBE
命令取消订阅指定的通道.可以指定一个或者多个取消的订阅通道名称,也可以不带任何参数,此时将取消所有的订阅的通道(不包括glob
通道).该命令的响应包括三部分, 依次是:命令名称(字符串unsubscribe
),取消的订阅通道名称,总共订阅的通道数(包含glob
通道).PUNSUBSCRIBE
命令取消订阅指定的glob
模式通道.可以指定一个或者多个取消的glob
模式的订阅通道名称,也可以不带任何参数,此时将取消所有的glob
模式订阅的通道(不包括非glob
通道).该命令的响应包括三部分, 依次是:命令名称(字符串punsubscribe
),取消的glob
模式的订阅通道名称,总共订阅的通道数(包含非glob
通道).PUBLISH
命令在指定的通道上发布消息.只能在一个通道上发布消息,不能在多个通道上同时发布消息.该命令的响应包括通知的接收者个数,需要注意的是,这里的接收者数目大于等于订阅该通道的客户端数目(因为一个客户端的glob通道和非glob通道同时匹配发布通道的话,则视为两个接收者).而在接收端,收到的响应包括三部分,依次是 :message
或者pmessage
字符串(取决于是否为glob
匹配),匹配的通道名称,发布的消息内容.PUBSUB
命令执行状态查询.支持若干子命令.需要注意的是,该命令不能在客户端进入订阅后执行.下面是一个连续交互使用的例子:
subscribe foo bar
*3
$9
subscribe
$3
foo
:1
*3
$9
subscribe
$3
bar
:2
同时订阅了两个非glob
通道,返回的响应中最后给出的订阅的通道总数为2.
接着继续执行:
psubscribe b?r *news
*3
$10
psubscribe
$3
b?r
:3
*3
$10
psubscribe
$5
*news
:4
同时订阅了两个glob
通道,这时总共订阅的通道数增加到4个(包括之前订阅的2个非glob
通道).
接着,在其他客户端上执行:
publish foo "Hello foo"
:1
说明有一个接收者在该通道上接受了消息.
接收客户端(订阅客户端)的响应为:
*3
$7
message
$3
foo
$9
Hello foo
从响应中,可以看出是在非glob
通道上(foo
通道)上接受的消息.
发布客户端继续执行:
publish bar "Hi bar"
:2
有两个接收者在该发布通道上接收了消息. 这是因为,订阅客户端的glob
通道和非glob
通道同时匹配这个通道名称,视为两个接收者.
订阅客户端上的响应为:
*3
$7
message
$3
bar
$6
Hi bar
*4
$8
pmessage
$3
b?r
$3
bar
$6
Hi bar
从响应中看到,message
和pmessage
类型的通道都收到了消息.
在订阅客户端上,继续执行:
unsubscribe foo
*3
$11
unsubscribe
$3
foo
:3
取消了一个非glob
通道的订阅,订阅总数更新为3个.
在订阅客户端上,继续执行:
punsubscribe
*3
$12
punsubscribe
$3
b?r
:2
*3
$12
punsubscribe
$5
*news
:1
取消了所有glob
通道的订阅,订阅总数更新为1个.
在发布客户端上,继续执行:
pubsub channels
*1
$3
bar
从响应中,可以看到Redis
系统中只存在一个订阅通道了.
在发布客户端上,继续执行:
select 1
+OK
因为每个客户端连接到Redis系统时,默认使用的是0
号数据库.该命令将发布客户端连接到Redis系统的1
号数据库上.
在发布客户端上,继续执行:
publish bar "howdy bar"
:1
说明有一个接收者在该通道上接收到了消息.
订阅客户端上的情况:
*3
$7
message
$3
bar
$9
howdy bar
订阅客户端接收到了消息.
小结:
源码实现机制
SUBSCRIBE
命令执行函数subscribeCommand
, 其实现为(pubsub.c):
void subscribeCommand(redisClient *c) {
int j;
for (j = 1; j < c->argc; j++)
pubsubSubscribeChannel(c,c->argv[j]);
c->flags |= REDIS_PUBSUB;
}
该命令设置客户端对象标志REDIS_PUBSUB
,表示进入了订阅模式.
函数pubsubSubscribeChannel
的实现为(pubsub.c):
/* 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(redisClient *c, robj *channel) {
struct 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;
}
需要说明的是,通道名称既在客户端对象的哈希表pubsub_channels
中保存,也在全局变量server
的哈希表pubsub_channels
中保存.保存在这两个数据结构的一个原因是, 通过在客户端对象的哈希表中可以快速判断该通道是否添加过. 通道名称并没有保存在连接的数据库中,而是直接保存在了全局的server
结构中,因此客户端改变连接的数据库时, 对发布/订阅没有任何影响.在全局哈希表pubsub_channels
中保存的是通道名称和订阅该通道的客户端链表.
命令PSUBSCRIBE
执行函数psubscribeCommand
, 其实现为(pubsub.c):
void psubscribeCommand(redisClient *c) {
int j;
for (j = 1; j < c->argc; j++)
pubsubSubscribePattern(c,c->argv[j]);
c->flags |= REDIS_PUBSUB;
}
该命令设置客户端对象标志REDIS_PUBSUB
,表示进入了订阅模式.
函数pubsubSubscribePattern
的实现为(pubsub.c):
/* 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(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;
}
和非glob
通道名称一样, glob
通道名称也是既保存在客户端对象中也保存在全局变量server
中. 并且均使用不同的字段,而且字段数据结构都是使用的链表.为什么不和非glob
通道一样用哈希表来保存, 个人认为,glob
模式的名字在系统中可能数量较少,而且名字长度可能要短一些,所以,使用链表处理就可以了. 这里需要注意的一点是,对于glob
通道名字,必须保存其原始编码格式(REDIS_ENCODING_RAW
), 因为后续在处理PUBLISH
命令的时候,涉及到glob
字符匹配.
命令PUBLISH
执行函数publishCommand
, 其实现为(pubsub.c):
void publishCommand(redisClient *c) {
int receivers = pubsubPublishMessage(c->argv[1],c->argv[2]);
forceCommandPropagation(c,REDIS_PROPAGATE_REPL);
addReplyLongLong(c,receivers);
}
消息发布会replica
节点同步.
函数pubsubPublishMessage
的实现为(pubsub.c):
/* Publish a message */
int pubsubPublishMessage(robj *channel, robj *message) {
int receivers = 0;
struct 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;
}
LINE8:25在全局的哈希表pubsub_channels
中查找订阅该通道的客户端,并把发布的消息写入订阅客户端的发送缓存中.
LINE26:46遍历全局glob
通道名称链表pubsub_patterns
, 对每个订阅的glob
通道,模式匹配发布命令的通道,如果匹配成功, 向该订阅客户端发送缓存写入发布消息.
命令UNSUBSCRIBE
执行函数unsubscribeCommand
, 其实现为(pubsub.c):
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;
}
如果当前客户端所有订阅的通道均被取消,则取消标识REDIS_PUBSUB
,退出订阅模式.
函数pubsubUnsubscribeAllChannels
的实现会调用pubsubUnsubscribeChannel
逐个取消订阅的通道, 其实现为(pubsub.c):
/* Unsubscribe a client from a channel. Returns 1 if the operation succeeded, or
* 0 if the client was not subscribed to the specified channel. */
int pubsubUnsubscribeChannel(redisClient *c, robj *channel, int notify) {
struct 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;
}
即分别在客户端对象的哈希表pubsub_channels
和全局变量server
的哈希表pubsub_channels
取消该通道.
命令PUNSUBSCRIBE
和命令UNSUBSCRIBE
的实现基本类似,这里不再分析.