发布与订阅
Redis 的发布与订阅功能由 PUBLISH, SUBSCRIBE,PSUBSCRIBE 组成
当客户端监听某个频道时,有人发布消息至该频道时,所有的订阅者都会收到这条信息.
18.1 频道的订阅与退订
当一个客户端执行了 SUBSCRIBE ,这个客户端将与被订阅频道之间就建立起了一种订阅关系.
以键值对模式建立相关链接,存入下面结构中, 键为字符串对象, 值为链表对象.
struct redisServer{
dict* pubsub_channels;
};
18.1.1 订阅频道
主要规则就是判断当前是否有对应键,如果没有直接创建添加,如果有就加入末尾
* Subscribe a client to a channel. Returns 1 if the operation succeeded, or
* 0 if the client was already subscribed to that channel.
*
* 设置客户端 c 订阅频道 channel 。
*
* 订阅成功返回 1 ,如果客户端已经订阅了该频道,那么返回 0 。
*/
int pubsubSubscribeChannel(redisClient *c, robj *channel) {
dictEntry *de;
list *clients = NULL;
int retval = 0;
/* Add the channel to the client -> channels hash table */
// 将 channels 填接到 c->pubsub_channels 的集合中(值为 NULL 的字典视为集合)
if (dictAdd(c->pubsub_channels,channel,NULL) == DICT_OK) {
retval = 1;
incrRefCount(channel);
// 关联示意图
// {
// 频道名 订阅频道的客户端
// 'channel-a' : [c1, c2, c3],
// 'channel-b' : [c5, c2, c1],
// 'channel-c' : [c10, c2, c1]
// }
/* Add the client to the channel -> list of clients hash table */
// 从 pubsub_channels 字典中取出保存着所有订阅了 channel 的客户端的链表
// 如果 channel 不存在于字典,那么添加进去
de = dictFind(server.pubsub_channels,channel);
if (de == NULL) {
clients = listCreate();
dictAdd(server.pubsub_channels,channel,clients);
incrRefCount(channel);
} else {
clients = dictGetVal(de);
}
// before:
// 'channel' : [c1, c2]
// after:
// 'channel' : [c1, c2, c3]
// 将客户端添加到链表的末尾
listAddNodeTail(clients,c);
}
/* Notify the client */
// 回复客户端。
// 示例:
// redis 127.0.0.1:6379> SUBSCRIBE xxx
// Reading messages... (press Ctrl-C to quit)
// 1) "subscribe"
// 2) "xxx"
// 3) (integer) 1
addReply(c,shared.mbulkhdr[3]);
// "subscribe\n" 字符串
addReply(c,shared.subscribebulk);
// 被订阅的客户端
addReplyBulk(c,channel);
// 客户端订阅的频道和模式总数
addReplyLongLong(c,dictSize(c->pubsub_channels)+listLength(c->pubsub_patterns));
return retval;
}
18.1.2 退订频道
简单的来说,从dict 中链表中删除对应的客户端节点.
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);
}
}
/* Unsubscribe a client from a channel. Returns 1 if the operation succeeded, or
* 0 if the client was not subscribed to the specified channel.
*
* 客户端 c 退订频道 channel 。
*
* 如果取消成功返回 1 ,如果因为客户端未订阅频道,而造成取消失败,返回 0 。
*/
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 */
// 将频道 channel 从 client->channels 字典中移除
incrRefCount(channel); /* channel may be just a pointer to the same object
we have in the hash tables. Protect it... */
// 示意图:
// before:
// {
// 'channel-x': NULL,
// 'channel-y': NULL,
// 'channel-z': NULL,
// }
// after unsubscribe channel-y :
// {
// 'channel-x': NULL,
// 'channel-z': NULL,
// }
if (dictDelete(c->pubsub_channels,channel) == DICT_OK) {
// channel 移除成功,表示客户端订阅了这个频道,执行以下代码
retval = 1;
/* Remove the client from the channel -> clients list hash table */
// 从 channel->clients 的 clients 链表中,移除 client
// 示意图:
// before:
// {
// 'channel-x' : [c1, c2, c3],
// }
// after c2 unsubscribe channel-x:
// {
// 'channel-x' : [c1, c3]
// }
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);
// 如果移除 client 之后链表为空,那么删除这个 channel 键
// 示意图:
// before
// {
// 'channel-x' : [c1]
// }
// after c1 ubsubscribe channel-x
// then also delete 'channel-x' key in dict
// {
// // nothing here
// }
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]);
// "ubsubscribe" 字符串
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;
}
18.2 模式的订阅与退订
类似于频道的订阅与退订,模式的订阅与退订.
struct redisServer{
list* pubsub_patterns;
};
/*
* 记录订阅模式的结构
*/
typedef struct pubsubPattern {
// 订阅模式的客户端
redisClient *client;
// 被订阅的模式
robj *pattern;
} pubsubPattern;
18.2.1 订阅模式
通过遍历查找来判断客户端有没有进行订阅,如果没有订阅则加入链表表尾.
void psubscribeCommand(redisClient *c) {
int j;
for (j = 1; j < c->argc; j++)
pubsubSubscribePattern(c,c->argv[j]);
}
/* Subscribe a client to a pattern. Returns 1 if the operation succeeded, or 0 if the client was already subscribed to that pattern.
*
* 设置客户端 c 订阅模式 pattern 。
*
* 订阅成功返回 1 ,如果客户端已经订阅了该模式,那么返回 0 。
*/
int pubsubSubscribePattern(redisClient *c, robj *pattern) {
int retval = 0;
// 在链表中查找模式,看客户端是否已经订阅了这个模式
// 这里为什么不像 channel 那样,用字典来进行检测呢?
// 虽然 pattern 的数量一般来说并不多
if (listSearchKey(c->pubsub_patterns,pattern) == NULL) {
// 如果没有的话,执行以下代码
retval = 1;
pubsubPattern *pat;
// 将 pattern 添加到 c->pubsub_patterns 链表中
listAddNodeTail(c->pubsub_patterns,pattern);
incrRefCount(pattern);
// 创建并设置新的 pubsubPattern 结构
pat = zmalloc(sizeof(*pat));
pat->pattern = getDecodedObject(pattern);
pat->client = c;
// 添加到末尾
listAddNodeTail(server.pubsub_patterns,pat);
}
/* Notify the client */
// 回复客户端。
// 示例:
// redis 127.0.0.1:6379> PSUBSCRIBE xxx*
// Reading messages... (press Ctrl-C to quit)
// 1) "psubscribe"
// 2) "xxx*"
// 3) (integer) 1
addReply(c,shared.mbulkhdr[3]);
// 回复 "psubscribe" 字符串
addReply(c,shared.psubscribebulk);
// 回复被订阅的模式
addReplyBulk(c,pattern);
// 回复客户端订阅的频道和模式的总数
addReplyLongLong(c,dictSize(c->pubsub_channels)+listLength(c->pubsub_patterns));
return retval;
}
18.2.2 退订模式
PUNSUBSCRIBECOMMAND
查找链表中是否有对应的客户端,进行一个删除完成退订
/* Unsubscribe a client from a channel. Returns 1 if the operation succeeded, or
* 0 if the client was not subscribed to the specified channel.
*
* 取消客户端 c 对模式 pattern 的订阅。
*
* 取消成功返回 1 ,因为客户端未订阅 pattern 而造成取消失败,返回 0 。
*/
int pubsubUnsubscribePattern(redisClient *c, robj *pattern, int notify) {
listNode *ln;
pubsubPattern pat;
int retval = 0;
incrRefCount(pattern); /* Protect the object. May be the same we remove */
// 先确认一下,客户端是否订阅了这个模式
if ((ln = listSearchKey(c->pubsub_patterns,pattern)) != NULL) {
retval = 1;
// 将模式从客户端的订阅列表中删除
listDelNode(c->pubsub_patterns,ln);
// 设置 pubsubPattern 结构
pat.client = c;
pat.pattern = pattern;
// 在服务器中查找
ln = listSearchKey(server.pubsub_patterns,&pat);
listDelNode(server.pubsub_patterns,ln);
}
/* Notify the client */
// 回复客户端
if (notify) {
addReply(c,shared.mbulkhdr[3]);
// "punsubscribe" 字符串
addReply(c,shared.punsubscribebulk);
// 被退订的模式
addReplyBulk(c,pattern);
// 退订频道之后客户端仍在订阅的频道和模式的总数
addReplyLongLong(c,dictSize(c->pubsub_channels)+
listLength(c->pubsub_patterns));
}
decrRefCount(pattern);
return retval;
}
18.3 发送消息
Redis 客户端通过执行
PUBLISH <channel> <message>
来发送对应的消息
通过查找 channel 频道的订阅者,来进行对应消息的转播
18.3.1 将消息发送给频道订阅者
大致逻辑是通过查找订阅记录结构中是否有对应的频道,对订阅者进行一个遍历,来达到消息转播的目的
void publishCommand(redisClient *c) {
int receivers = pubsubPublishMessage(c->argv[1],c->argv[2]);
if (server.cluster_enabled)
clusterPropagatePublish(c->argv[1],c->argv[2]);
else
forceCommandPropagation(c,REDIS_PROPAGATE_REPL);
addReplyLongLong(c,receivers);
}
/* Publish a message
*
* 将 message 发送到所有订阅频道 channel 的客户端,
* 以及所有订阅了和 channel 频道匹配的模式的客户端。
*/
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;
// 遍历客户端链表,将 message 发送给它们
listRewind(list,&li);
while ((ln = listNext(&li)) != NULL) {
redisClient *c = ln->value;
// 回复客户端。
// 示例:
// 1) "message"
// 2) "xxx"
// 3) "hello"
addReply(c,shared.mbulkhdr[3]);
// "message" 字符串
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
pubsubPattern *pat = ln->value;
// 如果 channel 和 pattern 匹配
// 就给所有订阅该 pattern 的客户端发送消息
if (stringmatchlen((char*)pat->pattern->ptr,
sdslen(pat->pattern->ptr),
(char*)channel->ptr,
sdslen(channel->ptr),0)) {
// 回复客户端
// 示例:
// 1) "pmessage"
// 2) "*"
// 3) "xxx"
// 4) "hello"
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;
}
18.3.2 将消息发送模式订阅者
先进行频道的发送,再遍历模式检查是否有对应的客户端,再进行发送
18.4 查看订阅信息
PUBSUB 命令是 Redis 2.8 新增加的命令之一.
可以查看频道或者模式的相关信息,比如某个频道目前有多少订阅者.
18.4.1 PUBSUB CHANNELS
通过
PUBSUB CHANNELS [pattern]
来进行对应频道的信息查找,根据是否给定了 pattern 来确定返回那个平道的信息.
if (!strcasecmp(c->argv[1]->ptr,"channels") &&
(c->argc == 2 || c->argc ==3))
{
/* PUBSUB CHANNELS [<pattern>] */
// 检查命令请求是否给定了 pattern 参数
// 如果没有给定的话,就设为 NULL
sds pat = (c->argc == 2) ? NULL : c->argv[2]->ptr;
// 创建 pubsub_channels 的字典迭代器
// 该字典的键为频道,值为链表
// 链表中保存了所有订阅键所对应的频道的客户端
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;
// 顺带一提
// 因为 Redis 的字典实现只能遍历字典的值(客户端)
// 所以这里才会有遍历字典值然后通过字典值取出字典键(频道)的蹩脚用法
// 如果没有给定 pattern 参数,那么打印所有找到的频道
// 如果给定了 pattern 参数,那么只打印和 pattern 相匹配的频道
if (!pat || stringmatchlen(pat, sdslen(pat),
channel, sdslen(channel),0))
{
// 向客户端输出频道
addReplyBulk(c,cobj);
mblen++;
}
}
// 释放字典迭代器
dictReleaseIterator(di);
setDeferredMultiBulkLength(c,replylen,mblen);
// PUBSUB NUMSUB [channel-1 channel-2 ... channel-N] 子命令
}
18.4.2 PUBSUB NUMSUB
返回对应频道的订阅数量
if (!strcasecmp(c->argv[1]->ptr,"numsub") && c->argc >= 2) {
/* PUBSUB NUMSUB [Channel_1 ... Channel_N] */
int j;
addReplyMultiBulkLen(c,(c->argc-2)*2);
for (j = 2; j < c->argc; j++) {
// c->argv[j] 也即是客户端输入的第 N 个频道名字
// pubsub_channels 的字典为频道名字
// 而值则是保存了 c->argv[j] 频道所有订阅者的链表
// 而调用 dictFetchValue 也就是取出所有订阅给定频道的客户端
list *l = dictFetchValue(server.pubsub_channels,c->argv[j]);
addReplyBulk(c,c->argv[j]);
// 向客户端返回链表的长度属性
// 这个属性就是某个频道的订阅者数量
// 例如:如果一个频道有三个订阅者,那么链表的长度就是 3
// 而返回给客户端的数字也是三
addReplyBulkLongLong(c,l ? listLength(l) : 0);
}
// PUBSUB NUMPAT 子命令
}
18.4.3 PUBSUB NUMPAT
用于返回当前服务器被订阅模式的数量
if (!strcasecmp(c->argv[1]->ptr,"numpat") && c->argc == 2) {
/* PUBSUB NUMPAT */
// pubsub_patterns 链表保存了服务器中所有被订阅的模式
// pubsub_patterns 的长度就是服务器中被订阅模式的数量
addReplyLongLong(c,listLength(server.pubsub_patterns));
// 错误处理
}
总结
本章介绍了订阅与发布,其的运作原理.
在看之前,想象过各种高大上的实现方式,但是,在源代码以及书籍面前,我深刻的意识到一句话.
程序本质是算法+数据结构
数据结构的简单组合就可以完成各种看似复杂的逻辑,且简单没有复杂度.