订阅模式
Redis 提供两个订阅模式:频道(channel)订阅和 glob-style 模式(pattern)频道订阅。
相关命令模式:
- 频道订阅:SUBSCRIBE [channel-1 …channel-N ]
- 频道退订:UNSUBSCRIBE [channel-1 …channel-N ]
- 查看服务器当前被订阅的频道:PUBSUB CHANNELS [pattern]
- 查看频道的订阅数量:PUBSUB NUMSUB [channel-1 … channel-N]
- 模式订阅:PSUBSCRIBE pattern
- 模式退订:PUNSUBSCRIBE pattern
- 查看模式的订阅数量:PUBSUB NUMPAT
- 发送消息:PUBLISH channel message
在Redis中的存放形式
struct redisServer
{
/* Pubsub */
// 字典,键为频道,值为链表
// 链表中保存了所有订阅某个频道的客户端
// 新客户端总是被添加到链表的表尾
dict *pubsub_channels; //存放在哈希表中
// 这个链表记录了客户端订阅的所有模式的名字
list *pubsub_patterns; //存放在链表中
}
频道的订阅与退订
Redis将所有频道的订阅关系都保存在服务器中的pubsub_channels里边,它的键是被订阅的频道,值是一个链表,链表里面记录了所有订阅这个频道的Client。
订阅频道SUBSCRIBE
当Client订阅频道时,服务器会将Client与被订阅的频道在pubsub_channels 中进行关联。具体分为下边两种情况进行关联:
- 频道已经存在,则pubsub_channels中必然有相应的订阅者链表,将Client添加到订阅者链表的末尾。
- 如果频道不存在,首先在pubsub_channels字典中创建该频道,再将Client添加到链表末尾。
源代码接口: ../src/pubsubs.c
int pubsubSubscribeChannel(redisClient *c, robj *channel)
{
dictEntry *de;
list *clients = NULL;
int retval = 0;
// 将 channels 填接到 c->pubsub_channels 的集合中(值为 NULL 的字典视为集合),这里是看看Client之前是否已经订阅过这个频道了,(值为NULL是因为client自己也要像Server那样存一份自己的频道数据,而频道对应的Client就是自己本身,所以就设为NULL)
if (dictAdd(c->pubsub_channels,channel,NULL) == DICT_OK)
{
retval = 1;
//将自己的引用计数加一
incrRefCount(channel);
// 从 pubsub_channels 字典中取出保存着所有订阅了 channel 的客户端的链表
// 如果 channel 不存在于字典,那么添加进去
//查找下Server的pubsub_channels中是否有此频道,有返回频道在字典中的位置,没有返回NULL
de = dictFind(server.pubsub_channels,channel);
if (de == NULL)
{
//Server中没有此频道信息,先创建一个空链表,将空链表和频道信息加多Server频道字典中
clients = listCreate();
dictAdd(server.pubsub_channels,channel,clients);
incrRefCount(channel);
}
else
{
//Server中有此频道信息,那么就获取该频道的链表clients
clients = dictGetVal(de);
}
// 将客户端添加到链表的末尾
listAddNodeTail(clients,c);
}
/* Notify the client */
// 回复客户端。
addReply(c,shared.mbulkhdr[3]);
// "subscribe\n" 字符串
addReply(c,shared.subscribebulk);
// 被订阅的客户端
addReplyBulk(c,channel);
// 客户端订阅的频道数量(pubsub_channels)和模式总数(pubsub_patterns)
addReplyLongLong(c,dictSize(c->pubsub_channels)+listLength(c->pubsub_patterns));
return retval;
}
退订频道UNSUBSCRIBE
收到退订消息后,服务器将从pubsub_channels中解除Client与频道之间的关联:
- 根据退订频道的名字,在pubsub_channels中找到频道对应的订阅者链表,然后将退订Client的信息由链表中删除。
- 如果删除退订Client后,频道的订阅者链表变空,则将频道由pubsub_channels字典中删除。
源代码接口: ../src/pubsubs.c
int pubsubUnsubscribeChannel(redisClient *c, robj *channel, int notify) {
dictEntry *de;
list *clients;
listNode *ln;
int retval = 0;
// 将频道 channel 从 client->channels 字典中移除
incrRefCount(channel);
if (dictDelete(c->pubsub_channels,channel) == DICT_OK) {
// channel 移除成功,表示客户端订阅了这个频道,执行以下代码
retval = 1;
// 从 channel->clients 的 clients 链表中,移除 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);
// 如果移除 client 之后链表为空,那么删除这个 channel 键
if (listLength(clients) == 0) {
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;
}
模式订阅与退订
Redis服务器将模式的订阅关系保存到pubsub_patterns中,pubsub_patterns是一个链表,链表的每个节点都包含一个pubsub_Pattern结构。
struct pubsubPattern{
//订阅模式的客户端
redisClient *client;
// 被订阅的模式
robj *pattern;
}
订阅模式 psubscribe
服务器对每个被订阅的模式执行以下两个操作:
1、新建一个pubsubPattern结构,将结构的pattern属性设置为被订阅的模式,client属性设置为订阅模式的客户端。
2、将pubsubPattern结构体添加到pubsub_patterns链表的表尾。
源码接口:../src/pubsub.c
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 */
// 回复客户端。
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;
}
退订模式 punsubscribe
服务器收到Client退订某个或多个模式时,在链表pubsub_patterns中查找并删除那些pattern属性为被退订模式,并且Client属性为执行退订命令的客户端的pubsubPattern结构。
源码接口:../src/pubsub.c
int pubsubUnsubscribePattern(redisClient *c, robj *pattern, int notify) {
listNode *ln;
pubsubPattern pat;
int retval = 0;
incrRefCount(pattern);
// 先确认一下,客户端是否订阅了这个模式
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;
}