最近忙着用redis实现一个消息通知系统,今天大概总结了一下技术细节,其中演示代码如果没有特殊说明,使用的都是phpredis扩展来实现的。 内存 比如要推送一条全局消息,如果真的给所有用户都推送一遍的话,那么会占用很大的内存,实际上不管粘性有多高的产品
最近忙着用redis实现一个消息通知系统,今天大概总结了一下技术细节,其中演示代码如果没有特殊说明,使用的都是phpredis扩展来实现的。
内存比如要推送一条全局消息,如果真的给所有用户都推送一遍的话,那么会占用很大的内存,实际上不管粘性有多高的产品,活跃用户同全部用户比起来,都会小很多,所以如果只处理登录用户的话,那么至少在内存消耗上是相当划算的,至于未登录用户,可以推迟到用户下次登录时再处理,如果用户一直不登录,就一了百了了。
队列当大量用户同时登录的时候,如果全部都即时处理,那么很容易就崩溃了,此时可以使用一个队列来保存待处理的登录用户,如此一来顶多是反应慢点,但不会崩溃。
redis的list数据类型可以很自然的创建一个队列,代码如下:
connect('/tmp/redis.sock');
$redis->lpush('usr',);
while ($usr = $redis->rpop('usr')) {
var_dump($usr);
}
?>
出于类似的原因,我们还需要一个队列来保存待处理的消息。当然也可以使用list来实现,但list只能按照插入的先后顺序实现类似fifo或lifo形式的队列,然而消息实际上是有优先级的:比如说个人消息优先级高,全局消息优先级低。此时可以使用zset来实现,它里面分数的概念很自然的实现了优先级。
不过zset没有原生的pop操作,所以我们需要模拟实现,代码如下:
zsetpop($zset, self::position_first);
}
public function zrevpop($zset)
{
return $this->zsetpop($zset, self::position_last);
}
private function zsetpop($zset, $position)
{
$this->watch($zset);
$element = $this->zrange($zset, $position, $position);
if (!isset($element[0])) {
return false;
}
if ($this->multi()->zrem($zset, $element[0])->exec()) {
return $element[0];
}
return $this->zsetpop($zset, $position);
}
}
?>
模拟实现了pop操作后,我们就可以使用zset实现队列了,代码如下:
connect('/tmp/redis.sock');
$redis->zadd('msg',,);
while ($msg = $redis->zrevpop('msg')) {
var_dump($msg);
}
?>
推拉以前微博架构中推拉选择的问题已经被大家讨论过很多次了。实际上消息通知系统和微博差不多,也存在推拉选择的问题,同样答案也是类似的,那就是应该推拉结合。具体点说:在登陆用户获取消息的时候,就是一个拉消息的过程;在把消息发送给登陆用户的时候,就是一个推消息的过程。
速度假设要推送一百万条消息的话,那么最直白的实现就是不断的插入,代码如下:
<?php
for ($msgid = 1; $msgid sadd('usr::msg', $msgid);
}
?>
说明:这里我使用了set数据类型,当然你也可以视需求换成list或者zset。
redis的速度是很快的,但是借助pipeline,会更快,代码如下:
<?php
for ($i = 1; $i multi(redis::pipeline);
for ($j = 1; $j sadd('usr::msg', $msgid);
}
$redis->exec();
}
?>
说明:所谓pipeline,就是省略了无谓的折返跑,把命令打包给服务端统一处理。
前后两段代码在我的测试里,使用pipeline的速度大概是不使用pipeline的十倍。
查询我们用redis命令行来演示一下用户是如何查询消息的。
先插入三条消息,其
redis> hmset msg:1 title title1 content content1
redis> hmset msg:2 title title2 content content2
redis> hmset msg:3 title title3 content content3
再把这三条消息发送给某个用户,其
redis> sadd usr:123:msg 1
redis> sadd usr:123:msg 2
redis> sadd usr:123:msg 3
此时如果简单查询用户有哪些消息的话,无疑只能查到一些
redis> smembers usr:123:msg
1) 1
2) 2
3) 3
如果还需要用程序根据
redis> sort usr:123:msg get msg:*->title
1) title1
2) title2
3) title3
redis> sort usr:123:msg get msg:*->content
1) content1
2) content2
3) content3
sort的缺点是它只能get出字符串类型的数据,如果你想要多个数据,就要多次get:
redis> sort usr:123:msg get msg:*->title get msg:*->content
1) title1
2) content1
3) title2
4) content2
5) title3
6) content3
很多情况下这显得不够灵活,好在我们可以采用其他一些方法平衡一下利弊,比如说新加一个字段,冗余保存完整消息的序列化,接着只get这个字段就ok了。
实际暴露查询接口的时候,不会使用php等程序来封装,因为那会成倍降低rps,推荐使用webdis,它是一个redis的web代理,效率没得说。
…
最近tumblr发表了一篇类似的文章:staircar: redis-powered notifications,介绍了他们使用redis实现消息通知系统的一些情况,有兴趣的不妨一起看看。
原文地址:redis消息通知系统的实现, 感谢原作者分享。
相关文章:
html 5的消息通知机制
web消息通知系统设计问题
基于html5 notifications api的消息通知插件