谈到发布订阅模式,相信不会陌生,典型的观察者模式的实现。然而从表面来看,本地实现一个wait/notify通知、register/update调用, 实现一个远程mq服务, 还有本文说的 pub/sub, 其实道理都差不多。只是,同样的需求,针对不同的环境,实现上往往是有天壤之别的。

  所以,我们就来看看 redis 的 pub/sub 是如何实现的吧!

零、redis发布订阅相关概念介绍

  Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。Redis 客户端可以订阅任意数量的频道。

  下图展示了频道 channel1,以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系:

  当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:

  Redis的pub/sub实现中,发布消息的方式只有一种,但是订阅消息却有很多种方式。

  使用场景如: 可以用做简单消息通信中间件,监听某些事件的变化;

  从官方手册上查到相关使用方法。

1> PUBLISH channel message
功能: 将信息发送到指定的频道。
返回值: 接收到该消息的个数;

2> SUBSCRIBE channel [channel ...]
功能: 订阅给定的一个或多个频道的信息。
返回值: 等待消息状态,客户端不能再处理其他命令了。 除了 SUBSCRIBE, PSUBSCRIBE, UNSUBSCRIBE, PUNSUBSCRIBE, PING and QUIT commands.

3> PSUBSCRIBE pattern [pattern ...]
功能: 订阅一个或多个符合给定模式的频道。
返回值: 等待消息状态,客户端不能再处理其他命令了。 除了 SUBSCRIBE, PSUBSCRIBE, UNSUBSCRIBE, PUNSUBSCRIBE, PING and QUIT commands.

4> PUBSUB subcommand [argument [argument ...]]
功能: 查看订阅与发布系统状态。subcomand 有 CHANNELS,NUMSUB,NUMPAT .
返回值:
PUBSUB CHANNELS [pattern] 列举出所有至少有一个订阅者的符合表达式的channel(精确订阅的客户端,即使用 SUBSCRIBE 进行订阅的客户端);
PUBSUB NUMSUB [channel-1 ... channel-N] 每个要查询的channel的订阅数kv(精确订阅的客户端,即使用 SUBSCRIBE 进行订阅的客户端);
PUBSUB NUMPAT 返回使用了 PSUBSCRIBE 订阅的客户端总数;
5> UNSUBSCRIBE [channel [channel ...]]
功能: 指退订给定的频道。
返回值: 退订频道的影响订阅数,自身未订阅时,影响数为0;

6> PUNSUBSCRIBE [pattern [pattern ...]]
功能: 退订所有给定模式的频道。
返回值: 退订频道的影响订阅数,自身未订阅时,影响数为0;

  以上命令的操作,当使用 redis-cli 时,将受限。使用 SUBSCRIBE/PSUBSCRIBE 订阅channel后,只能强行退出,不能再接受其他命令。即只有配合各语言实现的sdk,才能连贯完成上面完整的操作。

一、pub/sub 相关数据结构

  pub/sub 相关接口定义如下:

  1. {"subscribe",subscribeCommand,-,"rpslt",,NULL,,,,,},
  2. {"unsubscribe",unsubscribeCommand,-,"rpslt",,NULL,,,,,},
  3. {"psubscribe",psubscribeCommand,-,"rpslt",,NULL,,,,,},
  4. {"punsubscribe",punsubscribeCommand,-,"rpslt",,NULL,,,,,},
  5. {"publish",publishCommand,,"pltrF",,NULL,,,,,},
  6. {"pubsub",pubsubCommand,-,"pltrR",,NULL,,,,,},

  整个pub/sub使用的数据结构,都是之前介绍过的。主要有  dict, list 两种,针对模式匹配订阅稍微多了个属性:

  1. // 使用 PSUBSCRIBE 订阅方式,做一层数据格式封装
  2. typedef struct pubsubPattern {
  3. client *client;
  4. robj *pattern;
  5. } pubsubPattern;

  

