Redis订阅命令包括subscribe(订阅指定频道)和psubscribe(订阅符合指定模式的频道)两种,这些命令被广泛用于实时通信应用,如实时广播、实时提醒等。现在,我们就来研究一下redis-server订阅与发布的工作机制。
1、订阅
1.1、数据结构
//服务端信息
struct redisServer
{
//省略...
dict *pubsub_channels; //字典,标记所有频道和订阅客户端,其中key为频道,value是一个双向链表,链表的每一个结点代表订阅该频道的一个客户端。(用于subscribe命令)
list *pubsub_patterns; //链表,标记所有模式,每一个结点包括一个模式和订阅该模式的一个客户端,即在单个结点中,模式与客户端属于一一对应关系。(用于psubscribe命令)
// 省略...
};
struct redisServer server;
//客户端信息
typedef struct redisClient
{
// 省略...
dict *pubsub_channels; //字典,标记该客户端订阅的所有频道,key为频道,value为空(不使用)。(用于subscribe命令)
list *pubsub_patterns; //链表,标记该客户端订阅的所有模式。(用于psubscribe命令)
// 省略...
} redisClient;
1.2、subscribe
redis-server接收到客户端的subscribe命令之后,首先会将命令的参数个数存入redisClient.argc,各命令参数存入指针数组redisClient.argv,然后调用pubsubSubscribeChannel(),依次订阅单个频道。
void subscribeCommand(redisClient *c)
{
int j;
//依次订阅单个频道
for (j = 1; j < c->argc; j++)
pubsubSubscribeChannel(c,c->argv[j]);
c->flags |= REDIS_PUBSUB;
}
//功能:为一个客户端订阅一个频道
//将客户端添加至字典redisServer.pubsub_channels,即:将客户端添加至指定频道所对应的客户端双向链表中。
//返回值:1(订阅成功),0(该客户端已经订阅过该频道)
int pubsubSubscribeChannel(redisClient *c, robj *channel)
{
dictEntry *de;
list *clients = NULL;
int retval = 0;
//dictAdd():订阅频道
//功能:将频道channel添加至字典redisClient.pubsub_channels,登记该客户端所订阅的频道
//返回值:DICT_OK,订阅成功
if (dictAdd(c->pubsub_channels,channel,NULL) == DICT_OK)
{
retval = 1;
//对象引用计数+1,便于后续释放对象
incrRefCount(channel);
//1、检索字典redisServer.pubsub_channels,检查key=频道channel是否已存在
//返回值:key存在时,直接返回key所对应的记录,否则返回NULL
de = dictFind(server.pubsub_channels,channel);
if (de == NULL)
{
//主键不存在时
//2.1、首先创建一个空链表clients,用于后续存储订阅该频道的所有客户端
clients = listCreate();
//2.2、往字典redisServer.pubsub_channels中新增一条记录,新增记录的key为频道channel,value是一个空链表clients
dictAdd(server.pubsub_channels,channel,clients);
incrRefCount(channel);
}
else
{
//3、主键已存在时,直接获取key所对应的value,即订阅该频道的客户端双向链表
clients = dictGetVal(de);
}
//4、将客户端c添加至双向链表clients
listAddNodeTail(clients,c);
}
//5、设置响应消息
//以下四个函数功能大体相似:
//函数首先会检查本次响应是否已创建IO事件,若没有,则创建,并指定文件描述符fd、监听的事件类型mask为2(读:1,写:2)、事件函数为sendReplyToClient(读:readQueryFromClient,写:sendReplyToClient)。
//(注意:针对单个客户端的一条subscribe命令,即使同时订阅多个频道,IO事件只会创建一次。)
//然后将响应消息以Redis协议的形式添加至客户端的响应缓冲区
//若一次订阅多个频道,则待所有频道订阅完毕后,再执行sendReplyToClient,将缓冲区内容一次性发送给客户端。
//返回的参数个数
//如:
//*3\r\n
addReply(c,shared.mbulkhdr[3]);
//返回参数“subscribe”的长度和参数数据
//如:
//$9\r\n
//subscribe\r\n
addReply(c,shared.subscribebulk);
//返回订阅的频道(包括频道名称的长度和频道名称)
//如:
//$7\r\n
//redis01\r\n
addReplyBulk(c,channel);
//返回客户端当前已订阅的频道总和
//如:
//:1\r\n
addReplyLongLong(c,clientSubscriptionsCount(c));
return retval;
}
假设客户端执行:subscribe redis01 redis02,则pubsubSubscribeChannel()分三次依次订阅各频道,并分别往相应缓冲区中追加:
*3\r\n
$9\r\n
subscribe\r\n
$7\r\n
redis01\r\n
:1\r\n
*3\r\n
$9\r\n
subscribe\r\n
$7\r\n
redis02\r\n
:1\r\n
redis-cli收到的响应为:
1) "subscribe"
2) "redis01"
3) (integer) 1
1) "subscribe"
2) "redis02"
3) (integer) 2
1.3、psubscribe
redis-server接收到客户端的psubscribe命令之后,首先会将命令的参数个数存入redisClient.argc,各命令参数存入指针数组redisClient.argv,然后调用pubsubSubscribePattern(),依次订阅单个模式。
void psubscribeCommand(redisClient *c)
{
int j;
//依次订阅单个模式
for (j = 1; j < c->argc; j++)
pubsubSubscribePattern(c,c->argv[j]);
c->flags |= REDIS_PUBSUB;
}
//功能:为一个客户端订阅一个模式
//将客户端添加至链表redisServer.pubsub_patterns,即:往链表中追加单个结点,结点模式和客户端信息。单个结点中,模式和客户端属于一一对应关系。
//返回值:1(订阅成功),0(该客户端已经订阅过该模式)
int pubsubSubscribePattern(redisClient *c, robj *pattern) {
int retval = 0;
//1、检查客户端是否已经订阅该模式
//返回值:NULL,说明该客户端未曾订阅过该模式
if (listSearchKey(c->pubsub_patterns,pattern) == NULL)
{
retval = 1;
pubsubPattern *pat;
//2.1、将模式pattern添加至链表redisClient.pubsub_patterns,登记该客户端所订阅的模式。
listAddNodeTail(c->pubsub_patterns,pattern);
incrRefCount(pattern);
//2.2、创建一个结点pubsubPattern,用于后续存入链表redisServer.pubsub_patterns。
pat = zmalloc(sizeof(*pat));
//2.3、设置结点信息
//getDecodedObject():获取编码对象的解码版本,并返回解码版本对象
//若对象已经是原始编码,则对象引用计数+1,并返回原始对象
//此处pattern为原始编码
pat->pattern = getDecodedObject(pattern);
pat->client = c;
//2.4、将模式添加至链表redisServer.pubsub_patterns
listAddNodeTail(server.pubsub_patterns,pat);
}
addReply(c,shared.mbulkhdr[3]);
addReply(c,shared.psubscribebulk);
addReplyBulk(c,pattern);
addReplyLongLong(c,clientSubscriptionsCount(c));
return retval;
}
2.1、publish
redis-server接收到客户端的publish命令之后,首先会将命令的参数个数存入redisClient.argc,各命令参数存入指针数组redisClient.argv,然后调用pubsubPublishMessage(),将信息推送到各订阅客户端。
void publishCommand(redisClient *c)
{
//1、发布消息
int receivers = pubsubPublishMessage(c->argv[1],c->argv[2]);
if (server.cluster_enabled)
clusterPropagatePublish(c->argv[1],c->argv[2]);
else
forceCommandPropagation(c,REDIS_PROPAGATE_REPL);
//2、向publish客户端返回频道订阅者的个数
addReplyLongLong(c,receivers);
}
//功能:发布消息
//函数首先向频道的订阅者发送消息,然后向频道所符合模式的订阅者发送消息
//返回值:订阅者的个数
int pubsubPublishMessage(robj *channel, robj *message)
{
int receivers = 0;
dictEntry *de;
listNode *ln;
listIter li;
//向频道的订阅者发送消息
//1、检索字典redisServer.pubsub_channels,返回订阅该频道的客户端链表
de = dictFind(server.pubsub_channels,channel);
if (de)
{
list *list = dictGetVal(de);
listNode *ln;
listIter li;
//2、创建一个迭代器,用于遍历客户端链表
listRewind(list,&li);
//3、遍历客户端链表
while ((ln = listNext(&li)) != NULL)
{
redisClient *c = ln->value;
//4、设置客户端响应缓冲区
//返回的参数个数
//如:
//*3\r\n
addReply(c,shared.mbulkhdr[3]);
//返回参数“message”的长度和参数数据
//如:
//$9\r\n
//message\r\n
addReply(c,shared.messagebulk);
//返回频道名称
//如:
//$7\r\n
//redis02\r\n
addReplyBulk(c,channel);
//返回消息内容
//如:
//$10\r\n
//helloworld\r\n
addReplyBulk(c,message);
receivers++;
}
}
//向频道所符合模式的订阅者发送消息
if (listLength(server.pubsub_patterns))
{
listRewind(server.pubsub_patterns,&li);
channel = getDecodedObject(channel);
//5、遍历链表pubsub_patterns.redisServer
while ((ln = listNext(&li)) != NULL)
{
pubsubPattern *pat = ln->value;
//6、检查频道是否符合指定模式
if (stringmatchlen((char*)pat->pattern->ptr,
(int)sdslen(pat->pattern->ptr), WIN_PORT_FIX /* cast (int) */
(char*)channel->ptr,
(int)sdslen(channel->ptr),0)) { WIN_PORT_FIX /* cast (int) */
//7、设置客户端响应缓冲区
//返回的参数个数
//如:
//*4\r\n
addReply(pat->client,shared.mbulkhdr[4]);
//返回参数“pmessage”的长度和参数数据
//如:
//$8\r\n
//pmessage\r\n
addReply(pat->client,shared.pmessagebulk);
//返回模式
//如:
//$6\r\n
//redis*\r\n
addReplyBulk(pat->client,pat->pattern);
//返回频道名称
//如:
//$7\r\n
//redis02\r\n
addReplyBulk(pat->client,channel);
//返回消息内容
//如:
//$10\r\n
//helloworld\r\n
addReplyBulk(pat->client,message);
receivers++;
}
}
decrRefCount(channel);
}
return receivers;
}