Redis源代码分析(三十)--- pubsub公布订阅模式
今天学习了Redis中比較高大上的名词,“公布订阅模式”。公布订阅模式这个词在我最開始接触听说的时候是在JMS(Java Message Service)java消息服务中听说的。这个名次用通俗的一点话说。就是我订阅了这类消息,当仅仅有这类的消息进行广播发送的时候。我才会。其它的消息直接过滤,保证了一个高效的传输效率。以下切入正题。学习一下Redis是怎样实现这个公布订阅模式的。先看看里面的简单的API构造;
/*-----------------------------------------------------------------------------
* Pubsub low level API
*----------------------------------------------------------------------------*/
void freePubsubPattern(void *p) /* 释放公布订阅的模式 */
int listMatchPubsubPattern(void *a, void *b) /* 公布订阅模式是否匹配 */
int clientSubscriptionsCount(redisClient *c) /* 返回client的所订阅的数量,包含channels + patterns管道和模式 */
int pubsubSubscribeChannel(redisClient *c, robj *channel) /* Client订阅一个Channel管道 */
int pubsubUnsubscribeChannel(redisClient *c, robj *channel, int notify) /* 取消订阅Client中的Channel */
int pubsubSubscribePattern(redisClient *c, robj *pattern) /* Clientclient订阅一种模式 */
int pubsubUnsubscribePattern(redisClient *c, robj *pattern, int notify) /* Clientclient取消订阅pattern模式 */
int pubsubUnsubscribeAllChannels(redisClient *c, int notify) /* client取消自身订阅的全部Channel */
int pubsubUnsubscribeAllPatterns(redisClient *c, int notify) /* client取消订阅全部的pattern模式 */
int pubsubPublishMessage(robj *channel, robj *message) /* 为全部订阅了Channel的Client发送消息message */ /* ------------PUB/SUB API ---------------- */
void subscribeCommand(redisClient *c) /* 订阅Channel的命令 */
void unsubscribeCommand(redisClient *c) /* 取消订阅Channel的命令 */
void psubscribeCommand(redisClient *c) /* 订阅模式命令 */
void punsubscribeCommand(redisClient *c) /* 取消订阅模式命令 */
void publishCommand(redisClient *c) /* 公布消息命令 */
void pubsubCommand(redisClient *c) /* 公布订阅命令 */
在这里面出现了高频的词Pattern(模式)和Channel(频道,叫管道比較别扭)。也就是说,兴许全部的关于公布订阅的东东都是基于这2者展开进行的。如今大致解说一下在Redis中是怎样实现此中模式的:
1.在RedisClient 内部维护了一个pubsub_channels的Channel列表。记录了此client所订阅的频道
2.在Server服务端。相同维护着一个类似的变量叫做,pubsub_channels,这是一个dict字典变量,每个Channel相应着一批订阅了此频道的Client,也就是Channel-->list of Clients
3.当一个Client publish一个message的时候。会先去服务端的pubsub_channels找对应的Channel,遍历里面的Client。然后发送通知,即完毕了整个公布订阅模式。
我们能够简单的看一下Redis订阅一个Channel的方法实现;
/* Subscribe a client to a channel. Returns 1 if the operation succeeded, or
* 0 if the client was already subscribed to that channel. */
/* Client订阅一个Channel管道 */
int pubsubSubscribeChannel(redisClient *c, robj *channel) {
struct dictEntry *de;
list *clients = NULL;
int retval = 0; /* Add the channel to the client -> channels hash table */
//在Client的字典pubsub_channels中加入Channel
if (dictAdd(c->pubsub_channels,channel,NULL) == DICT_OK) {
retval = 1;
incrRefCount(channel);
/* Add the client to the channel -> list of clients hash table */
//加入Clietn到server中的pubsub_channels,相应的列表中
de = dictFind(server.pubsub_channels,channel);
if (de == NULL) {
//假设此频道的Client列表为空,则创建新列表并加入
clients = listCreate();
dictAdd(server.pubsub_channels,channel,clients);
incrRefCount(channel);
} else {
//否则,获取这个频道的客户端列表。在尾部加入新的客户端
clients = dictGetVal(de);
}
listAddNodeTail(clients,c);
}
/* Notify the client */
//加入给回复客户端
addReply(c,shared.mbulkhdr[3]);
addReply(c,shared.subscribebulk);
addReplyBulk(c,channel);
addReplyLongLong(c,clientSubscriptionsCount(c));
return retval;
}
加入操作主要分2部,Client自身的内部维护的pubsub_channels的加入。是一个dict字典对象,然后,是server端维护的pubsub_channels中的client列表的加入。在进行Channel频道的删除的时候,也是运行的这2步骤操作:
/* Unsubscribe a client from a channel. Returns 1 if the operation succeeded, or
* 0 if the client was not subscribed to the specified channel. */
/* 取消订阅Client中的Channel */
int pubsubUnsubscribeChannel(redisClient *c, robj *channel, int notify) {
struct dictEntry *de;
list *clients;
listNode *ln;
int retval = 0; /* Remove the channel from the client -> channels hash table */
incrRefCount(channel); /* channel may be just a pointer to the same object
we have in the hash tables. Protect it... */
//字典删除Client中pubsub_channels中的Channel
if (dictDelete(c->pubsub_channels,channel) == DICT_OK) {
retval = 1;
/* Remove the client from the channel -> clients list hash table */
//再移除Channel相应的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);
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]);
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;
}
里面还有相应的模式的订阅和取消订阅的操作,原理和channel全然一致。二者的差别在于,pattern是用来匹配的Channel的,这个是什么意思呢。在后面会做出答案,接着看。最后看一个最最核心的方法,客户端发步消息方法:
/* Publish a message */
/* 为全部订阅了Channel的Client发送消息message */
int pubsubPublishMessage(robj *channel, robj *message) {
int receivers = 0;
struct dictEntry *de;
listNode *ln;
listIter li; /* Send to clients listening for that channel */
//找到Channel所相应的dictEntry
de = dictFind(server.pubsub_channels,channel);
if (de) {
//获取此Channel相应的客户单列表
list *list = dictGetVal(de);
listNode *ln;
listIter li; listRewind(list,&li);
while ((ln = listNext(&li)) != NULL) {
//依次取出List中的客户单,加入消息回复
redisClient *c = ln->value; addReply(c,shared.mbulkhdr[3]);
addReply(c,shared.messagebulk);
addReplyBulk(c,channel);
//加入消息回复
addReplyBulk(c,message);
receivers++;
}
}
/* Send to clients listening to matching channels */
/* 发送给尝试匹配该Channel的客户端消息 */
if (listLength(server.pubsub_patterns)) {
listRewind(server.pubsub_patterns,&li);
channel = getDecodedObject(channel);
while ((ln = listNext(&li)) != NULL) {
pubsubPattern *pat = ln->value; //客户端的模式假设匹配了Channel。也会发送消息
if (stringmatchlen((char*)pat->pattern->ptr,
sdslen(pat->pattern->ptr),
(char*)channel->ptr,
sdslen(channel->ptr),0)) {
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;
}
pattern的作用就在上面体现了,假设某种pattern匹配了Channel频道,则模式的客户端也会接收消息。在server->pubsub_patterns中,pubsub_patterns是一个list列表,里面的每个pattern仅仅相应一个Client,就是上面的pat->client,这一点和Channel还是有本质的差别的。
讲完公布订阅模式的基本操作后。顺便把与此相关的notify通知类也稍稍讲讲,通知仅仅有3个方法。
/* ----------------- API ------------------- */
int keyspaceEventsStringToFlags(char *classes) /* 键值字符类型转为相应的Class类型 */
sds keyspaceEventsFlagsToString(int flags) /* 通过输入的flag值类,转为字符类型*/
void notifyKeyspaceEvent(int type, char *event, robj *key, int dbid) /* 公布通知方法,分为2类,keySpace的通知。keyEvent的通知 */
涉及到string
To flag 和flag To String 的转换,也不知道这个会在哪里用到;
/* Turn a string representing notification classes into an integer
* representing notification classes flags xored.
*
* The function returns -1 if the input contains characters not mapping to
* any class. */
/* 键值字符类型转为相应的Class类型 */
int keyspaceEventsStringToFlags(char *classes) {
char *p = classes;
int c, flags = 0; while((c = *p++) != '\0') {
switch(c) {
case 'A': flags |= REDIS_NOTIFY_ALL; break;
case 'g': flags |= REDIS_NOTIFY_GENERIC; break;
case '$': flags |= REDIS_NOTIFY_STRING; break;
case 'l': flags |= REDIS_NOTIFY_LIST; break;
case 's': flags |= REDIS_NOTIFY_SET; break;
case 'h': flags |= REDIS_NOTIFY_HASH; break;
case 'z': flags |= REDIS_NOTIFY_ZSET; break;
case 'x': flags |= REDIS_NOTIFY_EXPIRED; break;
case 'e': flags |= REDIS_NOTIFY_EVICTED; break;
case 'K': flags |= REDIS_NOTIFY_KEYSPACE; break;
case 'E': flags |= REDIS_NOTIFY_KEYEVENT; break;
default: return -1;
}
}
return flags;
}
应该是响应键盘输入的类型和Redis类型之间的转换。在notify的方法另一个event事件的通知方法:
/* The API provided to the rest of the Redis core is a simple function:
*
* notifyKeyspaceEvent(char *event, robj *key, int dbid);
*
* 'event' is a C string representing the event name.
* 'key' is a Redis object representing the key name.
* 'dbid' is the database ID where the key lives. */
/* 公布通知方法,分为2类,keySpace的通知,keyEvent的通知 */
void notifyKeyspaceEvent(int type, char *event, robj *key, int dbid) {
sds chan;
robj *chanobj, *eventobj;
int len = -1;
char buf[24]; /* If notifications for this class of events are off, return ASAP. */
if (!(server.notify_keyspace_events & type)) return; eventobj = createStringObject(event,strlen(event)); //2种的通知形式,略有区别
/* __keyspace@<db>__:<key> <event> notifications. */
if (server.notify_keyspace_events & REDIS_NOTIFY_KEYSPACE) {
chan = sdsnewlen("__keyspace@",11);
len = ll2string(buf,sizeof(buf),dbid);
chan = sdscatlen(chan, buf, len);
chan = sdscatlen(chan, "__:", 3);
chan = sdscatsds(chan, key->ptr);
chanobj = createObject(REDIS_STRING, chan);
//上述几步操作,组件格式字符串。最后公布消息。以下keyEvent的通知同理
pubsubPublishMessage(chanobj, eventobj);
decrRefCount(chanobj);
} /* __keyevente@<db>__:<event> <key> notifications. */
if (server.notify_keyspace_events & REDIS_NOTIFY_KEYEVENT) {
chan = sdsnewlen("__keyevent@",11);
if (len == -1) len = ll2string(buf,sizeof(buf),dbid);
chan = sdscatlen(chan, buf, len);
chan = sdscatlen(chan, "__:", 3);
chan = sdscatsds(chan, eventobj->ptr);
chanobj = createObject(REDIS_STRING, chan);
pubsubPublishMessage(chanobj, key);
decrRefCount(chanobj);
}
decrRefCount(eventobj);
}
有keySpace和keyEvent的2种事件通知。
详细怎么用。等后面碰到的时候在看看。
Redis源代码分析(三十)--- pubsub公布订阅模式的更多相关文章
- Redis源代码分析(十)--- testhelp.h小测试框架和redis-check-aof.c
日志检测
周期分析struct结构体redis代码.最后,越多越发现很多的代码其实大同小异.于struct有袋1,2不分析文件,关于set集合的一些东西,就放在下次分析好了,在选择下个分析的对象时,我考虑了一下 ...
- Redis源代码分析(十二)--- redis-check-dump本地数据库检測
这个文件我在今天分析学习的时候,一直有种似懂非懂的感觉,代码量700+的代码,最后开放给系统的就是一个process()方法.这里说的说的数据库检測,是针对key的检測,会用到,以下提到的结构体: / ...
- Nouveau源代码分析(三):NVIDIA设备初始化之nouveau_drm_probe
Nouveau源代码分析(三) 向DRM注冊了Nouveau驱动之后,内核中的PCI模块就会扫描全部没有相应驱动的设备,然后和nouveau_drm_pci_table对比. 对于匹配的设备,PCI模 ...
- NetMQ(三): 发布订阅模式 Publisher-Subscriber
ZeroMQ系列 之NetMQ 一:zeromq简介 二:NetMQ 请求响应模式 Request-Reply 三:NetMQ 发布订阅模式 Publisher-Subscriber 四:NetMQ ...
- Redis进阶实践之十八 使用管道模式提高Redis查询的速度
原文:Redis进阶实践之十八 使用管道模式提高Redis查询的速度 一.引言 学习redis 也有一段时间了,该接触的也差不多了.后来有一天,以为同事问我,如何向redis中 ...
- Redis源代码分析(一)--Redis结构解析
从今天起,本人将会展开对Redis源代码的学习,Redis的代码规模比較小,很适合学习,是一份很不错的学习资料,数了一下大概100个文件左右的样子,用的是C语言写的.希望终于能把他啃完吧,C语言好久不 ...
- redis 源代码分析(一) 内存管理
一,redis内存管理介绍 redis是一个基于内存的key-value的数据库,其内存管理是很重要的,为了屏蔽不同平台之间的差异,以及统计内存占用量等,redis对内存分配函数进行了一层封装,程序中 ...
- Android 中View的绘制机制源代码分析 三
到眼下为止,measure过程已经解说完了,今天開始我们就来学习layout过程.只是在学习layout过程之前.大家有没有发现我换了编辑器,哈哈.最终下定决心从Html编辑器切换为markdown编 ...
- Redis源代码分析(23)--- CRC循环冗余算法RAND随机数的算法
他今天就开始学习Redis源代码的一些工具来实现,在任何一种语言工具.算法实现的原理应该是相同的,一些比較经典的算法.比方说我今天看的Crc循环冗余校验算法和rand随机数产生算法. CRC算法全称循 ...
随机推荐
- Java 8 (2) 使用Lambda表达式
什么是Lambda? 可以把Lambda表达式理解为 简洁的表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表.函数主体.返回类型,可能还有一个可以抛出的异常列表. 使用Lambda可以让你更 ...
- hibernate一对多查询
一对多查询 1,同时添加老师和学生案例 在进行具有关联关系的对象同时添加时 首先绑定对像间的关系 ---将多方关联一方 ---将一方关联多方 然后全部添加 备注: 1,保存老师对象时, 由于设置了学生 ...
- phpstorm设置代码片段
tab 向后推进 shift+tab 向前推进 ctrl+d 复制整行 ctrl+Y删除整行 代码片段就是代码快捷键,如果你设置了www.baidu.com这些内容,但是不想一直重复的打,可以设置个代 ...
- python的机器学习之路
2018-04-1712:22:40 这是python依靠计算机视觉进行的ocr手写字的识别. 通过KNN训练数据 kNN 可以说是最简单的监督学习分类器了.想法也很简单,就是找出测试数据在特征空间中 ...
- QS之shell script
1 Invoke Mdoelsim In order to open Modelsim automatically, it is better to use a shell script to inv ...
- 腾讯云 LNMP+wordpress 搭建个人网站
折腾了好几个小时才弄好(php nginx略知一二),其实一点都不难! 以此记录一下,献给首次搭建的朋友们!! 1)准备工作:(因为个人用的ubuntu16.04 LTS系统 所以这是debian版 ...
- 【OpenCV】像素操作的数字图像处理
之前几天捣鼓matlab,用来处理数字图像,矩阵操作什么的,如果忘记线性代数就真的GG了. 在用了matlab被深深地吐槽之后,决定改用opencv,C++貌似也是处理数字图像的很好的工具 1. 在u ...
- Redis系列(八)--缓存穿透、雪崩、更新策略
1.缓存更新策略 1.LRU/LFU/FIFO算法剔除:例如maxmemory-policy 2.超时剔除,过期时间expire,对于一些用户可以容忍延时更新的数据,例如文章简介内容改了几个字 3.主 ...
- centos7安装个人博客wordpress
第一步: 安装apache web 第二步: 安装MariaDB数据库 第三步: 安装PHP 第四步: 安装phpMyAdmin 第五部: 创建数据库: 第六部: 下载wordpress 第七部:复制 ...
- 05网页<div></div>块内容
网页<div></div>块内容 <header>此处为新 header 标签的内容</header> <navigation>此处为新 n ...