redis算是缓存界的老大哥了,最近做的事情对redis依赖较多,使用了里面的发布订阅功能,事务功能以及SortedSet等数据结构,后面准备好好学习总结一下redis的一些知识点。

原文地址:http://www.jianshu.com/p/8209554b36ce

先看下redis发布订阅的结构:

其中发布者跟订阅者之间通过channel进行交互,channel分为两种模式。

一、redis发布订阅命令简介

redis中为发布订阅(pub/sub)功能提供了六个命令,分为两种模式。

  1. 由subscribe,unsubscribe组成,它们是负责订阅有确定名称的channel,例如subscribe test表示订阅名字为test的channel。
  2. 由psubscribe,punsubscribe组成,是负责订阅模糊名字的channel,例如psubscribe test* 表示订阅所有以test开头的channel。

最后再加上发布命令publish以及查看订阅相关信息的pubsub命令组成。

二、redis发布订阅源码分析

redis所有的命令及其处理函数都放在了server.c文件的开头,从其中找出发布订阅功能相关的命令信息。

  1. {"subscribe",subscribeCommand,-2,"pslt",0,NULL,0,0,0,0,0},
  2. {"unsubscribe",unsubscribeCommand,-1,"pslt",0,NULL,0,0,0,0,0},
  3. {"psubscribe",psubscribeCommand,-2,"pslt",0,NULL,0,0,0,0,0},
  4. {"punsubscribe",punsubscribeCommand,-1,"pslt",0,NULL,0,0,0,0,0},
  5. {"publish",publishCommand,3,"pltF",0,NULL,0,0,0,0,0},
  6. {"pubsub",pubsubCommand,-2,"pltR",0,NULL,0,0,0,0,0},

这里可以看出创建一条命令需要很多参数,我们这里只需要关注前两个参数,第一个参数表示命令的内容,第二个表示该命令对应的处理函数。

普通模式订阅subscribe函数:

该命令支持多个参数,即subscribe channel1,channel2...

  1. void subscribeCommand(client *c) {
  2. int j;
  3. //这里挨个处理subscribe的参数,因为命令本身被作为参数0所以从1开始处理后面的参数
  4. for (j = 1; j < c->argc; j++)
  5. //订阅每个频道
  6. pubsubSubscribeChannel(c,c->argv[j]);
  7. //这里设置客户端的状态,下面会解释这个状态的作用
  8. c->flags |= CLIENT_PUBSUB;
  9. }

在server.c文件中,processCommand函数是在调用具体命令函数之前的判断逻辑,其中有一段:

  1. /* Only allow SUBSCRIBE and UNSUBSCRIBE in the context of Pub/Sub */
  2. if (c->flags & CLIENT_PUBSUB &&
  3. c->cmd->proc != pingCommand &&
  4. c->cmd->proc != subscribeCommand &&
  5. c->cmd->proc != unsubscribeCommand &&
  6. c->cmd->proc != psubscribeCommand &&
  7. c->cmd->proc != punsubscribeCommand) {
  8. addReplyError(c,"only (P)SUBSCRIBE / (P)UNSUBSCRIBE / PING / QUIT allowed in this context");
  9. return C_OK;
  10. }

这里注释也写的很清楚,就是当client处于pub/sub上下文时,只接收订阅相关命令以及一个ping命令,这就解释了上面subscribeCommand函数中为什么要设置客户端flag字段。

接下来看下订阅的具体逻辑:

  1. int pubsubSubscribeChannel(client *c, robj *channel) {
  2. dictEntry *de;
  3. list *clients = NULL;
  4. int retval = 0;
  5. //把指定channel加入到client的pubsub_channels哈希表中
  6. //不成功说明已经订阅了该频道
  7. if (dictAdd(c->pubsub_channels,channel,NULL) == DICT_OK) {
  8. retval = 1;
  9. //这里是把该channel加入到client的哈希表中,引用加1
  10. incrRefCount(channel);
  11. //在server的发布订阅哈希表中查找指定channel
  12. de = dictFind(server.pubsub_channels,channel);
  13. //如果该channel还不存在,则创建
  14. if (de == NULL) {
  15. //创建一个空list
  16. clients = listCreate();
  17. //把channel加入到server的哈希表中,value就是该channel的所有订阅者
  18. dictAdd(server.pubsub_channels,channel,clients);
  19. //该channel引用加1
  20. incrRefCount(channel);
  21. } else {
  22. clients = dictGetVal(de);
  23. }
  24. //把client加入到该channel的订阅列表中
  25. listAddNodeTail(clients,c);
  26. }
  27. //一系列通知客户端的操作
  28. addReply(c,shared.mbulkhdr[3]);
  29. addReply(c,shared.subscribebulk);
  30. addReplyBulk(c,channel);
  31. addReplyLongLong(c,clientSubscriptionsCount(c));
  32. return retval;
  33. }