二、subscribe/psubscribe 订阅channel实现

  只有先有订阅者之后,发布者发送的消息才会有意义。所以我们先看看订阅的实现:

  1. // 用法: SUBSCRIBE channel [channel ...]
  2. // pubsub.c
  3. void subscribeCommand(client *c) {
  4. int j;
  5. // n 个channel 的订阅,循环调用即可
  6. for (j = ; j < c->argc; j++)
  7. pubsubSubscribeChannel(c,c->argv[j]);
  8. // 添加pubsub订阅标识,方便其他地方判断
  9. c->flags |= CLIENT_PUBSUB;
  10. }
  11. // 具体的单个 channel 订阅实现
  12. /* Subscribe a client to a channel. Returns 1 if the operation succeeded, or
  13. * 0 if the client was already subscribed to that channel. */
  14. int pubsubSubscribeChannel(client *c, robj *channel) {
  15. dictEntry *de;
  16. list *clients = NULL;
  17. int retval = ;
  18.  
  19. /* Add the channel to the client -> channels hash table */
  20. // step1. 将要订阅的 channel 添加到各自客户端的 pubsub_channels 容器中
  21. if (dictAdd(c->pubsub_channels,channel,NULL) == DICT_OK) {
  22. retval = ;
  23. incrRefCount(channel);
  24. /* Add the client to the channel -> list of clients hash table */
  25. // step2. 将要订阅的channel 添加到 server.pubsub_channels 中, 方便在publish时判定是否触发通知
  26. de = dictFind(server.pubsub_channels,channel);
  27. if (de == NULL) {
  28. clients = listCreate();
  29. dictAdd(server.pubsub_channels,channel,clients);
  30. incrRefCount(channel);
  31. } else {
  32. clients = dictGetVal(de);
  33. }
  34. // step3. 将客户端自身添加到相应的 server.pubsub_channels 对应的队列中去, 在通知时只需遍历该队列即可
  35. listAddNodeTail(clients,c);
  36. }
  37. /* Notify the client */
  38. // 响应客户端:
  39. // *3 \r\n
  40. // $9\r\nsubscribe\r\n
  41. // channel
  42. // 111(该客户端总共订阅的channel数)
  43. addReply(c,shared.mbulkhdr[]);
  44. addReply(c,shared.subscribebulk);
  45. addReplyBulk(c,channel);
  46. addReplyLongLong(c,clientSubscriptionsCount(c));
  47. return retval;
  48. }
  49. // 客户端订阅的总channel数, 两种订阅方式相加
  50. /* Return the number of channels + patterns a client is subscribed to. */
  51. int clientSubscriptionsCount(client *c) {
  52. return dictSize(c->pubsub_channels)+
  53. listLength(c->pubsub_patterns);
  54. }

  如上就是单个channel的订阅方式了,总结如下:

    1. 客户端自行管理需要订阅的channel, 放到 c->pubsub_channels 中;
    2. redis使用的一个统一的 server->pubsub_channels dict容器进行管理所有的channel;
    3. 对于多个客户端订阅一个channel, redis 使用list进行管理追加;

  整个订阅过程,其实就是一个注册的过程,自然复杂不到哪里去。接下来,我们同步来看一下 使用模式订阅的方式的注册如何?

  1. // 用法: PSUBSCRIBE pattern [pattern ...]
  2. // pubsub.c
  3. void psubscribeCommand(client *c) {
  4. int j;
  5. // 同样是n个channel依次注册
  6. for (j = ; j < c->argc; j++)
  7. pubsubSubscribePattern(c,c->argv[j]);
  8. c->flags |= CLIENT_PUBSUB;
  9. }
  10. // 注册单个模式匹配的 channel 订阅
  11. /* Subscribe a client to a pattern. Returns 1 if the operation succeeded, or 0 if the client was already subscribed to that pattern. */
  12. int pubsubSubscribePattern(client *c, robj *pattern) {
  13. int retval = ;
  14. // 直接查找对应的 pattern, 没有则添加
  15. if (listSearchKey(c->pubsub_patterns,pattern) == NULL) {
  16. retval = ;
  17. pubsubPattern *pat;
  18. listAddNodeTail(c->pubsub_patterns,pattern);
  19. incrRefCount(pattern);
  20. pat = zmalloc(sizeof(*pat));
  21. pat->pattern = getDecodedObject(pattern);
  22. pat->client = c;
  23. listAddNodeTail(server.pubsub_patterns,pat);
  24. }
  25. /* Notify the client */
  26. addReply(c,shared.mbulkhdr[]);
  27. addReply(c,shared.psubscribebulk);
  28. addReplyBulk(c,pattern);
  29. addReplyLongLong(c,clientSubscriptionsCount(c));
  30. return retval;
  31. }

  PSUBSCRIBE 的管理方式与 SUBSCRIBE 的管理方式不一样,它是直接使用list保存订阅的模式到 server.pubsub_patterns 中,针对不一样的模式,使用一个新的pubsubPattern来保存。

  注意:所有客户端的订阅管理,server.pubsub_patterns 使用平坦式管理,即相同的模式订阅,有多少个客户端,就会有多个元素被添加到 pubsub_patterns 中。(为什么不使用子链表的方式进行管理呢???)

