Redis 通过 PUBLISH、 SUBSCRIBE 和 PSUBSCRIBE 等命令实现发布和订阅功能。

 

  这些命令被广泛用于构建即时通信应用,比如网络聊天室(chatroom)和实时广播、实时提醒等。

  本文通过分析 Redis 源码里的 pubsub.c 文件,了解发布和订阅机制的底层实现,籍此加深对 Redis 的理解。

  订阅、发布和退订

  在开始研究源码之前,不妨先来回顾一下几个相关命令的使用方式。

  PUBLISH 命令用于向给定的频道发送信息,返回值为接收到信息的订阅者数量:

redis> PUBLISH treehole "top secret here ..." 
  (integer) 0 
  redis> PUBLISH chatroom "hi?" 
  (integer) 1

  SUBSCRIBE 命令订阅给定的一个或多个频道:

redis> SUBSCRIBE chatroom 
  Reading messages... (press Ctrl-C to quit) 
  1) "subscribe" # 订阅反馈 
  2) "chatroom" # 订阅的频道 
  3) (integer) 1 # 目前客户端已订阅频道/模式的数量 
  1) "message" # 信息 
  2) "chatroom" # 发送信息的频道 
  3) "hi?" # 信息内容

  SUBSCRIBE 的返回值当中, 1) 为 subscribe 的是订阅的反馈信息,而 1) 为 message 的则是订阅的频道所发送的信息。

  SUBSCRIBE 还可以订阅多个频道,这样一来它接收到的信息就可能来自多个频道:

redis> SUBSCRIBE chatroom talk-to-jack 
  Reading messages... (press Ctrl-C to quit) 
  1) "subscribe" # 订阅 chatroom 的反馈 
  2) "chatroom" 
  3) (integer) 1 
  1) "subscribe" # 订阅 talk-to-jack 的反馈 
  2) "talk-to-jack" 
  3) (integer) 2 
  1) "message" # 来自 chatroom 的消息 
  2) "chatroom" 
  3) "yahoo" 
  1) "message" # 来自 talk-to-peter 的消息 
  2) "talk-to-jack" 
  3) "Goodmorning, peter."

  PSUBSCRIBE 提供了一种订阅符合给定模式的所有频道的方法,比如说,使用 it.* 为输入,就可以订阅所有以 it. 开头的频道,比如 it.news 、 it.blog 、 it.tweets ,诸如此类:

redis> PSUBSCRIBE it.* 
  Reading messages... (press Ctrl-C to quit) 
  1) "psubscribe" 
  2) "it.*" 
  3) (integer) 1 
  1) "pmessage" 
  2) "it.*" # 匹配的模式 
  3) "it.news" # 消息的来源频道 
  4) "Redis 2.6rc5 release" # 消息内容 
  1) "pmessage" 
  2) "it.*" 
  3) "it.blog" 
  4) "Why NoSQL matters" 
  1) "pmessage" 
  2) "it.*" 
  3) "it.tweet" 
  4) "@redis: when will the 2.6 stable release?"

  当然, PSUBSCRIBE 也可以接受多个参数,从而匹配多种模式。

  最后, UNSUBSCRIBE 命令和 PUNSUBSCRIBE 负责退订给定的频道或模式。

  发布和订阅机制

  当一个客户端通过 PUBLISH 命令向订阅者发送信息的时候,我们称这个客户端为发布者(publisher)。

  而当一个客户端使用 SUBSCRIBE 或者 PSUBSCRIBE 命令接收信息的时候,我们称这个客户端为订阅者(subscriber)。

  为了解耦发布者(publisher)和订阅者(subscriber)之间的关系,Redis 使用了 channel (频道)作为两者的中介 —— 发布者将信息直接发布给 channel ,而 channel 负责将信息发送给适当的订阅者,发布者和订阅者之间没有相互关系,也不知道对方的存在:

  

点击放大

  知道了发布和订阅的机制之后,接下来就可以开始研究具体的实现了,我们从 Redis 的订阅命令开始说起。

  SUBSCRIBE 命令的实现

  前面说到,Redis 将所有接受和发送信息的任务交给 channel 来进行,而所有 channel 的信息就储存在 redisServer 这个结构中:

struct redisServer { 
  // 省略 ... 
  dict *pubsub_channels; // Map channels to list of subscribed clients 
  // 省略 ... 
  };

  pubsub_channels 是一个字典,字典的键就是一个个 channel ,而字典的值则是一个链表,链表中保存了所有订阅这个 channel 的客户端。

  举个例子,如果在一个 redisServer 实例中,有一个叫做 news 的频道,这个频道同时被client_123 和 client_456 两个客户端订阅,那么这个 redisServer 结构看起来应该是这样子:

  

点击放大

  可以看出,实现 SUBSCRIBE 命令的关键,就是将客户端添加到给定 channel 的订阅链表中。

  函数 pubsubSubscribeChannel 是 SUBSCRIBE 命令的底层实现,它完成了将客户端添加到订阅链表中的工作:

// 订阅指定频道 
  // 订阅成功返回 1 ,如果已经订阅过,返回 0 
  int pubsubSubscribeChannel(redisClient *c, robj *channel) { 
  struct dictEntry *de; 
  list *clients = NULL; 
  int retval = 0; 
  /* Add the channel to the client -> channels hash table */ 
  // dictAdd 在添加新元素成功时返回 DICT_OK 
  // 因此这个判断句表示,如果新订阅 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 */ 
  // 将 client 添加到订阅给定 channel 的链表中 
  // 这个链表是一个哈希表的值,哈希表的键是给定 channel 
  // 这个哈希表保存在 server.pubsub_channels 里 
  de = dictFind(server.pubsub_channels,channel); 
  if (de == NULL) { 
  // 如果 de 等于 NULL 
  // 表示这个客户端是首个订阅这个 channel 的客户端 
  // 那么创建一个新的列表, 并将它加入到哈希表中 
  clients = listCreate(); 
  dictAdd(server.pubsub_channels,channel,clients); 
  incrRefCount(channel); 
  } else { 
  // 如果 de 不为空,就取出这个 clients 链表 
  clients = dictGetVal(de); 
  } 
  // 将客户端加入到链表中 
  listAddNodeTail(clients,c); 
  } 
  /* Notify the client */ 
  addReply(c,shared.mbulkhdr[3]); 
  addReply(c,shared.subscribebulk); 
  // 返回订阅的频道 
  addReplyBulk(c,channel); 
  // 返回客户端当前已订阅的频道和模式数量的总和 
  addReplyLongLong(c,dictSize(c->pubsub_channels)+listLength(c->pubsub_patterns)); 
  return retval; 
  }

  PSUBSCRIBE 命令的实现

  除了直接订阅给定 channel 外,还可以使用 PSUBSCRIBE 订阅一个模式(pattern),订阅一个模式等同于订阅所有匹配这个模式的 channel 。

  和 redisServer.pubsub_channels 属性类似, redisServer.pubsub_patterns 属性用于保存所有被订阅的模式,和 pubsub_channels 不同的是, pubsub_patterns 是一个链表(而不是字典):

struct redisServer { 
  // 省略 ... 
  list *pubsub_patterns; // A list of pubsub_patterns 
  // 省略 ... 
  };

  pubsub_patterns 的每一个节点都是一个 pubsubPattern 结构的实例,它保存了被订阅的模式,以及订阅这个模式的客户客户端:

typedef struct pubsubPattern { 
  redisClient *client; 
  robj *pattern; 
  } pubsubPattern;

  举个例子,假设在一个 redisServer 实例中,有一个叫做 news.* 的模式同时被客户端client_789 和 client_999 订阅,那么这个 redisServer 结构看起来应该是这样子:

  

点击放大

  现在可以知道,实现 PSUBSCRIBE 命令的关键,就是将客户端和订阅的模式添加到redisServer.pubsub_patterns 当中。

  pubsubSubscribePattern 是 PSUBSCRIBE 的底层实现,它将客户端和所订阅的模式添加到redisServer.pubsub_patterns 当中:

// 订阅指定模式 
  // 订阅成功返回 1 ,如果已经订阅过,返回 0 
  int pubsubSubscribePattern(redisClient *c, robj *pattern) { 
  int retval = 0; 
  // 向 c->pubsub_patterns 中查找指定 pattern 
  // 如果返回值为 NULL ,说明这个 pattern 还没被这个客户端订阅过 
  if (listSearchKey(c->pubsub_patterns,pattern) == NULL) { 
  retval = 1; 
  // 添加 pattern 到客户端 pubsub_patterns 
  listAddNodeTail(c->pubsub_patterns,pattern); 
  incrRefCount(pattern); 
  // 将 pattern 添加到服务器 
  pubsubPattern *pat; 
  pat = zmalloc(sizeof(*pat)); 
  pat->pattern = getDecodedObject(pattern); 
  pat->client = c; 
  listAddNodeTail(server.pubsub_patterns,pat); 
  } 
  /* Notify the client */ 
  addReply(c,shared.mbulkhdr[3]); 
  addReply(c,shared.psubscribebulk); 
  // 返回被订阅的模式 
  addReplyBulk(c,pattern); 
  // 返回客户端当前已订阅的频道和模式数量的总和 
  addReplyLongLong(c,dictSize(c->pubsub_channels)+listLength(c->pubsub_patterns)); 
  return retval; 
  }

  PUBLISH 命令的实现

  使用 PUBLISH 命令向订阅者发送消息,需要执行以下两个步骤:

  1) 使用给定的频道作为键,在 redisServer.pubsub_channels 字典中查找记录了订阅这个频道的所有客户端的链表,遍历这个链表,将消息发布给所有订阅者。

  2) 遍历 redisServer.pubsub_patterns 链表,将链表中的模式和给定的频道进行匹配,如果匹配成功,那么将消息发布到相应模式的客户端当中。

  举个例子,假设有两个客户端分别订阅 it.news 频道和 it.* 模式,当执行命令PUBLISH it.news "hello moto" 的时候, it.news 频道的订阅者会在步骤 1 收到信息,而当PUBLISH 进行到步骤 2 的时候, it.* 模式的订阅者也会收到信息。

  PUBLISH 命令的实际实现由 pubsubPublishMessage 函数完成,它的完整定义如下:

// 发送消息 
  int pubsubPublishMessage(robj *channel, robj *message) { 
  int receivers = 0; 
  struct dictEntry *de; 
  listNode *ln; 
  listIter li; 
  /* Send to clients listening for that channel */ 
  // 向所有频道的订阅者发送消息 
  de = dictFind(server.pubsub_channels,channel); 
  if (de) { 
  list *list = dictGetVal(de); // 取出所有订阅者 
  listNode *ln; 
  listIter li; 
  // 遍历所有订阅者, 向它们发送消息 
  listRewind(list,&li); 
  while ((ln = listNext(&li)) != NULL) { 
  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 */ 
  // 向所有被匹配模式的订阅者发送消息 
  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); // 释放用过的 channel 
  } 
  return receivers; // 返回接收者数量 
  }

  UNSUBSCRIBE 和 PUNSUBSCRIBE 的实现

  UNSUBSCRIBE 和 PUNSUBSCRIBE 分别是 SUBSCRIBE 和 PSUBSCRIBE 的反操作,如果明白了SUBSCRIBE 和 PSUBSCRIBE 的工作机制的话,应该不难理解这两个反操作的原理,所以这里就省略详细的分析了,有兴趣的可以直接看代码。

  小节

  Redis 的 pubsub 机制的分析就到此结束了,跟往常一样,带有注释的完整 pubsub.c 文件可以到我的 GITHUB 上找: https://github.com/huangz1990/reading_redis_source

