一、概述
- 在之前的文章分析过,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的所有客户端,不会进行消息存储。
本文深入剖析Redis的消息订阅与发布功能,包括精确订阅与模式订阅的实现原理,以及消息发布的过程。通过源代码分析,详细介绍了Redis如何在客户端和服务端维护订阅关系,以及消息是如何实时传输给订阅者的。
14万+

被折叠的 条评论
为什么被折叠?



