一、
发布者和订阅者的解耦合可以带来更大的扩展性和更加动态的网络拓扑
Redis的Pub/Sub功能,只能实时获取订阅的频道消息,当客户端离线后,离线后的频道消息不会被保存起来。
二、操作
redis是先执行订阅,在发布消息;
所以,第一步就是订阅频道;
表示我们订阅了cctv和cctv1频道
127.0.0.1:6379> subscribe CCTV CCTV1
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "CCTV"
3) (integer) 1
1) "subscribe"
2) "CCTV1"
3) (integer) 2
//此时可以吧这个订阅分为两组,
下面就是发布消息:
// 发布端发布消息
127.0.0.1:6379> publish CCTV "cctv is good"
(integer) 1
//而此时订阅消息则变成:
127.0.0.1:6379> subscribe CCTV CCTV1
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "CCTV"
3) (integer) 1
1) "subscribe"
2) "CCTV1"
3) (integer) 2
1) "message"
2) "CCTV"
3) "cctv is good"
//此时多出来最后一组消息
通配符的Pub/Sub发布/订阅消息;
客户端可以订阅 满足一个或多个规则的channel频道;
127.0.0.1:6379> psubscribe cctv*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "cctv*"
3) (integer) 1//该客户端目前订阅的所有规则个数
127.0.0.1:6379> psubscribe CCTV*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "CCTV*"
3) (integer) 1
1) "pmessage"
2) "CCTV*"
3) "CCTV1"
4) "goods"
可以看出 subscribe和psubscribe 最后得到的结果类似,但是psubscribe多出一个匹配哪个频道的结果
三、实现原理
路径:redis/src/pubsub.c 看出来是C语言基础的
1、Redis将所有订阅关系保持在服务器状态的pubsub_channels 字典 项中;
struct redisServer {
...
dict *pubsub_channels; /* Map channels to list of subscribed clients */
...
}
该字典项中的key是频道channel名称,value是一个链表,而链表中保存了所有订阅这个channel的客户端;
2、订阅频道
当客户端执行subscribe没命令。订阅某个或某些频道时,服务器会将客户端与被订阅的频道在pubsub_channels字典中进行关联。
void subscribeCommand(client *c) {
int j;
for (j = 1; j < c->argc; j++)
pubsubSubscribeChannel(c,c->argv[j]);
c->flags |= CLIENT_PUBSUB;
}
/* Subscribe a client to a channel. Returns 1 if the operation succeeded, or
* 0 if the client was already subscribed to that channel. */
int pubsubSubscribeChannel(client *c, robj *channel) {
dictEntry *de;
list *clients = NULL;
int retval = 0;
/* Add the channel to the client -> channels hash table 将频道添加到客户端-频道hash表 */
if (dictAdd(c->pubsub_channels,channel,NULL) == DICT_OK) {
retval = 1;
incrRefCount(channel);
/* Add the client to the channel -> list of clients hash table */
de = dictFind(server.pubsub_channels,channel);
if (de == NULL) {
clients = listCreate();
dictAdd(server.pubsub_channels,channel,clients);
incrRefCount(channel);
} else {
clients = dictGetVal(de);//订阅该频道所有客户端列表
}
listAddNodeTail(clients,c);
}
/* Notify the client */
addReply(c,shared.mbulkhdr[3]);
addReply(c,shared.subscribebulk);
addReplyBulk(c,channel);
addReplyLongLong(c,clientSubscriptionsCount(c));
return retval;
}
判断频道是否存在于pubsub_channels字典中分开操作:
3、取消订阅
void unsubscribeCommand(client *c) {
if (c->argc == 1) {
pubsubUnsubscribeAllChannels(c,1);
} else {
int j;
for (j = 1; j < c->argc; j++)
pubsubUnsubscribeChannel(c,c->argv[j],1);
}
if (clientSubscriptionsCount(c) == 0) c->flags &= ~CLIENT_PUBSUB;
}
/* Unsubscribe a client from a channel. Returns 1 if the operation succeeded, or
* 0 if the client was not subscribed to the specified channel. */
int pubsubUnsubscribeChannel(client *c, robj *channel, int notify) {
dictEntry *de;
list *clients;
listNode *ln;
int retval = 0;
/* Remove the channel from the client -> channels hash table */
incrRefCount(channel); /* channel may be just a pointer to the same object
we have in the hash tables. Protect it... */
if (dictDelete(c->pubsub_channels,channel) == DICT_OK) {
retval = 1;
/* Remove the client from the channel -> clients list hash table */
de = dictFind(server.pubsub_channels,channel);//找到要取消订阅的频道
serverAssertWithInfo(c,NULL,de != NULL);
clients = dictGetVal(de);//订阅该频道的所有客户端列表
ln = listSearchKey(clients,c);//找到要取消订阅的客户端
serverAssertWithInfo(c,NULL,ln != NULL);
listDelNode(clients,ln);//将从客户端列表移除
if (listLength(clients) == 0) {
//如果订阅该频道的客户端列表为空,表示没有人订阅该频道,将该频道从pubsub_channels字典中移除
/* 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]);
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;
}
4、 发布
void publishCommand(client *c) {
int receivers = pubsubPublishMessage(c->argv[1],c->argv[2]);
if (server.cluster_enabled)
clusterPropagatePublish(c->argv[1],c->argv[2]);
else
forceCommandPropagation(c,PROPAGATE_REPL);
addReplyLongLong(c,receivers);
}
四、php+redis代码实现
1、消费者订阅 subscribe.php
//设置php脚本执行时间
set_time_limit(0);
//设置socket连接超时时间
ini_set('default_socket_timeout', -1);
//声明测试频道名称
$channelName = "testPubSub";
$channelName2 = "testPubSub2";
try {
$redis = new Redis();
//建立一个长链接
$redis->pconnect('192.168.75.132', 6379);
//阻塞获取消息
$redis->subscribe(array($channelName, $channelName2), function ($redis, $chan, $msg){
echo "channel:".$chan.",message:".$msg."\n";
});
} catch (Exception $e){
echo $e->getMessage();
}
2、生产者发布 publish.php
$channelName = "testPubSub";
$channelName2 = "testPubSub2";
//向指定频道发送消息
try {
$redis = new Redis();
$redis->connect('192.168.75.132', 6379);
for ($i=0;$i<5;$i++){
$data = array('key' => 'key'.$i, 'data' => 'testdata');
$ret = $redis->publish($channelName, json_encode($data));
print_r($ret);
}
} catch (Exception $e){
echo $e->getMessage();
}
3、执行消费者订阅,开始阻塞获取消息php subscribe.php
4、执行生产者,开始发送消息php publish.php