总结一下,订阅其实就是把指定channel分别加入到client跟server的pub/sub哈希表中,然后在server端保存订阅了该channle的所有client列表,如下图:

下面看一下publish发布命令:

例如:publish channelName msg

  1. void publishCommand(client *c) {
  2. //发布逻辑
  3. int receivers = pubsubPublishMessage(c->argv[1],c->argv[2]);
  4. //这里是关于集群或者AOF的操作
  5. if (server.cluster_enabled)
  6. clusterPropagatePublish(c->argv[1],c->argv[2]);
  7. else
  8. forceCommandPropagation(c,PROPAGATE_REPL);
  9. //返回给client通知了的订阅者数
  10. addReplyLongLong(c,receivers);
  11. }

重点看下发布函数的源码:

  1. int pubsubPublishMessage(robj *channel, robj *message) {
  2. int receivers = 0;
  3. dictEntry *de;
  4. listNode *ln;
  5. listIter li;
  6. //根据上面的订阅源码,这里就是取出订阅该channel的所有clients
  7. de = dictFind(server.pubsub_channels,channel);
  8. if (de) {
  9. //获取client的链表
  10. list *list = dictGetVal(de);
  11. listNode *ln;
  12. listIter li;
  13. //由client链表创建它的迭代器,c++代码真是无力吐槽
  14. listRewind(list,&li);
  15. //遍历所有client并发送消息
  16. while ((ln = listNext(&li)) != NULL) {
  17. client *c = ln->value;
  18. addReply(c,shared.mbulkhdr[3]);
  19. addReply(c,shared.messagebulk);
  20. addReplyBulk(c,channel);
  21. addReplyBulk(c,message);
  22. receivers++;
  23. }
  24. }
  25. //开始模糊匹配的逻辑处理,模糊模式使用的是链表而不是哈希表,后面会讲
  26. if (listLength(server.pubsub_patterns)) {
  27. //创建模糊规则的迭代器li
  28. listRewind(server.pubsub_patterns,&li);
  29. channel = getDecodedObject(channel);
  30. //遍历所有的模糊模式,如果匹配成功则发送消息
  31. while ((ln = listNext(&li)) != NULL) {
  32. pubsubPattern *pat = ln->value;
  33. //判断当前channel是否可以匹配模糊规则
  34. if (stringmatchlen((char*)pat->pattern->ptr,
  35. sdslen(pat->pattern->ptr),
  36. (char*)channel->ptr,
  37. sdslen(channel->ptr),0)) {
  38. addReply(pat->client,shared.mbulkhdr[4]);
  39. addReply(pat->client,shared.pmessagebulk);
  40. addReplyBulk(pat->client,pat->pattern);
  41. addReplyBulk(pat->client,channel);
  42. addReplyBulk(pat->client,message);
  43. receivers++;
  44. }
  45. }
  46. decrRefCount(channel);
  47. }
  48. return receivers;
  49. }

从上面的publish处理函数可以看出每次进行消息发布的时候,都会向普通模式跟模糊模式发布消息,同时也能看出普通模式跟模糊模式使用的是两种不同的数据结构,下面看下模糊订阅模式。

模糊模式订阅psubscribe函数:

  1. //psubscribe命令对应的处理函数
  2. void psubscribeCommand(client *c) {
  3. int j;
  4. //挨个订阅client指定的pattern
  5. for (j = 1; j < c->argc; j++)
  6. pubsubSubscribePattern(c,c->argv[j]);
  7. //修改client状态
  8. c->flags |= CLIENT_PUBSUB;
  9. }
  10. int pubsubSubscribePattern(client *c, robj *pattern) {
  11. int retval = 0;
  12. //判断client是否已经订阅该pattern,这里与普通模式不同,是个链表
  13. if (listSearchKey(c->pubsub_patterns,pattern) == NULL) {
  14. retval = 1;
  15. pubsubPattern *pat;
  16. //把指定pattern加入到client的pattern链表中
  17. listAddNodeTail(c->pubsub_patterns,pattern);
  18. //引用计数+1
  19. incrRefCount(pattern);
  20. //这里是创建一个pattern对象,并指向该client,加入到server的pattern链表中
  21. //从这里可以看出,多个client订阅同一个pattern会创建多个patter对象,与普通模式不同
  22. pat = zmalloc(sizeof(*pat));
  23. pat->pattern = getDecodedObject(pattern);
  24. pat->client = c;
  25. listAddNodeTail(server.pubsub_patterns,pat);
  26. }
  27. //通知客户端
  28. addReply(c,shared.mbulkhdr[3]);
  29. addReply(c,shared.psubscribebulk);
  30. addReplyBulk(c,pattern);
  31. addReplyLongLong(c,clientSubscriptionsCount(c));
  32. return retval;
  33. }

