redis 发布订阅实现

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介:  redis的pub/sub当中主要分为subscribe/unsubscribe&psubscribe/punsubscribe两种模式,在这两种模式下支持publish命令。

 redis的pub/sub当中主要分为subscribe/unsubscribe&psubscribe/punsubscribe两种模式,在这两种模式下支持publish命令。
 本篇文章的主要目的是为了讲解清楚这三个过程,包括整个执行过程以及相关的数据结构。

subscribe 数据结构

 首先subscribe的数据结构主要如下图,其中subscribe对应的pubsub_channels其实是一个hashtable的数据结构,其中key为channel的名字,value为订阅了这个channel的客户端连接。

img_0f0f30678846a607cbaa06ccdae4d212.png
subscribe数据结构


subscribe 过程

 当我们执行subscribe channelA channelB的命令的时候,在内部执行的时候实际上是分两次执行的,从下面的for循环我们可以看出来,相当于对同一个client的多个subscribe命令一次进行订阅处理。

void subscribeCommand(redisClient *c) {
    int j;

    for (j = 1; j < c->argc; j++)
        pubsubSubscribeChannel(c,c->argv[j]);
}



 在执行pubsubSubscribeChannel的整个过程分为以下几个阶段:

  • 将channel关联到client当中的pubsub_channels当中
  • 根据channel去server的pubsub_channels中查找对应的client的list列表
  • 不存在就创建client的list列表并添加到hashtable当中
  • 将client添加到channel对应的client list当中
/* 
 * 设置客户端 c 订阅频道 channel 。
 *
 * 订阅成功返回 1 ,如果客户端已经订阅了该频道,那么返回 0 。
 */
int pubsubSubscribeChannel(redisClient *c, robj *channel) {
    dictEntry *de;
    list *clients = NULL;
    int retval = 0;

    /* Add the channel to the client -> channels hash table */
    // 将 channels 填接到 c->pubsub_channels 的集合中(值为 NULL 的字典视为集合)
    if (dictAdd(c->pubsub_channels,channel,NULL) == DICT_OK) {
        retval = 1;
        incrRefCount(channel);

        // 关联示意图
        // {
        //  频道名        订阅频道的客户端
        //  'channel-a' : [c1, c2, c3],
        //  'channel-b' : [c5, c2, c1],
        //  'channel-c' : [c10, c2, c1]
        // }
        /* Add the client to the channel -> list of clients hash table */
        // 从 pubsub_channels 字典中取出保存着所有订阅了 channel 的客户端的链表
        // 如果 channel 不存在于字典,那么添加进去
        de = dictFind(server.pubsub_channels,channel);
        if (de == NULL) {
            clients = listCreate();
            dictAdd(server.pubsub_channels,channel,clients);
            incrRefCount(channel);
        } else {
            clients = dictGetVal(de);
        }

        // before:
        // 'channel' : [c1, c2]
        // after:
        // 'channel' : [c1, c2, c3]
        // 将客户端添加到链表的末尾
        listAddNodeTail(clients,c);
    }

    /* Notify the client */
    // 回复客户端。
    // 示例:
    // redis 127.0.0.1:6379> SUBSCRIBE xxx
    // Reading messages... (press Ctrl-C to quit)
    // 1) "subscribe"
    // 2) "xxx"
    // 3) (integer) 1
    addReply(c,shared.mbulkhdr[3]);
    // "subscribe\n" 字符串
    addReply(c,shared.subscribebulk);
    // 被订阅的客户端
    addReplyBulk(c,channel);
    // 客户端订阅的频道和模式总数
    addReplyLongLong(c,dictSize(c->pubsub_channels)+listLength(c->pubsub_patterns));

    return retval;
}
     /* Pubsub */
    // 字典,键为频道,值为链表
    // 链表中保存了所有订阅某个频道的客户端
    // 新客户端总是被添加到链表的表尾
    dict *pubsub_channels;  /* Map channels to list of subscribed clients */

    // 这个链表记录了客户端订阅的所有模式的名字
    list *pubsub_patterns;  /* A list of pubsub_patterns */


unsubscribe 过程

 在unsubscribeCommand过程中,区分了两种场景,一个是取消client相关的所有订阅,一个是取消client关联的某几个订阅,但是殊途同归最终都会执行pubsubUnsubscribeChannel取消订阅的。