三、publish 发布消息的实现

  publish 是触发subscribe的方式,没有publish动作,subscribe就会一直在等待中。想来应该不难,消息发布之后,只要将注册上来的客户一个进行消息推送,就实现了相应功能。所以,pub/sub 操作,必然是基于长连接的实现方式,没毛病。

  redis 的发布命令如下:

  1. // 用法: PUBLISH channel message
  2. // pubsub.c
  3. void publishCommand(client *c) {
  4. // 使用 channel+message 进行发布消息
  5. int receivers = pubsubPublishMessage(c->argv[],c->argv[]);
  6. // 命令传播
  7. if (server.cluster_enabled)
  8. clusterPropagatePublish(c->argv[],c->argv[]);
  9. else
  10. forceCommandPropagation(c,PROPAGATE_REPL);
  11. addReplyLongLong(c,receivers);
  12. }
  13. // 发布一条消息
  14. /* Publish a message */
  15. int pubsubPublishMessage(robj *channel, robj *message) {
  16. int receivers = ;
  17. dictEntry *de;
  18. listNode *ln;
  19. listIter li;
  20.  
  21. /* Send to clients listening for that channel */
  22. // 使用 SUBSCRIBE 订阅的客户端,直接遍历相应的 channel集合即可
  23. de = dictFind(server.pubsub_channels,channel);
  24. if (de) {
  25. list *list = dictGetVal(de);
  26. listNode *ln;
  27. listIter li;
  28. // 依次进行数据响应,将消息传播到订阅端
  29. listRewind(list,&li);
  30. while ((ln = listNext(&li)) != NULL) {
  31. client *c = ln->value;
  32.  
  33. addReply(c,shared.mbulkhdr[]);
  34. addReply(c,shared.messagebulk);
  35. addReplyBulk(c,channel);
  36. addReplyBulk(c,message);
  37. receivers++;
  38. }
  39. }
  40. /* Send to clients listening to matching channels */
  41. // 处理使用 PSUBSCRIBE 订阅消息的客户端
  42. // 前面说过, PSUBSCRIBE 在redis使用平坦式管理,所以需要做的模式匹配将会更多
  43. // 也就是说 PSUBSCRIBE 的响应性能也会更差
  44. if (listLength(server.pubsub_patterns)) {
  45. listRewind(server.pubsub_patterns,&li);
  46. channel = getDecodedObject(channel);
  47. while ((ln = listNext(&li)) != NULL) {
  48. pubsubPattern *pat = ln->value;
  49.  
  50. if (stringmatchlen((char*)pat->pattern->ptr,
  51. sdslen(pat->pattern->ptr),
  52. (char*)channel->ptr,
  53. sdslen(channel->ptr),)) {
  54. addReply(pat->client,shared.mbulkhdr[]);
  55. addReply(pat->client,shared.pmessagebulk);
  56. addReplyBulk(pat->client,pat->pattern);
  57. addReplyBulk(pat->client,channel);
  58. addReplyBulk(pat->client,message);
  59. receivers++;
  60. }
  61. }
  62. decrRefCount(channel);
  63. }
  64. return receivers;
  65. }

  可以看到,redis消息的发布可能比想象中的还要简单。不过有一点需要注意的是,整个publish的消息并没有在redis进行存储操作,也就是说发布完一个消息之后,就再也找不到踪迹了。这也是很多消息中间件的实现方式,因为数据的保留可能会显得没有意义。

  整个发布消息的过程,其实就是向各个subscriber进行数据推送的过程,而这些scriber则是基于长连接客户端实例,以至于其看起来和本地实现的register/update 的观察者模块没啥两样。

  所以,基本上 redis 的发布订阅功能实现得,还是实现的粒度还是比较粗的。系统上的应用如哨兵模式下的master/slave的切换。而如果自己应用的话,就需要找准自己的应用场景,不要乱用了。