通过分析上面的源码可以总结一下模糊订阅中的数据结构,如下图:

注:正如上面提到的,模糊模式中,一个pat对象中包含一个pattern规则跟一个client指针,也就是说当多个client模糊订阅同一个pattern时同样会为每个client都创建一个节点。

普通模式取消订阅unsubscribe函数:

取消就相对简单了,说白了就是把上面锁保存在server跟client端的数据删除。

  1. 取消订阅入口
  2. void unsubscribeCommand(client *c) {
  3. //如果该命令没有参数,则把channel全部取消
  4. if (c->argc == 1) {
  5. pubsubUnsubscribeAllChannels(c,1);
  6. } else {
  7. int j;
  8. //迭代取消置顶channel
  9. for (j = 1; j < c->argc; j++)
  10. pubsubUnsubscribeChannel(c,c->argv[j],1);
  11. }
  12. //如果channel被全部取消,则修改client状态,这样client就可以发送其他命令了
  13. if (clientSubscriptionsCount(c) == 0) c->flags &= ~CLIENT_PUBSUB;
  14. }
  15. //一次性取消订阅所有channel
  16. int pubsubUnsubscribeAllChannels(client *c, int notify) {
  17. //取出client端所有的channel
  18. dictIterator *di = dictGetSafeIterator(c->pubsub_channels);
  19. dictEntry *de;
  20. int count = 0;
  21. while((de = dictNext(di)) != NULL) {
  22. robj *channel = dictGetKey(de);
  23. //最终也是挨个取消channel
  24. count += pubsubUnsubscribeChannel(c,channel,notify);
  25. }
  26. //如果client上面都没有订阅,依然返回响应
  27. if (notify && count == 0) {
  28. addReply(c,shared.mbulkhdr[3]);
  29. addReply(c,shared.unsubscribebulk);
  30. addReply(c,shared.nullbulk);
  31. addReplyLongLong(c,dictSize(c->pubsub_channels)+
  32. listLength(c->pubsub_patterns));
  33. }
  34. //释放空间
  35. dictReleaseIterator(di);
  36. return count;
  37. }
  38. //取消订阅指定channel
  39. int pubsubUnsubscribeChannel(client *c, robj *channel, int notify) {
  40. dictEntry *de;
  41. list *clients;
  42. listNode *ln;
  43. int retval = 0;
  44. //从client中删除指定channel
  45. if (dictDelete(c->pubsub_channels,channel) == DICT_OK) {
  46. retval = 1;
  47. //删除服务端该channel中的指定client
  48. de = dictFind(server.pubsub_channels,channel);
  49. serverAssertWithInfo(c,NULL,de != NULL);
  50. clients = dictGetVal(de);
  51. ln = listSearchKey(clients,c);
  52. serverAssertWithInfo(c,NULL,ln != NULL);
  53. listDelNode(clients,ln);
  54. if (listLength(clients) == 0) {
  55. //如果删除完以后channel没有了订阅者,则把channel也删除
  56. dictDelete(server.pubsub_channels,channel);
  57. }
  58. }
  59. //返回client响应
  60. if (notify) {
  61. addReply(c,shared.mbulkhdr[3]);
  62. addReply(c,shared.unsubscribebulk);
  63. addReplyBulk(c,channel);
  64. addReplyLongLong(c,dictSize(c->pubsub_channels)+
  65. listLength(c->pubsub_patterns));
  66. }
  67. //引用计数-1
  68. decrRefCount(channel);
  69. return retval;
  70. }

由于模糊模式的取消订阅与普通模式类似,这里就不再贴代码了。

三、redis发布订阅总结

整个发布订阅的代码比较简单清晰,一个值得思考的问题时普通模式跟模糊模式中分别使用了哈希表跟链表两种结构进行处理,而不是统一的,原因在于模糊模式不能精确匹配,需要遍历挨个判断,而哈希表的优势在于快速定位查找,在需要遍历跟模糊匹配的场景中并不适用。

