把 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),也就是我們知道的帖子(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) 集合的一種方式。
我們?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è)計為為幾百萬的項分頁,那最好求助于有序集合。
我們還沒有討論如何創(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)系的代碼。
親愛的讀者,如果你意識到了這一點你就已經(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ù)見。