void unsubscribeCommand(redisClient *c) {
    if (c->argc == 1) {
        pubsubUnsubscribeAllChannels(c,1);
    } else {
        int j;

        for (j = 1; j < c->argc; j++)
            pubsubUnsubscribeChannel(c,c->argv[j],1);
    }
}



 在取消client的所有订阅的过程中,我们通过遍历client的pubsub_channels的所有channel,然后依次进行取消订阅操作。

/* 
 * 退订客户端 c 订阅的所有频道。
 *
 * 返回被退订频道的总数。
 */
int pubsubUnsubscribeAllChannels(redisClient *c, int notify) {

    // 频道迭代器
    dictIterator *di = dictGetSafeIterator(c->pubsub_channels);
    dictEntry *de;
    int count = 0;

    // 退订
    while((de = dictNext(di)) != NULL) {
        robj *channel = dictGetKey(de);

        count += pubsubUnsubscribeChannel(c,channel,notify);
    }

    /* We were subscribed to nothing? Still reply to the client. */
    // 如果在执行这个函数时,客户端没有订阅任何频道,
    // 那么向客户端发送回复
    if (notify && count == 0) {
        addReply(c,shared.mbulkhdr[3]);
        addReply(c,shared.unsubscribebulk);
        addReply(c,shared.nullbulk);
        addReplyLongLong(c,dictSize(c->pubsub_channels)+
                       listLength(c->pubsub_patterns));
    }

    dictReleaseIterator(di);

    // 被退订的频道的数量
    return count;
}



 取消订阅的过程主要是订阅过程的逆向操作,整个过程如下:

  • 从client的pubsub_channels中删除channel
  • 从server的pubsub_channels查找channel对应的client list对象
  • 从client list对象中删除client对象
  • 如果client list此时为空,就从server.pubsub_channels中删除对应的channel
/* Unsubscribe a client from a channel. Returns 1 if the operation succeeded, or
 * 0 if the client was not subscribed to the specified channel. 
 *
 * 客户端 c 退订频道 channel 。
 *
 * 如果取消成功返回 1 ,如果因为客户端未订阅频道,而造成取消失败,返回 0 。
 */
int pubsubUnsubscribeChannel(redisClient *c, robj *channel, int notify) {
    dictEntry *de;
    list *clients;
    listNode *ln;
    int retval = 0;

    /* Remove the channel from the client -> channels hash table */
    // 将频道 channel 从 client->channels 字典中移除
    incrRefCount(channel); /* channel may be just a pointer to the same object
                            we have in the hash tables. Protect it... */
    // 示意图:
    // before:
    // {
    //  'channel-x': NULL,
    //  'channel-y': NULL,
    //  'channel-z': NULL,
    // }
    // after unsubscribe channel-y :
    // {
    //  'channel-x': NULL,
    //  'channel-z': NULL,
    // }
    if (dictDelete(c->pubsub_channels,channel) == DICT_OK) {

        // channel 移除成功,表示客户端订阅了这个频道,执行以下代码

        retval = 1;
        /* Remove the client from the channel -> clients list hash table */
        // 从 channel->clients 的 clients 链表中,移除 client 
        // 示意图:
        // before:
        // {
        //  'channel-x' : [c1, c2, c3],
        // }
        // after c2 unsubscribe channel-x:
        // {
        //  'channel-x' : [c1, c3]
        // }
        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);

        // 如果移除 client 之后链表为空,那么删除这个 channel 键
        // 示意图:
        // before
        // {
        //  'channel-x' : [c1]
        // }
        // after c1 ubsubscribe channel-x
        // then also delete 'channel-x' key in dict
        // {
        //  // nothing here
        // }
        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]);
        // "ubsubscribe" 字符串
        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;
}


psubscribe 过程

 psubscribe是指支持pattern模式的subscribe,跟subscribe一样,内部也是依次执行订阅的。

void psubscribeCommand(redisClient *c) {
    int j;

    for (j = 1; j < c->argc; j++)
        pubsubSubscribePattern(c,c->argv[j]);
}



 psubscribe保存订阅关系是通过list对象进行保存的,整个订阅过程操作顺序如下:

  • 首先检查client本身的pubsub_patterns列表当中是否存在pattern
  • 不存在就添加到client.pubsub_patterns
  • 不存在就添加到server.pubsub_patterns
/*
 * 设置客户端 c 订阅模式 pattern 。
 *
 * 订阅成功返回 1 ,如果客户端已经订阅了该模式,那么返回 0 。
 */