四、unsubscribe 解除订阅关系

  当关注的事件处理完成后,可能就不需要再订阅相关消息了,就需要进行解决订阅。解决订阅关系,即不再接受相应的发布消息,将自身从注册表中删除即可。基本上就是和订阅进行一个反解操作!

  1. // 用法: UNSUBSCRIBE [channel [channel ...]]
  2. // pubsub.c
  3. void unsubscribeCommand(client *c) {
  4. // unsubscribe, 直接解决所有的订阅
  5. if (c->argc == ) {
  6. pubsubUnsubscribeAllChannels(c,);
  7. } else {
  8. int j;
  9. // 根据指定的 channel 依次解除订阅关系
  10. for (j = ; j < c->argc; j++)
  11. pubsubUnsubscribeChannel(c,c->argv[j],);
  12. }
  13. // 当一个订阅也没有,则自身不再处理 pubsub 相关的事务
  14. if (clientSubscriptionsCount(c) == ) c->flags &= ~CLIENT_PUBSUB;
  15. }
  16. /* Unsubscribe a client from a channel. Returns 1 if the operation succeeded, or
  17. * 0 if the client was not subscribed to the specified channel. */
  18. int pubsubUnsubscribeChannel(client *c, robj *channel, int notify) {
  19. dictEntry *de;
  20. list *clients;
  21. listNode *ln;
  22. int retval = ;
  23.  
  24. /* Remove the channel from the client -> channels hash table */
  25. incrRefCount(channel); /* channel may be just a pointer to the same object
  26. we have in the hash tables. Protect it... */
  27. // 先删除自身的订阅标识,再删除 server.pubsub_channels 标识
  28. if (dictDelete(c->pubsub_channels,channel) == DICT_OK) {
  29. retval = ;
  30. /* Remove the client from the channel -> clients list hash table */
  31. de = dictFind(server.pubsub_channels,channel);
  32. serverAssertWithInfo(c,NULL,de != NULL);
  33. clients = dictGetVal(de);
  34. ln = listSearchKey(clients,c);
  35. serverAssertWithInfo(c,NULL,ln != NULL);
  36. listDelNode(clients,ln);
  37. if (listLength(clients) == ) {
  38. /* Free the list and associated hash entry at all if this was
  39. * the latest client, so that it will be possible to abuse
  40. * Redis PUBSUB creating millions of channels. */
  41. dictDelete(server.pubsub_channels,channel);
  42. }
  43. }
  44. /* Notify the client */
  45. // 调用 unsubscribe 进行解决订阅的,此处都需要进行客户端响应通知
  46. if (notify) {
  47. addReply(c,shared.mbulkhdr[]);
  48. addReply(c,shared.unsubscribebulk);
  49. addReplyBulk(c,channel);
  50. addReplyLongLong(c,dictSize(c->pubsub_channels)+
  51. listLength(c->pubsub_patterns));
  52.  
  53. }
  54. decrRefCount(channel); /* it is finally safe to release it */
  55. return retval;
  56. }
  57. // 解决所有的当前客户端的订阅关系 (SUBSCRIBE 建立的订阅)
  58. /* Unsubscribe from all the channels. Return the number of channels the
  59. * client was subscribed to. */
  60. int pubsubUnsubscribeAllChannels(client *c, int notify) {
  61. dictIterator *di = dictGetSafeIterator(c->pubsub_channels);
  62. dictEntry *de;
  63. int count = ;
  64. // 迭代 c->pubsub_channels 的订阅,依次删除即可
  65. while((de = dictNext(di)) != NULL) {
  66. robj *channel = dictGetKey(de);
  67.  
  68. count += pubsubUnsubscribeChannel(c,channel,notify);
  69. }
  70. /* We were subscribed to nothing? Still reply to the client. */
  71. if (notify && count == ) {
  72. addReply(c,shared.mbulkhdr[]);
  73. addReply(c,shared.unsubscribebulk);
  74. addReply(c,shared.nullbulk);
  75. addReplyLongLong(c,dictSize(c->pubsub_channels)+
  76. listLength(c->pubsub_patterns));
  77. }
  78. dictReleaseIterator(di);
  79. return count;
  80. }

  不出所料,就是一个从 pubsub_channels 中的删除一个元素的问题,别无其他。其中需要注意的是,SUBSCRIBE 对应 UNSUBSCRIBE, PSUBSCRIBE 对应 PUNSUBSCRIBE。

