PHP + Redis 實(shí)現(xiàn)一個(gè)簡(jiǎn)單的twitter

原文位于Redis官網(wǎng)http://redis.io/topics/twitter-clone

Redis是NoSQL數(shù)據(jù)庫(kù)中一個(gè)知名數(shù)據(jù)庫(kù),在新浪微博中亦有部署,適合固定數(shù)據(jù)量的熱數(shù)據(jù)的訪問(wèn)。

作為入門蚪黑,這是一篇很好的教材,簡(jiǎn)單描述了如何使用KV數(shù)據(jù)庫(kù)進(jìn)行數(shù)據(jù)庫(kù)的設(shè)計(jì)队橙。新的項(xiàng)目www.xiayucha.com亦采用Redis + MySQL進(jìn)行開發(fā)拦坠,考慮Redis文檔比較少连躏,故翻譯了此文。

其他參考資料:

Redis命令參考中文版(Redis Command Reference)

Try Redis

我會(huì)在此文中描述如何使用PHP以及僅使用Redis來(lái)設(shè)計(jì)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的Twitter克隆贞滨。

很多編程社區(qū)常認(rèn)為KV儲(chǔ)存是一個(gè)特別的數(shù)據(jù)庫(kù)入热,在web應(yīng)用中不能替代關(guān)系數(shù)據(jù)庫(kù)。

本文嘗試證明這恰恰相反疲迂。

這個(gè)twitter克隆名為Retwis才顿,結(jié)構(gòu)簡(jiǎn)單,性能優(yōu)異尤蒿,能很輕易地用N個(gè)web服務(wù)器和Redis服務(wù)器以分布式架構(gòu)郑气。

在此獲取源碼http://code.google.com/p/redis/downloads/list

我們使用PHP作為例子是因?yàn)樗鼙幻總€(gè)人讀懂腰池,也能使用Ruby尾组、Python、Erlang或其他語(yǔ)言獲取同樣(或者更佳)的效果示弓。

注意:Retwis-RB是一個(gè)由Daniel Lucraft用Ruby與Sinatra寫的Retwis分支讳侨!

此文全部代碼在本頁(yè)尾部的Git repository鏈接里。

此文以PHP為例奏属,但是Ruby程序員也能檢出其他源碼跨跨。他們很相似。

注意Retwis-J是Retwis的一個(gè)分支囱皿,由Costin Leau以Java和Spring框架寫成勇婴。

源碼能在GitHub找到,并且在springsource.org有綜合的文檔嘱腥。

Key-value 數(shù)據(jù)庫(kù)基礎(chǔ)

KV數(shù)據(jù)的精髓耕渴,是能夠把value儲(chǔ)存在key里,此后該數(shù)據(jù)僅能夠通過(guò)確切的key來(lái)獲取齿兔,無(wú)法搜索一個(gè)值橱脸。

確切的來(lái)講础米,它更像一個(gè)大型HASH/字典,但它是持久化的添诉,比如屁桑,當(dāng)你的程序終止運(yùn)行,數(shù)據(jù)不會(huì)消失栏赴。

比如我們能用SET命令以key foo 來(lái)儲(chǔ)存值 bar

SET foo bar

Redis會(huì)永久儲(chǔ)存我們的數(shù)據(jù)掏颊,所以之后我們可以問(wèn)Redis:“儲(chǔ)存在key foo里的數(shù)據(jù)是什么?”艾帐,Redis會(huì)返回一個(gè)值:bar

GET foo => bar

KV數(shù)據(jù)庫(kù)提供的其他常見操作有:DEL乌叶,用于刪除指定的key和關(guān)聯(lián)的value;

SET-if-not-exists (在Redis上稱為SETNX )僅會(huì)在key不存在的時(shí)候設(shè)置一個(gè)值柒爸;

INCR能夠?qū)χ付ǖ膋ey里儲(chǔ)存的數(shù)字進(jìn)行自增准浴。

SET foo 10

INCR foo => 11

INCR foo => 12

INCR foo => 13

原子操作