int pubsubSubscribePattern(redisClient *c, robj *pattern) {
    int retval = 0;

    // 在链表中查找模式,看客户端是否已经订阅了这个模式
    // 这里为什么不像 channel 那样,用字典来进行检测呢?
    // 虽然 pattern 的数量一般来说并不多
    if (listSearchKey(c->pubsub_patterns,pattern) == NULL) {
        
        // 如果没有的话,执行以下代码

        retval = 1;

        pubsubPattern *pat;

        // 将 pattern 添加到 c->pubsub_patterns 链表中
        listAddNodeTail(c->pubsub_patterns,pattern);

        incrRefCount(pattern);

        // 创建并设置新的 pubsubPattern 结构
        pat = zmalloc(sizeof(*pat));
        pat->pattern = getDecodedObject(pattern);
        pat->client = c;

        // 添加到末尾
        listAddNodeTail(server.pubsub_patterns,pat);
    }

    /* Notify the client */
    // 回复客户端。
    // 示例:
    // redis 127.0.0.1:6379> PSUBSCRIBE xxx*
    // Reading messages... (press Ctrl-C to quit)
    // 1) "psubscribe"
    // 2) "xxx*"
    // 3) (integer) 1
    addReply(c,shared.mbulkhdr[3]);
    // 回复 "psubscribe" 字符串
    addReply(c,shared.psubscribebulk);
    // 回复被订阅的模式
    addReplyBulk(c,pattern);
    // 回复客户端订阅的频道和模式的总数
    addReplyLongLong(c,dictSize(c->pubsub_channels)+listLength(c->pubsub_patterns));

    return retval;
}


punsubscribe 过程

 punsubscribe也区分全部取消订阅和部分取消订阅,当然殊途同归最终都会走到pubsubUnsubscribePattern进行取消订阅。

void punsubscribeCommand(redisClient *c) {
    if (c->argc == 1) {
        pubsubUnsubscribeAllPatterns(c,1);
    } else {
        int j;

        for (j = 1; j < c->argc; j++)
            pubsubUnsubscribePattern(c,c->argv[j],1);
    }
}



 遍历c->pubsub_patterns的所有pattern依次进行取消操作。

/* 
 * 退订客户端 c 订阅的所有模式。
 *
 * 返回被退订模式的数量。
 */
int pubsubUnsubscribeAllPatterns(redisClient *c, int notify) {
    listNode *ln;
    listIter li;
    int count = 0;

    // 迭代客户端订阅模式的链表
    listRewind(c->pubsub_patterns,&li);
    while ((ln = listNext(&li)) != NULL) {
        robj *pattern = ln->value;

        // 退订,并计算退订数
        count += pubsubUnsubscribePattern(c,pattern,notify);
    }

    // 如果在执行这个函数时,客户端没有订阅任何模式,
    // 那么向客户端发送回复
    if (notify && count == 0) {
        /* We were subscribed to nothing? Still reply to the client. */
        addReply(c,shared.mbulkhdr[3]);
        addReply(c,shared.punsubscribebulk);
        addReply(c,shared.nullbulk);
        addReplyLongLong(c,dictSize(c->pubsub_channels)+
                       listLength(c->pubsub_patterns));
    }

    // 退订总数
    return count;
}



 取消订阅的过程主要包括两个步骤:

  • c->pubsub_patterns当中删除对应的pattern
  • server.pubsub_patterns当中删除对应的pattern
/*
 * 取消客户端 c 对模式 pattern 的订阅。
 *
 * 取消成功返回 1 ,因为客户端未订阅 pattern 而造成取消失败,返回 0 。
 */
int pubsubUnsubscribePattern(redisClient *c, robj *pattern, int notify) {
    listNode *ln;
    pubsubPattern pat;
    int retval = 0;

    incrRefCount(pattern); /* Protect the object. May be the same we remove */

    // 先确认一下,客户端是否订阅了这个模式
    if ((ln = listSearchKey(c->pubsub_patterns,pattern)) != NULL) {

        retval = 1;

        // 将模式从客户端的订阅列表中删除
        listDelNode(c->pubsub_patterns,ln);

        // 设置 pubsubPattern 结构
        pat.client = c;
        pat.pattern = pattern;

        // 在服务器中查找
        ln = listSearchKey(server.pubsub_patterns,&pat);
        listDelNode(server.pubsub_patterns,ln);
    }

    /* Notify the client */
    // 回复客户端
    if (notify) {
        addReply(c,shared.mbulkhdr[3]);
        // "punsubscribe" 字符串
        addReply(c,shared.punsubscribebulk);
        // 被退订的模式
        addReplyBulk(c,pattern);
        // 退订频道之后客户端仍在订阅的频道和模式的总数
        addReplyLongLong(c,dictSize(c->pubsub_channels)+
                       listLength(c->pubsub_patterns));
    }

    decrRefCount(pattern);

    return retval;
}