五、关于redis pub/sub 之后的思考

  需要注意的是,消息中间件是远程通信组件,必然存在各种不确定性,所以确保长连接的有效性是非常重要,比如通过PING-PONG方式进行续租,以保持连接的有效性。

  可以说,我们要实现一个简单的pub/sub 功能是简单的,但是要应对各种异常情况则是困难的。

    1. 比如当订阅的量越来越大时,整个发布消息过程可能变量缓慢起来,如何处理?
    2. 如果消费者端处理失败,如何处理?
    3. 订阅者为什么只能做很少的事情,能不能多做一点?
    4. 出现问题时如何进行溯源?
    5. 如何处理单机瓶颈问题?
    6. 如果是多机负载,如何处理数据一致性问题?
    7. 消费者事务处理能力问题?

  redis是专业的缓存解决方案,但不是专业的消息通信解决方案,它的实现只能为打开我们的一点思路。我们还是要相信专业的力量,以上问题相信在很多消息中间件中很容易找到相应答案。

Redis(十):pub/sub 发布订阅源码解析的更多相关文章

  1. Redis系列(十):数据结构Set源码解析和SADD、SINTER、SDIFF、SUNION、SPOP命令

    1.介绍 Hash是以K->V形式存储,而Set则是K存储,空间节省了很多 Redis中Set是String类型的无序集合:集合成员是唯一的. 这就意味着集合中不能出现重复的数据.可根据应用场景 ...

  2. PHP使用Redis的Pub/Sub(发布订阅)命令

    1.概念 名称 含义 channel 频道:生产者和消费者直接操作的对象 publish 生产者:向channel发送消息 subscribe 消费者:订阅一个或多个channel psubscrib ...

  3. spring源码解析前瞻

    很多人有疑问:为什么要读源码?读源码有什么用?我也一直问自己这些问题,读源码非常枯燥,工作中又用不到,慢慢的自己读源码越发现自己知识的不足,无法把知识串起来,形成知识体系.从单系统中常用的Spring ...

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

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

  5. 第三百零一节,python操作redis缓存-管道、发布订阅

    python操作redis缓存-管道.发布订阅 一.管道 redis-py默认在执行每次请求都会创建(连接池申请连接)和断开(归还连接池)一次连接操作,如果想要在一次请求中指定多个命令,则可以使用pi ...

  6. 基于Redis缓存的Session共享(附源码)

    基于Redis缓存的Session共享(附源码) 在上一篇文章中我们研究了Redis的安装及一些基本的缓存操作,今天我们就利用Redis缓存实现一个Session共享,基于.NET平台的Seesion ...

  7. 第三十六节,目标检测之yolo源码解析

    在一个月前,我就已经介绍了yolo目标检测的原理,后来也把tensorflow实现代码仔细看了一遍.但是由于这个暑假事情比较大,就一直搁浅了下来,趁今天有时间,就把源码解析一下.关于yolo目标检测的 ...

  8. .Net Core缓存组件(Redis)源码解析

    上一篇文章已经介绍了MemoryCache,MemoryCache存储的数据类型是Object,也说了Redis支持五中数据类型的存储,但是微软的Redis缓存组件只实现了Hash类型的存储.在分析源 ...

  9. 第十四章 Executors源码解析

    前边两章介绍了基础线程池ThreadPoolExecutor的使用方式.工作机理.参数详细介绍以及核心源码解析. 具体的介绍请参照: 第十二章 ThreadPoolExecutor使用与工作机理 第十 ...