目前為止它是相當(dāng)簡(jiǎn)單的,但是INCR有些不同捎稚。設(shè)想一下乐横,為什么要提供這個(gè)操作?畢竟我們自己能用以下簡(jiǎn)單的命令實(shí)現(xiàn)這個(gè)功能:

x = GET foo

x = x + 1

SET foo x

問(wèn)題在于要使上面的操作正常進(jìn)行今野,同時(shí)只能有一個(gè)客戶端操作x的值葡公。看看如果兩臺(tái)電腦同時(shí)操作這個(gè)值會(huì)發(fā)生什么:

x = GET foo (返回10)

y = GET foo (返回10)

x = x + 1 (x現(xiàn)在是11)

y = y + 1 (y現(xiàn)在是11)

SET foo x (foo現(xiàn)在是11)

SET foo y (foo現(xiàn)在是11)

問(wèn)題發(fā)生了条霜!我們?cè)黾恿酥祪纱未呤玻緫?yīng)該從10變成12,現(xiàn)在卻停留在了11宰睡。這是因?yàn)橛肎ET和SET來(lái)實(shí)現(xiàn)INCR不是一個(gè)原子操作(atomic operation)蒲凶。

所以Redis\memcached之類提供了一個(gè)原子的INCR命令,服務(wù)器會(huì)保護(hù)get-increment-set操作拆内,以防止同時(shí)的操作旋圆。

讓Redis與眾不同的是它提供了更多類似INCR的方案,用于解決模型復(fù)雜的問(wèn)題麸恍。

因此你可以不使用任何SQL數(shù)據(jù)庫(kù)灵巧、僅用Redis寫一個(gè)完整的web應(yīng)用,而不至于抓狂抹沪。

超越Ke-Value數(shù)據(jù)庫(kù)

本節(jié)我們會(huì)看到構(gòu)建一個(gè)Twitter克隆所需Redis的功能刻肄。首先需要知道的是,Redis的值不僅僅可以是字符串(String)采够。

Redis的值可以是列表(Lists)也可以是集合(Sets)肄方,在操作更多類型的值時(shí)也是原子的冰垄,所以多方操作同一個(gè)KEY的值也是安全的蹬癌。

讓我們從一個(gè)Lists開始:

LPUSH mylist a (現(xiàn)在mylist含有一個(gè)元素:'a'的list)

LPUSH mylist b (現(xiàn)在mylist含有元素'b,a')

LPUSH mylist c (現(xiàn)在mylist含有'c,b,a')

LPUSH的意思是Left Push权她, 就是把一個(gè)元素加在列表(list)的左邊(或者說(shuō)頭上)。

在PUSH操作之前逝薪,如果mylist這個(gè)鍵(key)不存在隅要,Redis會(huì)自動(dòng)創(chuàng)建一個(gè)空的list。

就像你能想到的一樣董济,同樣有個(gè)RPUSH操作可以把元素加在列表(list)的右邊(尾部)步清。

這對(duì)我們復(fù)制一個(gè)twitter非常有用,例如我們可以把用戶的更新儲(chǔ)存在username:updates里虏肾。

當(dāng)然廓啊,我們也有相應(yīng)的操作來(lái)獲取數(shù)據(jù)或者信息。比如LRANGE返回列表(list)的一個(gè)范圍內(nèi)的元素封豪,或者所有元素

LRANGE mylist 0 1 => c,b

LRANGE使用從零開始的索引(zero-based indexes)谴轮,第一個(gè)元素的索引是0,第二個(gè)是1吹埠,以此類推第步。該命令的參數(shù)是:LRANGE key first-index last-index

參數(shù)last index可以是負(fù)數(shù),具有特殊的意義:-1是列表(list)的最后一個(gè)元素缘琅,-2是倒數(shù)第二個(gè)粘都,以此類推。

所以刷袍,如果要獲取整個(gè)list翩隧,我們能使用以下命令:

LRANGE mylist 0 -1 => c,b,a

其他重要的操作有LLEN,返回列表(list)的長(zhǎng)度呻纹,LTRIM類似于LRANGE鸽心,但不僅僅會(huì)返回指定范圍內(nèi)的元素,而且還會(huì)原子地把列表(list)的值設(shè)置這個(gè)新的值居暖。