Redis 发布/订阅机制原理分析的更多相关文章

  1. Redis发布订阅机制

    1. 什么是Redis Redis是一个开源的内存数据库,它以键值对的形式存储数据.由于数据存储在内存中,因此Redis的速度很快,但是每次重启Redis服务时,其中的数据也会丢失,因此,Redis也 ...

  2. 六. Redis发布订阅机制

    发布订阅(pub/sub)是一种消息通信模式,主要是解除消息发布者和消息订阅者之间通信的耦合. Redis作为一个pub/sub的服务器,在订阅者和发布者之间起到了一个消息路由的功能.订阅者可以通过s ...

  3. Redis发布订阅实现原理

    发布订阅中使用到的命令就只有三个:PUBLISH,SUBSCRIBE,PSUBSCRIBE PUBLISH 用于发布消息 SUBSCRIBE 也叫频道订阅,用于订阅某一特定的频道 PSUBSCRIBE ...

  4. nodejs redis 发布订阅机制封装

    最近项目使用redis,对publish 和 subscribe的使用进行了了解,并进行了封装. var config = require('../config/config'); var log = ...

  5. Redis进阶篇:发布订阅模式原理与运用

    "65 哥,如果你交了个漂亮小姐姐做女朋友,你会通过什么方式将这个消息广而告之给你的微信好友?" "那不得拍点女朋友的美照 + 亲密照弄一个九宫格图文消息在朋友圈发布大肆 ...

  6. 我在生产项目里是如何使用Redis发布订阅的?(一)使用场景

    转载请注明出处! 导语 Redis是我们很常用的一款nosql数据库产品,我们通常会用Redis来配合关系型数据库一起使用,弥补关系型数据库的不足. 其中,Redis的发布订阅功能也是它的一大亮点.虽 ...

  7. Redis 发布订阅,小功能大用处,真没那么废材!

    今天小黑哥来跟大家介绍一下 Redis 发布/订阅功能. 也许有的小伙伴对这个功能比较陌生,不太清楚这个功能是干什么的,没关系小黑哥先来举个例子. 假设我们有这么一个业务场景,在网站下单支付以后,需要 ...

  8. 【spring boot】【redis】spring boot 集成redis的发布订阅机制

    一.简单介绍 1.redis的发布订阅功能,很简单. 消息发布者和消息订阅者互相不认得,也不关心对方有谁. 消息发布者,将消息发送给频道(channel). 然后是由 频道(channel)将消息发送 ...

  9. Linux(6)- redis发布订阅/持久化/主从复制/redis-sentinel/redis-cluster、nginx入门

    一.redis发布订阅 Redis 通过 PUBLISH .SUBSCRIBE 等命令实现了订阅与发布模式. 其实从Pub/Sub的机制来看,它更像是一个广播系统,多个Subscriber可以订阅多个 ...

随机推荐

  1. 【计算机视觉领域】常用的 feature 提取方法,feature 提取工具包

    [计算机视觉领域]常用的 feature 提取方法,feature 提取工具包 利用 VL 工具包进行各种特征的提取: VL 工具包官网地址:http://www.vlfeat.org/index.h ...

  2. css2图片边框

    用父元素的背景作为边框图片 父元素的背景既可以是不平铺的成品边框图片,也可以是平铺的图片,填充子元素和父元素之间的padding 然后给子元素设置背景色或背景图覆盖掉父元素的背景图.

  3. UserDefault的使用,保存小数据到本地-iOS

    //保持到本地数据 NSArray *array=@[@"234",@"sdfe"]; NSUserDefaults *userDefault=[NSUserD ...

  4. 模仿ViewPager控件

    自定义控件是开发中经常使用的技术.系统中自带的ViewPager实现的功能有时候不能满足开发的需要,如ViewPager没有滑动图片时的动画切换效果.通过对 ViewPager的模仿和部分功能的加强, ...

  5. MARKDOWN--介绍http://www.jianshu.com/p/q81RER

    简       注册登录 添加关注 作者 简书2013.04.22 22:02* 写了267022字,被8398人关注,获得了9900个喜欢 献给写作者的 Markdown 新手指南 字数1600 阅 ...

  6. Java 笔试题集锦

    1.Java 不需要使用构造函数来创建对象的方法有哪些? 2.Java 创建多线程有哪些方法?你最喜欢哪一个?为什么? 3.Java

  7. unity htc vive, ugui for vr

    http://wacki.me/blog/2016/06/vr-gui-input-module-for-unity-htc-vive/

  8. RGB颜色对照图

  9. sql查询重复记录并取对应最小值

    原表(aa): id   a                      b 1    22                    456 2    22                    256 ...

  10. 封装类的方式访问数据库(封装字符串、json)

    <?php class DBDA { public $host="localhost";//服务器地址 public $uid="root";//用户名 ...