一、概述
- 在之前的文章分析过,Redis的消息发布与订阅支持基于频道channel的精确订阅与基于模式pattern的模糊订阅,并且是实时的消息传输,不会进行消息存储,如下从源代码来分析Redis的订阅与发布功能的实现,主要在Redis源码的pubsub.c文件定义。
二、消息订阅
1. 客户端和服务端的订阅存储结构
- 消息订阅主要从客户端client和服务端server两个角度来分析。首先在客户端client和服务端server都会维护一个字典类型dict的pubsub_channels来维护频道channel的订阅关系,维护一个列表类型的pubsub_patterns来维护模式pattern的订阅关系,如下redisServer为服务端数据结构,client为客户端数据结构:
// 服务端数据结构
struct redisServer {
// 省略其他代码
// 服务端维护频道channel和订阅了这个channel的客户端列表的映射
// 使用字典结构维护
dict *pubsub_channels; /* Map channels to list of subscribed clients */
// 服务端维护订阅了某个模式pattern的某个客户端的列表
// 使用列表结构维护
list *pubsub_patterns; /* A list of pubsub_patterns */
// 省略其他代码
}
// 客户端数据结构
typedef struct client {
// 省略其他代码
// 客户端所订阅的频道channel
// 使用字典结构维护
dict *pubsub_channels; /* channels a client is interested in (SUBSCRIBE) */
// 客户端所订阅的模式pattern,
// 使用列表结构维护
list *pubsub_patterns; /* patterns a client is interested in (SUBSCRIBE) */
// 省略其他代码
} client;
2. 频道channel的订阅数据结构
- 服务端redisServer维护的频道channel订阅字典pubsub_channels和客户端client维护的频道channel的订阅字典pubsub_channels所存储的数据的方式不一样:
- 服务端存储的是channel到所有订阅了这个频道的客户端,即channel -> clients的映射,即字典的key为频道channel,值为客户端列表clients;
- 客户端存储的是该客户端所订阅过的频道channel,即channel -> NULL,字典的key为频道channel,而值为NULL。
3. 模式pattern的订阅数据结构
- 模式pattern的订阅在服务端和客户端都是使用列表类型的pubsub_patterns来维护,内部存在的模式订阅数据如下:包含客户端的引用client和模式pattern。
typedef struct pubsubPattern {
// 客户端引用
client *client;
// 该
robj *pattern;
} pubsubPattern;
4. 订阅实现
频道channel订阅的源码实现
- Redis服务端redisServer接收客户端订阅某个频道的方法实现如下:在服务端redisServer和客户端client的pubsub_channels都需要添加这个映射关系,核心方法为其中dictAdd函数的参数列表为(dict,key,value),所以从以下代码可知服务端value是客户端引用集合clients,客户端的value为NULL。
int pubsubSubscribeChannel(client *c, robj *channel) {
dictEntry *de;
list *clients = NULL;
int retval = 0;
// 添加到该客户端自身引用client的pubsub_channels中
// 其中dictAdd函数的参数列表为(dict,key,value),
// 所以对于客户端pubsub_channels字典来说,key的值为channel,value为NULL。
// 即如果客户端要取消订阅某个channel,则只需要删除对应的key即可,时间复杂度为O(1)。
if (dictAdd(c->pubsub_channels,channel,NULL) == DICT_OK) {
retval = 1;
incrRefCount(channel);
de = dictFind(server.pubsub_channels,channel);
if (de == NULL) {
clients = listCreate();
// 添加该客户端client到Redis服务端的pubsub_channels中
// 其中dictAdd函数的参数列表为(dict,key,value),
// 所以对于服务端的pubsub_channels字典来说,key为channel,value为订阅了这个channel的客户端列表clients
dictAdd(server.pubsub_channels,channel,clients);
incrRefCount(channel);
} else {
clients = dictGetVal(de);
}
// 将当前客户端c添加到记录发起过订阅的客户端列表clients
listAddNodeTail(clients,c);
}
// 通知客户端订阅完成
addReply(c,shared.mbulkhdr[3]);
addReply(c,shared.subscribebulk);
addReplyBulk(c,channel);
addReplyLongLong(c,clientSubscriptionsCount(c));
return retval;
}
模式pattern订阅的源码实现
- 源码实现如下:创建模式pattern的订阅pubsubPattern的对象实例pat,pubsubPattern包含客户端引用client和模式字符串pattern,然后将pat对象分别添加到服务端的pubsub_patterns列表和客户端的pubsub_patterns列表。
// 客户端订阅某个模式,如item.*则是订阅所有以item.开头的channels
int pubsubSubscribePattern(client *c, robj *pattern) {
int retval = 0;
if (listSearchKey(c->pubsub_patterns,pattern) == NULL) {
retval = 1;
pubsubPattern *pat;
// 在客户端引用c的pubsub_patterns列表添加这个模式pattern
// client的pubsub_patterns为列表list类型,添加该模式pattern字符串到客户端的该列表中
listAddNodeTail(c->pubsub_patterns,pattern);
incrRefCount(pattern);
// 所订阅的模式和当前当前客户端的引用c
pat = zmalloc(sizeof(*pat));
pat->pattern = getDecodedObject(pattern);
pat->client = c;
// 添加模式pattern和客户端的订阅映射pat到服务端的pubsub_patterns列表中
listAddNodeTail(server.pubsub_patterns,pat);
}
// 通知客户端订阅完成
addReply(c,shared.mbulkhdr[3]);
addReply(c,shared.psubscribebulk);
addReplyBulk(c,pattern);
addReplyLongLong(c,clientSubscriptionsCount(c));
return retval;
}
三、消息发布
- 消息发布源码实现如下:方法参数为channel和message,即将消息message发往频道channel。在内部实现当中:
- 首先从服务端的pubsub_channels字典中查找订阅了这个频道channel的所有客户端,然后发送该消息message给这些客户端;
- 然后从服务端的pubsub_patterns列表中查找是否存在匹配这个channel的模式pattern,如果存在则将消息发送给对应的客户端client。
// 发布消息到某个频道channel
int pubsubPublishMessage(robj *channel, robj *message) {
int receivers = 0;
dictEntry *de;
listNode *ln;
listIter li;
// 从服务端查找订阅了这个频道channel的所有客户端
de = dictFind(server.pubsub_channels,channel);
if (de) {
list *list = dictGetVal(de);
listNode *ln;
listIter li;
listRewind(list,&li);
// 遍历所有订阅了这个频道channel的客户端列表
while ((ln = listNext(&li)) != NULL) {
client *c = ln->value;
// 发布消息给订阅了这个频道channel的客户端
addReply(c,shared.mbulkhdr[3]);
addReply(c,shared.messagebulk);
addReplyBulk(c,channel);
addReplyBulk(c,message);
receivers++;
}
}
// 查找服务端的模式pattern列表,看当前消息对应的channel是否与某个模式匹配,
// 有则发送该消息给订阅了这个模式的所有客户端
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)) {
// 发布消息给订阅了这个频道channel的客户端
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;
}
- 所以Redis的消息发布订阅其实就是通过方法调用指定消息内容message和该消息所属的频道channel,将该消息发送给所有订阅了这个channel的所有客户端,不会进行消息存储。