我們將會(huì)使用這些list操作顽频,但是注意閱讀Redis文檔來(lái)瀏覽所有redis支持的list操作。

數(shù)據(jù)類型:集合(set)

除了列表(list)太闺,Redis還提供了集合(sets)的支持糯景,是不排序(unsorted)的元素集合。

它能夠添加省骂、刪除蟀淮、檢查元素是否存在,并且獲取兩個(gè)結(jié)合之間的交集钞澳。當(dāng)然它也能請(qǐng)求獲取集合(set)里一個(gè)或者多個(gè)元素怠惶。

幾個(gè)例子可以使概念更為清晰。記自凇:SADD是往集合(set)里添元素策治;SREM是從集合(set)里刪除元素脓魏;SISMEMBER是檢測(cè)一個(gè)元素是否包含在集合里;SINTER用于顯示兩個(gè)集合的交集通惫。

其他操作有茂翔,SCARD用于獲取集合的基數(shù)(集合中元素的數(shù)量);SMEMBERS返回集合中所有的元素

SADD myset a

SADD myset b

SADD myset foo

SADD myset bar

SCARD myset => 4

SMEMBERS myset => bar,a,foo,b

注意SMEMBERS不會(huì)以我們添加的順序返回元素履腋,因?yàn)榧?Sets)是一個(gè)未排序的元素集合珊燎。如果你要儲(chǔ)存順序,最好使用列表(Lists)取而代之遵湖。以下是基于集合的一些操作:

SADD mynewset b

SADD mynewset foo

SADD mynewset hello

SINTER myset mynewset => foo,b

SINTER能夠返回集合之間的交集悔政,但并不僅限于兩個(gè)集合(Sets),你能獲取4個(gè)延旧、5個(gè)甚至1000個(gè)集合(sets)的交集卓箫。

最后,讓我們看下SISMEMBER是如何工作的:

SISMEMBER myset foo => 1

SISMEMBER myset notamember => 0

Okay垄潮,我覺(jué)得我們可以開始coding啦烹卒!

先決條件

如果你還沒(méi)下載,請(qǐng)前往<http: //code.google.com/p/redis/downloads/list>下載Retwis的源碼弯洗。它包含幾個(gè)PHP文件旅急,是個(gè)簡(jiǎn)單的 tar.gz文件。

實(shí)現(xiàn)的非常簡(jiǎn)單牡整,你會(huì)在里面找到PHP客戶端(redis.php)藐吮,用于redis與PHP的交互。該庫(kù)由Ludovico Magnocavallo(http://qix.it/)編寫逃贝,你可以在自己的項(xiàng)目中免費(fèi)使用谣辞。

但如果要更新庫(kù)的版本請(qǐng)下載Redis的發(fā)行版。(注意:現(xiàn)在有更好的PHP庫(kù)了沐扳,請(qǐng)檢查我們的客戶端頁(yè)面<http://redis.io/clients>)

你需要的另一個(gè)東西是正常運(yùn)行的Redis服務(wù)器泥从。僅需要獲取源碼、用make編譯沪摄、用./redis-server就完工了躯嫉,點(diǎn)兒也不須配置就可以在你的電腦上運(yùn)行Retwis。

數(shù)據(jù)結(jié)構(gòu)規(guī)劃

當(dāng)使用關(guān)系數(shù)據(jù)庫(kù)的時(shí)候杨拐,這一步往往是在設(shè)計(jì)數(shù)據(jù)表祈餐、索引的表單里處理。我們沒(méi)有表哄陶,那我們?cè)O(shè)計(jì)什么呢帆阳? 我們需要確認(rèn)物體使用的key以及key采用的類型。

讓我們從用戶這塊開始設(shè)計(jì)屋吨。當(dāng)然了蜒谤,首先需要展示用戶的username, userid, password, followers山宾,自己follow的用戶等。第一個(gè)問(wèn)題是:如何在我們的系統(tǒng)中標(biāo)識(shí)一個(gè)用戶芭逝?

username是個(gè)好主意,因?yàn)樗俏ㄒ坏脑ㄐ亍2贿^(guò)它太大了旬盯,我們想要降低內(nèi)存的使用。如果我們的數(shù)據(jù)庫(kù)是關(guān)系數(shù)據(jù)庫(kù)翎猛,我們能關(guān)聯(lián)唯一ID到每一個(gè)用戶胖翰。每一個(gè)對(duì)用戶的引用都通過(guò)ID來(lái)關(guān)聯(lián)。

