轉載:http://huoding.com/2012/02/29/146php
最近忙着用Redis實現一個消息通知系統,今天大概總結了一下技術細節,其中演示代碼若是沒有特殊說明,使用的都是PhpRedis擴展來實現的。redis
內存架構
好比要推送一條全局消息,若是真的給全部用戶都推送一遍的話,那麼會佔用很大的內存,實際上無論粘性有多高的產品,活躍用戶同所有用戶比起來,都會小不少,因此若是隻處理登陸用戶的話,那麼至少在內存消耗上是至關划算的,至於未登陸用戶,能夠推遲到用戶下次登陸時再處理,若是用戶一直不登陸,就一了百了了。ide
隊列測試
當大量用戶同時登陸的時候,若是所有都即時處理,那麼很容易就崩潰了,此時可使用一個隊列來保存待處理的登陸用戶,如此一來頂可能是反應慢點,但不會崩潰。this
Redis的LIST數據類型能夠很天然的建立一個隊列,代碼以下:命令行
<?php隊列
$redis = new Redis;內存
$redis->connect('/tmp/redis.sock');element
$redis->lPush('usr', <USRID>);
while ($usr = $redis->rPop('usr')) {
var_dump($usr);
}
?>
出於相似的緣由,咱們還須要一個隊列來保存待處理的消息。固然也可使用LIST來實現,但LIST只能按照插入的前後順序實現相似FIFO或LIFO形式的隊列,然而消息其實是有優先級的:好比說我的消息優先級高,全局消息優先級低。此時可使用ZSET來實現,它裏面分數的概念很天然的實現了優先級。不過ZSET沒有原生的POP操做,因此咱們須要模擬實現,代碼以下:
<?php
class RedisClient extends Redis
{
const POSITION_FIRST = 0;
const POSITION_LAST = -1;
public function zPop($zset)
{
return $this->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實現隊列了,代碼以下:
<?php
$redis = new RedisClient;
$redis->connect('/tmp/redis.sock');
$redis->zAdd('msg', <PRIORITY>, <MSGID>);
while ($msg = $redis->zRevPop('msg')) {
var_dump($msg);
}
?>
推拉
之前微博架構中推拉選擇的問題已經被你們討論過不少次了。實際上消息通知系統和微博差很少,也存在推拉選擇的問題,一樣答案也是相似的,那就是應該推拉結合。具體點說:在登錄用戶獲取消息的時候,就是一個拉消息的過程;在把消息發送給登錄用戶的時候,就是一個推消息的過程。
速度
假設要推送一百萬條消息的話,那麼最直白的實現就是不斷的插入,代碼以下:
<?php
for ($msgid = 1; $msgid <= 1000000; $msgid++) {
$redis->sAdd('usr:<USRID>:msg', $msgid);
}
?>
說明:這裏我使用了SET數據類型,固然你也能夠視需求換成LIST或者ZSET。Redis的速度是很快的,可是藉助PIPELINE,會更快,代碼以下:
<?php
for ($i = 1; $i <= 100; $i++) {
$redis->multi(Redis::PIPELINE);
for ($j = 1; $j <= 10000; $j++) {
$msgid = ($i - 1) * 10000 + $j;
$redis->sAdd('usr:<USRID>:msg', $msgid);
}
$redis->exec();
}
?>
說明:所謂PIPELINE,就是省略了無謂的折返跑,把命令打包給服務端統一處理。先後兩段代碼在個人測試裏,使用PIPELINE的速度大概是不使用PIPELINE的十倍。
查詢
咱們用Redis命令行來演示一下用戶是如何查詢消息的。先插入三條消息,其分別是1,2,3:
先插入三條消息,其分別是1,2,3:
redis> HMSET msg:1 title title1 content content1
redis> HMSET msg:2 title title2 content content2
redis> HMSET msg:3 title title3 content content3
再把這三條消息發送給某個用戶,其是123:
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命令能夠達到事半功倍的效果,實際上它相似於SQL中的JOIN:
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了。