鍍金池/ 教程/ 大數(shù)據(jù)/ 使用 Redis 實現(xiàn) Twitter(下)
使用 Redis 實現(xiàn) Twitter(上)
集群(下)
使用 Redis 實現(xiàn) Twitter(下)
使用 Redis 作為 LRU 緩存
高可用(上)
高可用客戶端指引
集群(中)
高可用(下)
持久化
Redis 介紹
集中插入
集群(上)
從入門到精通(上)
從入門到精通(下)
從入門到精通(中)
分片
數(shù)據(jù)類型初探
復(fù)制

使用 Redis 實現(xiàn) Twitter(下)

把 loadUserInfo 作為一個單獨的函數(shù)有點大題小做了,但是在復(fù)雜的程序中這是一個很好的方法。認證中唯一被遺漏的事情就是登出了。我們怎么來做登出呢?很簡單,我們改變 user:1000 的 auth 字段中的隨機串,從 auths 哈希中刪除舊的認證秘鑰,然后添加一個新的。

重要:登出的步驟解釋了為什么我們不是僅僅在 auths 哈希中查看認證秘鑰以后認證用戶,而是雙重檢查 user:1000 的 auth 字段。真正的認證字符串是后者,auths 哈希只不過是一個會揮發(fā)(volatile)的認證字段,或者,如果程序中 bug 或者腳本被中斷,我們會發(fā)現(xiàn) auths 鍵中有多個對應(yīng)同一個用戶 ID 的入口。登出代碼如下(logout.php):

include("retwis.php");  

if (!isLoggedIn()) {  
    header("Location: index.php");  
    exit;  
}  

$r = redisLink();  
$newauthsecret = getrand();  
$userid = $User['id'];  
$oldauthsecret = $r->hget("user:$userid","auth");  

$r->hset("user:$userid","auth",$newauthsecret);  
$r->hset("auths",$newauthsecret,$userid);  
$r->hdel("auths",$oldauthsecret);  

header("Location: index.php");  

這就是我們所描述的,你需要去理解的。

帖子(Updates)

更新(updates),也就是我們知道的帖子(posts),更加簡單。為了創(chuàng)建一個新的帖子我們這么干:

INCR next_post_id => 10343  
HMSET post:10343 user_id $owner_id time $time body "I'm having fun with Retwis"  

如你所見,每篇帖子由有 3 個字段的哈希組成。帖子擁有者的用戶 ID,帖子撞見時間,最后是帖子的正文,真正的狀態(tài)消息。

創(chuàng)建一個帖子后,我們獲取其帖子 ID,LPUSH 其 ID 到帖子作者的每個粉絲用戶的時間軸中,當(dāng)然還有作者自己的帖子列表中(每個人事實上關(guān)注了他自己)。post.php 文件展示了這一切是怎么執(zhí)行的:

include("retwis.php");  

if (!isLoggedIn() || !gt("status")) {  
    header("Location:index.php");  
    exit;  
}  

$r = redisLink();  
$postid = $r->incr("next_post_id");  
$status = str_replace("\n"," ",gt("status"));  
$r->hmset("post:$postid","user_id",$User['id'],"time",time(),"body",$status);  
$followers = $r->zrange("followers:".$User['id'],0,-1);  
$followers[] = $User['id']; /* Add the post to our own posts too */  

foreach($followers as $fid) {  
    $r->lpush("posts:$fid",$postid);  
}  
# Push the post on the timeline, and trim the timeline to the  
# newest 1000 elements.  
$r->lpush("timeline",$postid);  
$r->ltrim("timeline",0,1000);  

header("Location: index.php");  

函數(shù)的核心是這個 foreach 循環(huán)。我們使用 ZRANGE 獲取當(dāng)前用戶的所有粉絲,然后通過遍歷 LPUSH 帖子到每一位粉絲的時間軸列表中。

注意,我們也為所有的帖子維護了一個全局的時間軸,這樣我們就可以在 Retwis 首頁輕易的展示每個人的帖子。這只需要執(zhí)行 LPUSH 到時間軸列表?;氐浆F(xiàn)實,我們難道沒有開始覺得在 SQL 中使用 ORDER BY 來排序按照時間順序添加的東西有一點點奇怪嗎?至少我只這么認為的。

上面的代碼有個有意思的地方值得注意:我們對全局時間軸執(zhí)行完 LPUSH 操作之后使用了一個新命令 LTRIM。這是為了裁剪列表到 1000 個元素。全局時間軸事實上只會用在首頁展示少量帖子,沒有必要獲取全部歷史帖子。

基本上 LTRIM+LPUSH 是 Redis 中創(chuàng)建上限 (capped) 集合的一種方式。

帖子分頁(Paginating)