做起來(lái)很簡(jiǎn)單切厘,因?yàn)槲覀冇形覀兊脑拥腎NCR命令萨咳!當(dāng)我們創(chuàng)建一個(gè)新用戶,我們假設(shè)這個(gè)用戶叫"antirez":

INCR global:nextUserId => 1000

SET uid:1000:username antirez

SET uid:1000:password p1pp0

我們使用global:nextUserId為鍵(Key)是為了給每個(gè)新用戶分配一個(gè)唯一ID疫稿,然后用這個(gè)唯一ID來(lái)加入其他key培他,以識(shí)別保存用戶的其他數(shù)據(jù)。這就是kv數(shù)據(jù)庫(kù)的設(shè)計(jì)模式!請(qǐng)牢記于心遗座,

除了已經(jīng)定義的KEY舀凛,我們還需要更多的來(lái)完整定義一個(gè)用戶,比如有時(shí)需要通過(guò)用戶名來(lái)獲取用戶ID途蒋,所以我們也需要設(shè)置這么一個(gè)鍵(Key)

SET username:antirez:uid 1000

一開始看上去這樣很奇怪猛遍,但請(qǐng)記住我們只能通過(guò)key來(lái)獲取數(shù)據(jù)!這不可能告訴Redis返回包含某值的Key,這也是我們的強(qiáng)處号坡。

用關(guān)系數(shù)據(jù)庫(kù)方式來(lái)講懊烤,這個(gè)新實(shí)例強(qiáng)迫我們組織數(shù)據(jù),以便于僅使用primary key訪問(wèn)任何數(shù)據(jù)宽堆。

關(guān)注\被關(guān)注與更新

這也是在我們系統(tǒng)中另一個(gè)重要需求.每個(gè)用戶都有follower腌紧,也有follow的用戶.對(duì)此我們有最佳的數(shù)據(jù)結(jié)構(gòu)!那就是.....集合(Sets).那就讓我們?cè)诮Y(jié)構(gòu)中加入兩個(gè)新字段:

uid:1000:followers => Set of uids of all the followers users

uid:1000:following => Set of uids of all the following users

另一個(gè)重要的事情是我們需要有個(gè)地方來(lái)放用戶主頁(yè)上的更新。這個(gè)要以時(shí)間順序排序畜隶,最新的排在舊的前面寄啼。所以,最佳的類型是列表(List)代箭。

基本上每個(gè)更新都會(huì)被LPUSH到該用戶的updates key.多虧了LRANGE墩划,我們能夠?qū)崿F(xiàn)分頁(yè)等功能。請(qǐng)注意更新(updates)和帖子(posts)講的是同一個(gè)東西嗡综,實(shí)際上更新(updates)是有點(diǎn)小的帖子(posts)乙帮。

uid:1000:posts => a List of post ids, every new post is LPUSHed here.

驗(yàn)證

OK,除了驗(yàn)證极景,或多或少我們已經(jīng)有了關(guān)于該用戶的一切東西察净。我們處理驗(yàn)證用一個(gè)簡(jiǎn)單而健壯(魯棒)的辦法:我們不使用PHP的session或者其他類似方式驾茴。

我們的系統(tǒng)必須是能夠在不同不同服務(wù)器上分布式部署的,所以一切狀態(tài)都必須保存在Redis里氢卡。所以我們所需要的一個(gè)保存在已驗(yàn)證用戶cookie里的隨機(jī)字符串锈至。

包含同樣隨機(jī)字符串的一個(gè)key告訴我們用戶的ID。我們需要使用兩個(gè)key來(lái)保證這個(gè)驗(yàn)證機(jī)制的健壯性:

SET uid:1000:auth fea5e81ac8ca77622bed1c2132a021f9

SET auth:fea5e81ac8ca77622bed1c2132a021f9 1000

為了驗(yàn)證一個(gè)用戶译秦,我們需要做一些簡(jiǎn)單的工作(login.php):

* 從登錄表單獲取用戶的用戶名和密碼

* 檢查是否存在一個(gè)鍵 username::uid

* 如果這個(gè)user id存在(假設(shè)1000)

* 檢查 uid:1000:password 是否匹配峡捡,如果不匹配,顯示錯(cuò)誤信息

* 匹配則設(shè)置cookie為字符串"fea5e81ac8ca77622bed1c2132a021f9"(uid:1000:auth的值)

實(shí)例代碼:

PHP代碼

include("retwis.php");

#?Form?sanity?checks

if(!gt("username")?||?!gt("password"))

goback("You?need?to?enter?both?username?and?password?to?login.");

#?The?form?is?OK,?checkifthe?username?is?available

$username=?gt("username");

$password=?gt("password");

$r=?redisLink();

$userid=$r->get("username:$username:id");

if(!$userid)

goback("Wrong?username?or?password");

$realpassword=$r->get("uid:$userid:password");

if($realpassword!=$password)

goback("Wrong?useranme?or?password");

#?Username?/?password?OK,?set?the?cookieandredirect?to?index.php

$authsecret=$r->get("uid:$userid:auth");

setcookie("auth",$authsecret,time()+3600*24*365);

header("Location:?index.php");

每次用戶登錄都會(huì)運(yùn)行筑悴,但我們需要一個(gè)函數(shù)isLoggedIn用于檢驗(yàn)一個(gè)用戶是否已經(jīng)驗(yàn)證们拙。

這些是isLoggedIn的邏輯步驟

* 從用戶獲取cookie里auth的值。如果沒(méi)有cookie阁吝,該用戶未登錄砚婆。我們稱這個(gè)cookie為

* 檢查auth:是否存在,存在則獲取值(例子里是1000)

* 為了再次確認(rèn)突勇,檢查uid:1000:auth是否匹配

* 用戶已驗(yàn)證装盯,在全局變量$User中載入一點(diǎn)信息

也許代碼比描述更短:

PHP代碼

functionisLoggedIn()?{

global$User,$_COOKIE;

if(isset($User))returntrue;

if(isset($_COOKIE['auth']))?{

$r=?redisLink();

$authcookie=$_COOKIE['auth'];

if($userid=$r->get("auth:$authcookie"))?{

if($r->get("uid:$userid:auth")?!=$authcookie)returnfalse;

loadUserInfo($userid);

returntrue;

}

}

returnfalse;

}

functionloadUserInfo($userid)?{

global$User;

$r=?redisLink();

$User['id']?=$userid;

$User['username']?=$r->get("uid:$userid:username");

returntrue;

}

把loadUserInfo作為一個(gè)獨(dú)立函數(shù)對(duì)于我們的應(yīng)用而言有點(diǎn)殺雞用牛刀了,但是對(duì)于復(fù)雜的應(yīng)用而言這是一個(gè)不錯(cuò)的模板甲馋。

作為一個(gè)完整的驗(yàn)證验夯,還剩下logout還沒(méi)實(shí)現(xiàn)。在logout的時(shí)候我們?cè)趺醋瞿兀?/p>

很簡(jiǎn)單摔刁,僅僅改變uid:1000:auth里的隨機(jī)字符串挥转,刪除舊的auth:并增加一個(gè)新的auth:

重要:logout過(guò)程解釋了為什么我們不僅僅查找auth:而是再次檢查了uid:1000:auth。真正的驗(yàn)證字符串是后者共屈,auth:是易變的.

假設(shè)程序中有BUGs或者腳本被意外中斷绑谣,那么就有可能有多個(gè)auth:指向同一個(gè)用戶id。

logout代碼如下:(logout.php)

PHP代碼

include("retwis.php");

