redis 源码系列(16):We will call you --- PUB/SUB命令

redis 源代码系列到现在,已经基本上介绍完了单节点 redis 服务的整体架构以及一些特别重要的特性(RDB,AOF 等),但是对于其支持的 commands 还未加以介绍。鉴于这些 commands 大多数都是建立在之前介绍过的几种数据结构之上,其实现逻辑并未有太多值得大书特书之处,这里就不再对每个命令加以讲解,仅挑选 PUB/SUB 和 lua script 脚本相关命令进一步学习。

今天我们来学习一下 redis 的 PUB/SUB 命令。PUB/SUB 是一种解耦 sender 和 receiver 的设计模式,是消息队列的近亲(区别只在于消费者的是否主动去获取消息)。sender 将信息发送到指定的 channel 而不关心 receiver,receiver 只需要表明自己对那些 channel 感兴趣,然后等待 channel 的消息即可,完全不必了解 sender。

现在我们来看一下 PUB/SUB 在 redis 的实现逻辑。

channel、pattern 作用域

PUB/SUB 中的 channel 或者 pattern 不与某个具体的 database 绑定,其作用域是全局的。就是说假设某个客户端A在 database1 SUBSCRIBE channel1,那么另一个在客户端B在 database10 的 PUBLISH channel1 message 是会发送到客户端A的

SUBSCRIBE

客户端通过 subscribe 向 redis 注册感兴趣的 channel(s)。我们在介绍 redis 通信协议的时候说过,redis 的 RESP 在一般情况下是一个简单的请求/响应模型。但是如果客户端调用 SUBSCRIBE 命令后,就进入了 push 模式,即客户端不再主动发送命令,而是等待服务端推送信息到客户端。

// subscribe,订阅指定的若干个频道
void subscribeCommand(redisClient *c) {
    int j;

    for (j = 1; j < c->argc; j++)
        pubsubSubscribeChannel(c,c->argv[j]);
}
int pubsubSubscribeChannel(redisClient *c, robj *channel) {
    dictEntry *de;
    list *clients = NULL;
    int retval = 0;
    // client->pubsub_channels 是一个保存该 client 所有注册 channel 的集合 
    if (dictAdd(c->pubsub_channels,channel,NULL) == DICT_OK) {
        retval = 1;
        incrRefCount(channel);
        // server.pubsub_channels 是注册 channel->list(client) 的倒排表
        de = dictFind(server.pubsub_channels,channel);
        if (de == NULL) {
            clients = listCreate();
            dictAdd(server.pubsub_channels,channel,clients);
            incrRefCount(channel);
        } else {
            clients = dictGetVal(de);
        }
				// 加入到 client list 中
        listAddNodeTail(clients,c);
    }

    // 回复客户端。
    addReply(c,shared.mbulkhdr[3]);
    // "subscribe\n" 字符串
    addReply(c,shared.subscribebulk);
    // 被订阅的 channel
    addReplyBulk(c,channel);
    // 客户端订阅的 channel 和 pattern 总数
    addReplyLongLong(c,dictSize(c->pubsub_channels)+listLength(c->pubsub_patterns));

    return retval;
}
// psubscribe,按照 pattern 订阅频道(pattern 类似简化版的正则表达式)
void psubscribeCommand(redisClient *c) {
    int j;
    for (j = 1; j < c->argc; j++)
        pubsubSubscribePattern(c,c->argv[j]);
}

int pubsubSubscribePattern(redisClient *c, robj *pattern) {
    int retval = 0;
    // 在链表中查找模式,看客户端是否已经订阅了这个模式
    // 这里使用 list 来存储订阅的 pattern,个人感觉应该是因为 publish 的时候 pattern 并不能用"查找"这样的操作
    // 所以就没有使用 dict,还要注意 pattern 存储中一个 pattern 只对应了一个 client
    // 个人不太明白为什么 pubsubPattern 中保存的是单个 client,而不是 client 链表
    if (listSearchKey(c->pubsub_patterns,pattern) == NULL) {
        // 如果没有的话,插入到 pubsub_patterns
        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);
    }
    // 回复客户端。
    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;
}

笔者本人对 redis 中存储 channel 和 pattern 的方式如此大相径庭有点不太明白。如果说存储 pattern 时候用链表是因为在 PUBLISH 的时候,即使有 dict 也无法使用查找的语意,需要遍历整个容器,所以选择空间更加节省的 list 还可以理解。但是每个 pattern 都只对应一个 client,而不是 client 链表,就让我彻底无法理解了。

UNSUBSCRIBE

客户端可以 unsubscribe channel,后续就不会再收到相关 channel 的消息:

void unsubscribeCommand(redisClient *c) {
    if (c->argc == 1) {
        // 没有指定 channel 则退订全部 channel
        pubsubUnsubscribeAllChannels(c,1);
    } else {
        int j;
        for (j = 1; j < c->argc; j++)
            pubsubUnsubscribeChannel(c,c->argv[j],1);
    }
}

int pubsubUnsubscribeChannel(redisClient *c, robj *channel, int notify) {
    dictEntry *de;
    list *clients;
    listNode *ln;
    int retval = 0;
    // 将频道 channel 从 client->channels 字典中移除
    incrRefCount(channel); /* channel may be just a pointer to the same object
                            we have in the hash tables. Protect it... */
		// 从 client->pubsub_channels 中删除
    if (dictDelete(c->pubsub_channels,channel) == DICT_OK) {
        // 如果客户端订阅了该 channel,则修改倒排表中对应的 key
        retval = 1;
        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,从 server.pubsub_channels 中删除
        if (listLength(clients) == 0) {
            dictDelete(server.pubsub_channels,channel);
        }
    }
    // 回复客户端
    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;
}

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);
    }
    // 如果客户端其实没有订阅任何 channel,发送一个特殊的NULL响应
    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;
}

void punsubscribeCommand(redisClient *c) {
    if (c->argc == 1) {
        // 退订全部 patterns
        pubsubUnsubscribeAllPatterns(c,1);
    } else {
        int j;

        for (j = 1; j < c->argc; j++)
            pubsubUnsubscribePattern(c,c->argv[j],1);
    }
}
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;
        // 从 server.pubsub_pattern 中删除
        ln = listSearchKey(server.pubsub_patterns,&pat);
        listDelNode(server.pubsub_patterns,ln);
    }
    // 回复客户端
    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;
}
int pubsubUnsubscribeAllPatterns(redisClient *c, int notify) {
    listNode *ln;
    listIter li;
    int count = 0;
    // 退订全部 pattern
    listRewind(c->pubsub_patterns,&li);
    while ((ln = listNext(&li)) != NULL) {
        robj *pattern = ln->value;
        count += pubsubUnsubscribePattern(c,pattern,notify);
    }
    // 如果客户端并为订阅任何 pattern,发送一个特殊的NULL响应
    if (notify && count == 0) {
        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;
}

PUBLISH

客户端通过 PUBLISH 向指定的 channel 或者

void publishCommand(redisClient *c) {
    int receivers = pubsubPublishMessage(c->argv[1],c->argv[2]);
    if (server.cluster_enabled)
        clusterPropagatePublish(c->argv[1],c->argv[2]);
    else
        // 如果非集群模式,强制命令传播到 slave 
        forceCommandPropagation(c,REDIS_PROPAGATE_REPL);
    addReplyLongLong(c,receivers);
}

int pubsubPublishMessage(robj *channel, robj *message) {
    int receivers = 0;
    dictEntry *de;
    listNode *ln;
    listIter li;
		// 因为 channel 是精准匹配的,使用 dict 存储查找
    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;
            addReply(c,shared.mbulkhdr[3]);
            // "message" 字符串
            addReply(c,shared.messagebulk);
            // 消息的来源频道
            addReplyBulk(c,channel);
            // 消息内容
            addReplyBulk(c,message);
            // 接收客户端计数
            receivers++;
        }
    }
    // 将消息也发送给那些和频道匹配的模式
    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)) {
                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;
}

PUBLISH 的逻辑也很简单,现在服务器订阅的 channels 中查询,如果有订阅该 channel,则发送数据到所有客户端,后续在 patterns 中查询,发送数据到符合的 pattern 对应的客户端。

这里要注意的一点是,如果 channel 属于 pattern ,客户端会收到多次消息。即如果客户端A订阅了 channel abc,同时订阅了 pattern a*, pattern *b,那么当客户端B执行如下命令:

PUBLISH abc Hello

客户端A会收到3条消息

// 第一条,来自 channel abc
*3
$7
message
$3
abc
$5
Hello
// 第二条,来自 pattern a*(假设 a* pattern 先于 *b 订阅)
*4
$8	
pmessage
$2
a*
$3
abc
$5
Hello
// 第二条,来自 pattern b*
*4
$8	
pmessage
$2
b*
$3
abc
$5
Hello

总结

  1. redis 支持 PUB/SUB 模式,同时支持 channel 订阅和 pattern 订阅
  2. 客户端一旦订阅了 channel 或者 pattern,就处于订阅模式,这时候不应该再发送任何除 PUB/SUB 相关的命令,而应该等待 redis 推送其感兴趣的数据
  3. 如果一次 PUBLISH 与客户端订阅的 channel 和 pattern 都匹配,客户端会收到多条消息
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值