redis源码分析之发布订阅(pub/sub)的更多相关文章

  1. redis源码分析之事务Transaction(下)

    接着上一篇,这篇文章分析一下redis事务操作中multi,exec,discard三个核心命令. 原文地址:http://www.jianshu.com/p/e22615586595 看本篇文章前需 ...

  2. Redis源码分析:serverCron - redis源码笔记

    [redis源码分析]http://blog.csdn.net/column/details/redis-source.html   Redis源代码重要目录 dict.c:也是很重要的两个文件,主要 ...

  3. redis源码分析之事务Transaction(上)

    这周学习了一下redis事务功能的实现原理,本来是想用一篇文章进行总结的,写完以后发现这块内容比较多,而且多个命令之间又互相依赖,放在一篇文章里一方面篇幅会比较大,另一方面文章组织结构会比较乱,不容易 ...

  4. redis源码分析之有序集SortedSet

    有序集SortedSet算是redis中一个很有特色的数据结构,通过这篇文章来总结一下这块知识点. 原文地址:http://www.jianshu.com/p/75ca5a359f9f 一.有序集So ...

  5. Dubbo2.7源码分析-如何发布服务

    Dubbo的服务发布逻辑是比较复杂的,我还是以Dubbo自带的示例讲解,这样更方便和容易理解. Provider配置如下: <?xml version="1.0" encod ...

  6. Nacos源码分析-事件发布机制

    温馨提示: 本文内容基于个人学习Nacos 2.0.1版本代码总结而来,因个人理解差异,不保证完全正确.如有理解错误之处欢迎各位拍砖指正,相互学习:转载请注明出处. Nacos的服务注册.服务变更等功 ...

  7. SOFA 源码分析 —— 服务发布过程

    前言 SOFA 包含了 RPC 框架,底层通信框架是 bolt ,基于 Netty 4,今天将通过 SOFA-RPC 源码中的例子,看看他是如何发布一个服务的. 示例代码 下面的代码在 com.ali ...

  8. Redis源码分析(intset)

    源码版本:4.0.1 源码位置: intset.h:数据结构的定义 intset.c:创建.增删等操作实现 1. 整数集合简介 intset是Redis内存数据结构之一,和之前的 sds. skipl ...

  9. Redis源码分析(dict)

    源码版本:redis-4.0.1 源码位置: dict.h:dictEntry.dictht.dict等数据结构定义. dict.c:创建.插入.查找等功能实现. 一.dict 简介 dict (di ...

随机推荐

  1. AngularJS的运用

      前  言 JRedu AngularJS[1]  诞生于2009年,由Misko Hevery 等人创建,后为Google所收购.是一款优秀的前端JS框架,已经被用于Google的多款产品当中.A ...

  2. 数据库服务器构建和部署列表(For SQL Server 2012)

    前言 我们可能经常安装和部署数据库服务器,但是可能突然忘记了某个设置,为后来的运维造成隐患.下面是国外大牛整理的的检查列表. 其实也包含了很多我们平时数据库配置的最佳实践.比如TEMPDB 文件的个数 ...

  3. java集合系列——List集合之LinkedList介绍(三)

    1. LinkedList的简介 JDK 1.7 LinkedList是基于链表实现的,从源码可以看出是一个双向链表.除了当做链表使用外,它也可以被当作堆栈.队列或双端队列进行操作.不是线程安全的,继 ...

  4. mint-ui vue双向绑定

    由于最近项目需求,用上了mint-ui来重构移动端页面,从框架本身来讲我觉得很强大了,用起来也很不错,但是文档就真的是,,,,让我无言以对,给的api对于我们这些小菜鸟来讲真的是处处是坑呀(ps:用v ...

  5. http://codeforces.com/problemset/problem/712/D

    D. Memory and Scores time limit per test 2 seconds memory limit per test 512 megabytes input standar ...

  6. spring 内部工作机制(二)

    本章节讲Spring容器从加载配置文件到创建出一个完整Bean的作业流程及参与的角色. Spring 启动时读取应用程序提供的Bean配置信息,并在Spring容器中生成一份相应的Bean配置注册表, ...

  7. 网时|云计算的集群技术对于传统IDC而言,又有哪些提高呢?

    当传统的IDC产品已经不足以满足现在科技的飞速发展时,云计算便应运而生.咱们暂且不论云计算在其他领域的贡献,仅IDC来讲,云计算的集群技术对于传统IDC而言,又有哪些提高呢? 1.服务类型 常用的传统 ...

  8. springboot使用zookeeper(curator)实现注册发现与负载均衡

    最简单的实现服务高可用的方法就是集群化,也就是分布式部署,但是分布式部署会带来一些问题.比如: 1.各个实例之间的协同(锁) 2.负载均衡 3.热删除 这里通过一个简单的实例来说明如何解决注册发现和负 ...

  9. 选择排序的3种语言实现方法(C java python)

    1.选择排序的思路是:遍历数组,第一遍找出所有成员的最小值,放到数组下标为0的位置,第二遍从剩余内容中,再次找出最小值,放到数组下标为1的位置,以此类推,遍历完成所有的数组内容,最后结果就是:数组是按 ...

  10. iOS开发之AutoLayout中的Content Hugging Priority和 Content Compression Resistance Priority解析

    本篇博客的内容也不算太复杂,算是AutoLayout的一些高级的用法.本篇博客我们主要通过一些示例来看一下AutoLayout中的Content Hugging Priority以及Content C ...