if(!isLoggedIn())?{

header("Location:?index.php");

exit;

}

$r=?redisLink();

$newauthsecret=?getrand();

$userid=$User['id'];

$oldauthsecret=$r->get("uid:$userid:auth");

$r->set("uid:$userid:auth",$newauthsecret);

$r->set("auth:$newauthsecret",$userid);

$r->delete("auth:$oldauthsecret");

header("Location:?index.php");

以上是我們所描述過(guò)的拗引,應(yīng)該比較易于理解借宵。

更新(Updates)

更新,或者稱為帖子(posts)的實(shí)現(xiàn)則更為簡(jiǎn)單矾削。為了在數(shù)據(jù)庫(kù)里創(chuàng)建一個(gè)新的帖子壤玫,我們做了以下工作:

INCR global:nextPostId => 10343

SET post:10343 "$owner_id|$time|I'm having fun with Retwis"

就像你看到的一樣,帖子的用戶id和時(shí)間直接儲(chǔ)存在了字符串里哼凯。

在這個(gè)例子中我們不需要根據(jù)時(shí)間或者用戶id來(lái)查找帖子欲间,所以把他們緊湊地?cái)D在一個(gè)post字符串里更佳。

在新建一個(gè)帖子之后断部,我們獲得了帖子的id猎贴。需要LPUSH這個(gè)帖子的id到每一個(gè)follow了作者的用戶里去,當(dāng)然還有作者的帖子列表。

update.php這個(gè)文件展示了這個(gè)工作是如何完成的:

PHP代碼

include("retwis.php");

if(!isLoggedIn()?||?!gt("status"))?{

header("Location:index.php");

exit;

}

$r=?redisLink();

$postid=$r->incr("global:nextPostId");

$status=str_replace("\n","?",gt("status"));

$post=$User['id']."|".time()."|".$status;

$r->set("post:$postid",$post);

$followers=$r->smembers("uid:".$User['id'].":followers");

if($followers===?false)$followers=?Array();

$followers[]?=$User['id'];

foreach($followersas$fid)?{

$r->push("uid:$fid:posts",$postid,false);

}

#?Push?the?post?on?the?timeline,andtrim?the?timeline?to?the

#?newest?1000?elements.

$r->push("global:timeline",$postid,false);

$r->ltrim("global:timeline",0,1000);

header("Location:?index.php");

函數(shù)的核心是foreach她渴。 通過(guò)SMEMBERS獲取當(dāng)前用戶的所有follower达址,然后循環(huán)會(huì)把帖子(post)LPUSH到每一個(gè)用戶的 uid::posts里

注意我們同時(shí)維護(hù)了一個(gè)所有帖子的時(shí)間線。為此我們還需要LPUSH到global:timeline里趁耗。

面對(duì)這個(gè)現(xiàn)實(shí)沉唠,你是否開始覺(jué)得:SQL里面用ORDER BY來(lái)按時(shí)間排序有一點(diǎn)兒奇怪? 我確實(shí)是這么想的。

分頁(yè)

現(xiàn)在很清楚苛败,我們能用LRANGE來(lái)獲取帖子的范圍满葛,并在屏幕上顯示。代碼很簡(jiǎn)單:

PHP代碼