publish 过程

 发送消息的过程很简单,也就是遍历server.pubsub_channels 和 server.pubsub_patterns然后匹配对应的channel,然后取出channel当中的client的list列表发送消息即可。

/* Publish a message 
 *
 * 将 message 发送到所有订阅频道 channel 的客户端,
 * 以及所有订阅了和 channel 频道匹配的模式的客户端。
 */
int pubsubPublishMessage(robj *channel, robj *message) {
    int receivers = 0;
    dictEntry *de;
    listNode *ln;
    listIter li;

    /* Send to clients listening for that channel */
    // 取出包含所有订阅频道 channel 的客户端的链表
    // 并将消息发送给它们
    de = dictFind(server.pubsub_channels,channel);
    if (de) {
        list *list = dictGetVal(de);
        listNode *ln;
        listIter li;

        // 遍历客户端链表,将 message 发送给它们
        listRewind(list,&li);
        while ((ln = listNext(&li)) != NULL) {
            redisClient *c = ln->value;

            // 回复客户端。
            // 示例:
            // 1) "message"
            // 2) "xxx"
            // 3) "hello"
            addReply(c,shared.mbulkhdr[3]);
            // "message" 字符串
            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
            pubsubPattern *pat = ln->value;

            // 如果 channel 和 pattern 匹配
            // 就给所有订阅该 pattern 的客户端发送消息
            if (stringmatchlen((char*)pat->pattern->ptr,
                                sdslen(pat->pattern->ptr),
                                (char*)channel->ptr,
                                sdslen(channel->ptr),0)) {

                // 回复客户端
                // 示例:
                // 1) "pmessage"
                // 2) "*"
                // 3) "xxx"
                // 4) "hello"
                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实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
7月前
|
存储 缓存 NoSQL
聊聊 Redis 的高级特性之一: 发布订阅
Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。 图中,消费者1和消费者2 订阅了 Redis 服务的频道 channel ,当生产者通过 PUBLISH 命令发送给频道 channel 时, 这个消息就会被发送给订阅它的两个客户端。
聊聊 Redis 的高级特性之一: 发布订阅
|
7月前
|
消息中间件 移动开发 NoSQL
Redis 协议 事务 发布订阅 异步连接
Redis 协议 事务 发布订阅 异步连接
|
7月前
|
NoSQL Redis
Redis之发布订阅
Redis之发布订阅
|
2月前
|
NoSQL Redis
Redis 发布订阅
10月更文挑战第18天
29 1
Redis 发布订阅
|
3月前
|
消息中间件 存储 NoSQL
18)Redis 的发布订阅模型
18)Redis 的发布订阅模型
33 0
|
7月前
|
存储 NoSQL Redis
Redis 简介 + Python 操作发布订阅
Redis 简介 + Python 操作发布订阅
108 0
|
7月前
|
消息中间件 设计模式 缓存
Redis发布订阅:美丽的陷阱与不宜之境
Redis发布订阅:美丽的陷阱与不宜之境
698 1
|
消息中间件 缓存 NoSQL
探索Redis发布订阅与消息队列:构建实时消息通信系统
本篇深入探讨了Redis的发布订阅模式和消息队列功能,展示了如何使用这两个特性构建实时消息通信系统。我们首先介绍了Redis的发布订阅模式,演示了如何通过PUBLISH命令将消息发布到特定频道,并使用SUBSCRIBE和UNSUBSCRIBE命令进行订阅和取消订阅操作。
576 0
|
7月前
|
消息中间件 NoSQL Redis
Redis发布订阅
【1月更文挑战第7天】 消息队列 Redis 发布订阅(publsub)是一种消息通信模式︰发送者(pub)发送消息,订阅者(sub)接收消息。微信、微博、关注系统! Redis客户端可以订阅任意数量的频道。
205 6
|
7月前
|
消息中间件 NoSQL Java
硬核 | Redis Pub/Sub 发布订阅与宅男有什么关系?
硬核 | Redis Pub/Sub 发布订阅与宅男有什么关系?
84 0
下一篇
无影云桌面