channel的数据结构
Redis将所有订阅关系保持在服务器状态的pubsub_channels
字典项中:
struct redisServer {
...
dict *pubsub_channels; /* Map channels to list of subscribed clients */
...
}
该字典项的key
是被订阅的某个channel名称,字典项的value
是一个链表,链表中保存了所有订阅这个channel的客户端:
订阅频道
SUBSCRIBE channel [channel ...]
当客户端执行SUBSCRIBE
命令,订阅某个或某些频道时,服务器会将客户端与被订阅的频道在pubsub_channels
字典中进行关联。pubsub.c
:
void subscribeCommand(client *c) {
int j;
for (j = 1; j < c->argc; j++)
pubsubSubscribeChannel(c,c->argv[j]);
c->flags |= CLIENT_PUBSUB;
}
根据频道是否存在于pubsub_channels
字典中,关联操作分俩种情况:
- 如果频道不存在,表示该频道还未有任何订阅者,程序首先要在
pubsub_channels
字典中创建该频道,键名即为该频道名,并将这个键的值设置为空链表,然后再将客户端添加到链表。 - 如果频道存在,表示该频道已有其他订阅者,那么它在
pubsub_channels
字典中必然有相应的订阅者链表,程序唯一要做的就是将客户端添加到订阅者链表的末尾。
/* 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) {
...
/* Add the channel to the client -> channels hash table */
if (dictAdd(c->pubsub_channels,channel,NULL) == DICT_OK) {
/* 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);
}
...
}
由代码可知,在进行关联操作之前,先将该频道添加到客户端状态的pubsub_channels
字典中,留待客户端离线时使用。字典的键为该频道名,键的值为null。
// 将频道添加到客户端状态的pubsub_channels字典中,键为channel,键的值为NULL
if (dictAdd(c->pubsub_channels,channel,NULL) == DICT_OK) {}
假如执行下面操作:
client-9527> subscribe qiuxiang
当某个频道有新消息时,Redis遍历该频道的客户端列表,依次给每个客户端发送消息。
取消订阅
UNSUBSCRIBE channel [channel ...]
取消订阅,就是将该客户端从pubsub_channels
字典中移除:
client-9527> unsubscribe qiuxiang
int pubsubUnsubscribeChannel(client *c, robj *channel, int notify) {
...
// 找到要取消订阅频道对应的字典项
de = dictFind(server.pubsub_channels,channel);
...
// 订阅该频道的所有客户端列表
clients = dictGetVal(de);
// 找到要取消订阅的客户端
ln = listSearchKey(clients,c);
...
// 将该客户端从列表中移除
listDelNode(clients,ln);
// 如果列表项未空,表示没有客户端订阅该频道了,将该频道从pubsub_channels字典中移除
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);
}
...
}
客户端离线
当客户端离线时,Redis会执行freeClient
函数,
void freeClient(client *c) {
...
// 取消所有频道的订阅
pubsubUnsubscribeAllChannels(c,0);
...
}
int pubsubUnsubscribeAllChannels(client *c, int notify) {
// 获取客户端已订阅频道的字典迭代器
dictIterator *di = dictGetSafeIterator(c->pubsub_channels);
...
// 迭代字典
while((de = dictNext(di)) != NULL) {
robj *channel = dictGetKey(de);
// 依次取消订阅
count += pubsubUnsubscribeChannel(c,channel,notify);
}
}
假如客户端"Client 1"离线:
"Client 1"离线后:
后记
Redis的pub/sub
功能,只能实时获取订阅的频道消息,当客户端离线后,离线后的频道消息不会被保存起来,这点和MQ服务还是有区别的。