随机推荐

  1. Linux环境下Mysql开启远程访问权限

    如何通过Window的数据库可视化工具远程连接装在Linux上的Mysql呢?可以按照以下步骤逐一确认! 检查监听端口是否存在 netstat -ntlp |grep mysql 检查Mysql配置配 ...

  2. Scrum.自用SCRUM项目开发流程

    目前团队所使用的开发模式流程,请大家指正优化. 1.PM需求整理:在禅道整理出需求清单和原型图(标注清楚):需求评审.往复,直到需求定稿. 2.SprintMaster根据需求清单进行技术任务拆解,包 ...

  3. 从0开发3D引擎(八):准备“搭建引擎雏形”

    大家好,现在开始本系列的第三部分,按照以下几个步骤来搭建引擎雏形: 1.分析引擎的需求 2.实现最小的3D程序 3.从中提炼引擎原型 4.一步一步地对引擎进行改进,使其具备良好的架构 5.实现与架构相 ...

  4. JS-05-元素获取

    <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...

  5. 深入理解Java 中SPI 制

    深入理解Java 中SPI 制 概述 SPI(Service Provider Interface),是JDK内置的一种服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用,比 ...

  6. ios--->NStimer用法

    NStimer用法 1.初始化 (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL ...

  7. Macbook 安装 opencv(cv2) 及在pycharm 下的使用

    python和opencv的安装都很顺利,就是在PyCharm下的配置浪费了一点时间. 一.原料 1.max系统 2.python(本文用的版本是3.6.5) 3.opencv(本文中使用的版本是3. ...

  8. RocketMQ消息模型

    rocketmq采用的是发布-订阅的模式,不需要每个消费者维护自己的消息队列,生产者将消息发送到topic,消费者订阅此topic 读取消息. 基本概念: 消息模型:消息模型包括producer,co ...

  9. CTF--HTTP服务--路径遍历(提升root用户权限)

    开门见山 1. 在上次实验取的靶机低用户权限,查看该系统的内核版本 2. 查看该系统的发行版本 3. 查看该内核版本是否存在内核溢出漏洞,并没有 如果有内核溢出漏洞,则可以 4. 在靶机查看/etc/ ...

  10. Spring实战:第一个spring mvc项目

    我自己看的pdf书中有几个小错误导致项目一直起不来,具体错误是: 此处的名称不一致导致的,此外对于映射@RequestMapping("/"),需要删除创建框架时自带的index. ...