functionshowPost($id)?{

$r=?redisLink();

$postdata=$r->get("post:$id");

if(!$postdata)returnfalse;

$aux=explode("|",$postdata);

$id=$aux[0];

$time=$aux[1];

$username=$r->get("uid:$id:username");

$post=?join(array_splice($aux,2,count($aux)-2),"|");

$elapsed=?strElapsed($time);

$userlink=".urlencode($username)."">".utf8entities($username)."";

echo(''.$userlink.'?'.utf8entities($post)."

");

echo('posted?'.$elapsed.'?ago?via?web

');

returntrue;

}

functionshowUserPosts($userid,$start,$count)?{

$r=?redisLink();

$key=?($userid==?-1)??"global:timeline":"uid:$userid:posts";

$posts=$r->lrange($key,$start,$start+$count);

$c=?0;

foreach($postsas$p)?{

if(showPost($p))$c++;

if($c==$count)break;

}

returncount($posts)?==$count+1;

}

當(dāng)showUserPosts獲取帖子的范圍并傳遞給showPost時(shí)著拭,showPost會(huì)簡(jiǎn)單輸出一篇帖子的HTML代碼纱扭。

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

如果用戶id 1000 (antirez)想要follow用戶id1000的pippo牍帚,我們做到這個(gè)僅需兩步SADD:

SADD uid:1000:following 1001

SADD uid:1001:followers 1000

再次注意這個(gè)相同的模式:在關(guān)系數(shù)據(jù)庫(kù)里的理論里follow的用戶和被follow的用戶是一張包含類似following_id和follower_id的單獨(dú)數(shù)據(jù)表儡遮。

用查詢你能明確follow和被follow的每一個(gè)用戶。在key-value數(shù)據(jù)里有一點(diǎn)特別暗赶,需要我們分別設(shè)置1000follow了1001并且1001被1000follow的關(guān)系鄙币。

這是需要付出的代價(jià),但是另一方面講蹂随,獲取這些數(shù)據(jù)即簡(jiǎn)單又超快十嘿。并且這些是獨(dú)立的集合,允許我們做一些有趣的事情岳锁,比如使用SINTER獲取兩個(gè)不同用戶的集合绩衷。

這樣我們也許可以在我們的twitter復(fù)制品中加入一個(gè)功能:當(dāng)你訪問(wèn)某個(gè)人的資料頁(yè)時(shí)顯示"你和foobar有34個(gè)共同關(guān)注者"之類的東西。

你能夠在follow.php中找到增加或者刪除following/folloer關(guān)系的代碼激率。它如你所見般平常咳燕。

使它能夠水平分割

親愛(ài)的讀者,如果你看到這里乒躺,你已經(jīng)是一個(gè)英雄了招盲,謝謝你。在講到水平分割之前嘉冒,看看單臺(tái)服務(wù)器的性能是個(gè)不錯(cuò)的主意曹货。

Retwis讓人驚訝地快,沒(méi)有任何緩存讳推。在一臺(tái)非常緩慢和高負(fù)載的服務(wù)器上顶籽,以100個(gè)線程并發(fā)請(qǐng)求100000次進(jìn)行apache基準(zhǔn)測(cè)試,平均占用5ms银觅。

這意味著你可以僅僅使用一臺(tái)linux服務(wù)器接受每天百萬(wàn)用戶的訪問(wèn)蜕衡,并且慢的跟個(gè)傻猴似的,就算用更新的硬件。

雖然慨仿,就算你有一堆用戶久脯,也許也不需要超過(guò)1臺(tái)服務(wù)器來(lái)跑應(yīng)用,但讓我們假設(shè)我們是Twitter镰吆,需要處理海量的訪問(wèn)量呢?該怎么做?

Hashing the key

第一件事是把KEY進(jìn)行hash運(yùn)算并基于hash在不同服務(wù)器上處理請(qǐng)求帘撰。有大量知名的hash算法,例如ruby客戶端自帶的consistent hashing

大致意思是你能把key轉(zhuǎn)換成數(shù)字万皿,并除以你的服務(wù)器數(shù)量

server_id = crc32(key) % number_of_servers

這里還有大量因?yàn)樘砑右慌_(tái)服務(wù)器產(chǎn)生的問(wèn)題摧找,但這僅僅是大致的意思,哪怕使用一個(gè)類似consistent hashing的更好索引算法牢硅,

是不是key就可以分布式訪問(wèn)了呢?所有用戶數(shù)據(jù)都分布在不同的服務(wù)器上蹬耘,沒(méi)有inter-keys使用到(比如SINTER,否則你需要注意要在同一臺(tái)服務(wù)器上進(jìn)行)

這是Redis不像memcached一樣強(qiáng)制指定索引算法的原因减余,需要應(yīng)用來(lái)指定综苔。另外,有幾個(gè)key訪問(wèn)的比較頻繁位岔。

特殊的Keys

比如每次發(fā)布新帖如筛,我們都需要增加global:nextPostId。單臺(tái)服務(wù)器會(huì)有大量增加的請(qǐng)求抒抬。如何修復(fù)這個(gè)問(wèn)題呢?一個(gè)簡(jiǎn)單的辦法是用一臺(tái)專門的服務(wù)器來(lái)處理增加請(qǐng)求杨刨。

除非你有大量的請(qǐng)求,否則矯枉過(guò)正了擦剑。另一個(gè)小技巧是ID并不需要真正地增加妖胀,只要唯一即可。這樣你可以使用長(zhǎng)度為不太可能發(fā)生碰撞的隨機(jī)字符串(除了MD5這樣的大小惠勒,幾乎是不可能)赚抡。

完工,我們成功消除了水平分割帶來(lái)的問(wèn)題捉撮。

另一個(gè)問(wèn)題是global:timeline怕品。這里有個(gè)不是解決辦法的解決辦法,你可以分別保存在不同服務(wù)器上巾遭,并且在需要這些數(shù)據(jù)時(shí)從不同的服務(wù)器上取出來(lái)肉康,或者用一個(gè)key來(lái)進(jìn)行排序。

如果你確實(shí)每秒有這么多帖子灼舍,你能夠再次用一臺(tái)獨(dú)立服務(wù)器專門處理這些請(qǐng)求吼和。請(qǐng)記住,商用硬件的Redis能夠以100000/s的速度寫入數(shù)據(jù)骑素。我猜測(cè)對(duì)于twitter這足夠了炫乓。

請(qǐng)隨意在下面評(píng)論處提問(wèn)以及反饋。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市末捣,隨后出現(xiàn)的幾起案子侠姑,更是在濱河造成了極大的恐慌,老刑警劉巖箩做,帶你破解...
    沈念sama閱讀 211,948評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件莽红,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡邦邦,警方通過(guò)查閱死者的電腦和手機(jī)安吁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)燃辖,“玉大人鬼店,你說(shuō)我怎么就攤上這事∏辏” “怎么了妇智?”我有些...
    開封第一講書人閱讀 157,490評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)捌锭。 經(jīng)常有香客問(wèn)我俘陷,道長(zhǎng)罗捎,這世上最難降的妖魔是什么观谦? 我笑而不...
    開封第一講書人閱讀 56,521評(píng)論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮桨菜,結(jié)果婚禮上豁状,老公的妹妹穿的比我還像新娘。我一直安慰自己倒得,他們只是感情好泻红,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,627評(píng)論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著霞掺,像睡著了一般谊路。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上菩彬,一...
    開封第一講書人閱讀 49,842評(píng)論 1 290
  • 那天缠劝,我揣著相機(jī)與錄音,去河邊找鬼骗灶。 笑死惨恭,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的耙旦。 我是一名探鬼主播脱羡,決...
    沈念sama閱讀 38,997評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了锉罐?” 一聲冷哼從身側(cè)響起帆竹,我...
    開封第一講書人閱讀 37,741評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎脓规,沒(méi)想到半個(gè)月后馆揉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,203評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡抖拦,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,534評(píng)論 2 327
  • 正文 我和宋清朗相戀三年升酣,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片态罪。...
    茶點(diǎn)故事閱讀 38,673評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡噩茄,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出复颈,到底是詐尸還是另有隱情绩聘,我是刑警寧澤,帶...
    沈念sama閱讀 34,339評(píng)論 4 330
  • 正文 年R本政府宣布耗啦,位于F島的核電站凿菩,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏帜讲。R本人自食惡果不足惜衅谷,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,955評(píng)論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望似将。 院中可真熱鬧获黔,春花似錦、人聲如沸在验。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,770評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)腋舌。三九已至盏触,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間块饺,已是汗流浹背赞辩。 一陣腳步聲響...
    開封第一講書人閱讀 32,000評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留刨沦,地道東北人诗宣。 一個(gè)月前我還...
    沈念sama閱讀 46,394評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像想诅,于是被迫代替她去往敵國(guó)和親召庞。 傳聞我的和親對(duì)象是個(gè)殘疾皇子岛心,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,562評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容