我們?nèi)绾问褂?LRANGE 來獲取一個范圍的帖子,展現(xiàn)這些帖子在屏幕上,現(xiàn)在已經(jīng)相當(dāng)清楚了。代碼很簡單:

function showPost($id) {  
    $r = redisLink();  
    $post = $r->hgetall("post:$id");  
    if (emptyempty($post)) return false;  

    $userid = $post['user_id'];  
    $username = $r->hget("user:$userid","username");  
    $elapsed = strElapsed($post['time']);  
    $userlink = "<a class=\"username\"href=\"profile.php?u=".urlencode($username)."\">".utf8entities($username)."</a>";  

    echo('<div class="post">'.$userlink.' '.utf8entities($post['body'])."<br>");  
    echo('<i>posted '.$elapsed.'ago via web</i></div>');  
    return true;  
}  

function showUserPosts($userid,$start,$count) {  
    $r = redisLink();  
    $key = ($userid == -1) ? "timeline" : "posts:$userid";  
    $posts = $r->lrange($key,$start,$start+$count);  
    $c = 0;  
    foreach($posts as $p) {  
        if (showPost($p)) $c++;  
        if ($c == $count) break;  
    }  
    return count($posts) == $count+1;  
}  

showPost 只是轉(zhuǎn)換和打印一篇 HTML 帖子,showUserPosts 獲取一個范圍的帖子然后傳遞給 showPost。

注意:如果帖子列表很大的話,LRANGE 比較低效,我們想訪問列表的中間元素,因為 Redis 列表的背后實現(xiàn)是鏈表。如果系統(tǒng)設(shè)計為為幾百萬的項分頁,那最好求助于有序集合。

關(guān)注用戶(Following users)

我們還沒有討論如何創(chuàng)建關(guān)注 / 粉絲關(guān)系,盡管這并不困難。如果 ID 為 1000 的用戶(antirez) 想關(guān)注用戶 ID 為 5000 的用戶(pippo),我們需要同時創(chuàng)建關(guān)注和被關(guān)注關(guān)系。我們只需要調(diào)用 ZADD:

ZADD following:1000 5000  
ZADD followers:5000 1000  

仔細關(guān)注一下同一個模式。理論上,在關(guān)系型數(shù)據(jù)庫中,關(guān)注者列表和粉絲列表會在同一張表中,使用像 following_id 和 follower_id 這樣的列。你可以使用 SQL 查詢來抽取每個用戶的關(guān)注者和粉絲。在鍵值數(shù)據(jù)庫中則有一些不同,因為我們需要設(shè)置 1000 關(guān)注 5000,同時 5000 被 1000 關(guān)注的雙重關(guān)系。這是要付出的代價,但是另一方面,訪問數(shù)據(jù)很簡單并相當(dāng)?shù)目?。將這些作為獨立的集合可以讓我們做一些有意思的事情。例如,使用 ZINTERSTORE 我們可以獲得兩個不同用戶的粉絲的交集,于是我們可以給我們的 Twitter 系統(tǒng)增加一個特性,當(dāng)你訪問某個人的主頁時,可以很快的告訴你” 你和 Alice 有 34 個共同粉絲” 這樣類似的事情。

你可以在 follow.php 中找到設(shè)置和刪除關(guān)注/粉絲關(guān)系的代碼。

水平伸縮(horizontally scalable)

親愛的讀者,如果你意識到了這一點你就已經(jīng)是一個英雄了。謝謝你。在討論水平伸縮之前有必要查看一下單臺服務(wù)器的性能。Retwis 相當(dāng)?shù)目欤瑳]有任何的緩存。在一臺很慢的過載的服務(wù)器上,apache 的 benchmark 使用 100 個并發(fā)客戶端發(fā)出 10000 個請求,測量出平均 uv 為 5 毫秒。這意味著單臺 Linux 服務(wù)器每天可以服務(wù)數(shù)以百萬計的用戶,這個像猴子屁股一樣的慢,想象一下如果用更新的硬件會是什么結(jié)果。

然而,你不可能永遠使用單臺服務(wù)器,如何伸縮一個鍵值存儲?

Retwis 不執(zhí)行任何多鍵操作,所以伸縮很簡單:你可以使用客戶端分片,或者類似于 Twemproxy 的分片代理,或者是即將橫空出世的 Redis 集群。

想更多的了解這個主題請閱讀我們的分片文檔。這里我們想強調(diào)的是,在鍵值存儲系統(tǒng)中,如果你小心設(shè)計,數(shù)據(jù)集是可以拆分到相互獨立的小的鍵上去。相比較使用語義上更復(fù)雜的數(shù)據(jù)庫系統(tǒng),分布這些鍵到多個節(jié)點更簡單直接和可預(yù)見。

上一篇:復(fù)制下一篇:高可用(下)