Redis應用執行個體Twitter Alike Example

來源:互聯網
上載者:User
1.1     A case study: Design and implementation of a simple Twitter clone using only the Redis key-value store as database and PHP

本章將闡述一個模仿Twitter應用的設計與實現,使用PHP並把Redis作為唯一的資料庫。編程者社區常常將key-value儲存是為一個特殊的資料庫,不能被用來代替web應用開發中的關聯式資料庫。本證將證明相反的結論。

我們的Twitter模仿,叫做Retwis,結構簡單而效能優秀,可以很小成本發布到n個web server和M個Redis Server上。你可以從這裡找到源碼。

用PHP舉例是因為php可能更迅捷,同樣(或者更好)的結果對Ruby,Python,Erlang一樣。 1.2     Key-value stores basics

Key-value儲存的本質是儲存資料的能力,是包含一個key的value。當我們知道儲存時的key時,我們可以之後擷取這個資料。沒有辦法用value來查詢什麼東西。比如我使用SET來將值bar儲存在鍵key上:

SET foo bar

Redis將永久儲存我們的資料,所以我們以後可以查詢”鍵foo上儲存的值是什麼。”,而Redis將返回bar:

GET foo => bar

其他key-value儲存常用的操作是用來刪除指定key及相關values的DEL,SET-if-not-exists(在Redis上叫做SETNX)設定一個key當它不存在時,以及INCR可以原子增加指定key上儲存的一個數值:

SET foo 10
INCR foo => 11
INCR foo => 12
INCR foo => 13

  1.3     Atomic operations

到現在還很簡單,但是在INCR上有點特殊。想象一下,為什麼提供這樣的一個操作而我們可以自己通過很少的指令來實現它。畢竟這很簡單:

X = GET foo

X= x +1

SET foo x

問題是,這種方式在一個用戶端的時候表現良好,x任何時候只有一個值。看看如果兩台機器同時訪問會怎樣:

x = GET foo (yields 10)
y = GET foo (yields 10)
x = x + 1 (x is now 11)
y = y + 1 (y is now 11)
SET foo x (foo is now 11)
SET foo y (foo is now 11)

出問題了。我們兩次增加了值,但應該從10變到12的key現在存的是11。這是因為這個INCR操作是通過GET/increment/SET完成的,不是一個原子操作。而Redis,Memechaed..提供的INCR是原子實現的,服務端會在get-increment-set操作在需要的整個時間內保護它,以避免同時訪問。

令Redis與其他key-value儲存不同的是它提供了更多類似INCR的操作,可以被結合起來完成複雜的問題。這就是為什麼你可以用Redis實現整個web應用而不需要一個SQL資料庫,也不會因此發瘋。 1.4     Beyond key-value stores

本節我們會看到建立仿Twitter需要Reids的什麼功能。第一點是要知道Redis的值可以不僅僅是String。Redis的值支援List和Set,並且有原子操作來控制這些更進階的值,因此即使是在同一個key上的多訪問也是安全的。從list開始:

LPUSH mylist a (now mylist holds one element list 'a')
LPUSH mylist b (now mylist holds 'b,a')
LPUSH mylist c (now mylist holds 'c,b,a')

LPUSH意思是left push,就是向mylist所儲存的list的左端(或者說頭部)增加一個元素。如果mylist鍵不存在,就會在push之前自動建立一個空的list。你可以想象,RPUSH指令會把元素加在list的右端(尾部)。

對於我們的仿Twitter來說這非常有用。比如使用者的更新可以放在一個存在username:updates的list上。當然有操作從這些lists擷取資料或資訊。比如LRANGE返回list的一段,或者整個list。

LRANGE mylist 0 1 => c, b

LRANGE使用0起始的索引,第一個元素索引是0,第二個是1,以此類推。指令的參數是LRANGE key first-index last-index,last index參數可以是負數,-1表示列表的最後一個元素,-2是倒數第二個,類推。所以我們可以這樣擷取整個list:

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

另一個重要的指令是LLEN,返回list的長度,以及LTRIM,LTRIM比較象LRANGE但是不是返回指定範圍而是縮減list,所以它就像”擷取mysql的一段,然後設定為新的值”的原子操作。我們只使用了這些List的操作,但是請務必查看Redis文檔來找到所有Redis支援的List操作。

  1.5     The set data type

不僅僅是List,Redis也支援Set,無序的元素集合。可以增加,移除以及檢測成員是否存在,以及執行不同集合之間的交集。當然也可以要求list或集合的元素數目。舉些例子更清晰。記住SADD是增加到set的操作,SREM是從set移除的操作,sismember是檢測是否為成員的操作,SINTER是執行交集操作,SCARD是擷取集合的勢(成員數),SMEMBER將返回set的所有成員。

SADD myset a
SADD myset b
SADD myset foo
SADD myset bar
SCARD myset => 4
SMEMBERS myset => bar,a,foo,b

注意SMEMBER的返回成員的順序並不是我們加入的順序,因為set是無序的元素集合。如果你想有序儲存最好用list。一些對set的操作:

SADD mynewset b
SADD mynewset foo
SADD mynewset hello
SINTER myset mynewset => foo,b

SINTER可以返回集合的交集,但是不限於兩個集合,你可以查詢4,5個或10000個集合。最後我們看看SISMEMBER怎麼工作的:

SISMEMBER myset foo => 1
SISMEMBER myset notamember => 0

Ok,我想我們可以開始coding了。

(代碼太多,沒什麼寫在這裡的價值,刪掉了) 1.1     Prerequisites

如果還沒有下載源碼,請先下載Retwis的源碼。簡單的tar.gz檔案,裡面是幾個php檔案。實現很簡單。你可以從裡面找到用來串連Redis server的php用戶端庫(redis.php)。這個庫檔案是Ludovico Magnocavallo寫的,你可以在你的項目裡隨意使用,但是庫檔案的更新版本,請從Redis的發布下載。

另一個你可能需要的是一個工作的Redis server,擷取源碼,用make編譯,用./redis-server運行就好了。隨便在你機器上泡泡,完全不需要設定什麼。

  1.2     Data layout

如果用關聯式資料庫,這一步是規劃資料應該是怎樣的表,索引,等等。我們沒有表,該怎麼設計。我們需要識別出要表示出我們的對象需要哪些key,以及這些key要保持的是怎樣的value。

從user開始。我們當然需要表示user,通過使用者名稱username,userid,password,followers以及following user,等等。第一個問題是,在我們系統內部通過什麼標記使用者。username是一個好主意因為他是唯一的,但是也是很大的,我們希望節省記憶體。所以就像我們的資料庫是關係型的一樣,我們可以管理一個唯一的id到每個使用者。使用者所有其他資訊通過id引用。做起來很簡單,因為我們有一個原子操作INCR。當我們建立一個新使用者的時候,我們可以像下面這樣,假如使用者叫做”antirez”:

INCR global:nextUserId => 1000
SET uid:1000:username antirez
SET uid:1000:password p1pp0

我們用global:nextUserId鍵來為每個新使用者獲得始終唯一的ID。然後我們用這個唯一id聯結其他使用者資料。這是個key-value的設計模式。牢記這點。此外的欄位已經定義了,我們需要一些更多的材料來完整定義一個使用者。比如有時候是很有用的就是通過username擷取id,所以我們也設定這個key:

SET username:antirez:uid 1000

乍看有點奇怪,但是記住我們只能通過key訪問資料。不可能告訴Redis指定的value返回key。這也是我們的力量,這個新範例強制我們這樣組織資料,用關聯式資料庫的話來說,每個東西都是通過主鍵訪問的。 1.3     Following, followers and updates

這是另一個我們系統需要的樞紐。每個使用者都有followers使用者和following使用者。對此我們有一個完美的資料結構。就是…set。所以我們增加兩個新欄位到規劃裡:

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

另一個我們需要的重要東西是我們可以增加更新到使用者首頁的顯示上。我們需要按照時間順序從新到舊訪問這個資料,所以這項工作最完美的值是List。基本上每個新更新會LPUSH到使用者更新的key上,而通過LRANGE我們可以進行分頁等工作。注意我們使用的”更新”和”發布”是可替換的,因為更新在某種程度上是”小型發布”:

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

  1.4     Authentication

Ok,我們有了使用者或多或少的資訊,但是除了鑒權。我們將鑒權控制的簡單而健壯:我們不需要php的session或者其他類似的東西,我們的系統必須為在不同的server上分布做好準備,所以我們在Redis資料庫上控制整個狀態。我們需要做的是一個隨機串作為一個已驗證使用者的cookie,以及一個key來告訴我們用戶端的這個隨機串對應的user id。我們需要兩個key來實現這種方式:

SET uid:1000:auth fea5e81ac8ca77622bed1c2132a021f9
SET auth:fea5e81ac8ca77622bed1c2132a021f9 1000

為了校正一個使用者,我們做了簡單的事情(login.php): Get the username and password via the login form 通過登入表單擷取使用者名稱和密碼 Check if the username:<username>:uid key actually exists 檢查 username:<username>:uid的索引值是否存在 If it exists we have the user id, (i.e. 1000) 如果存在,我們就有了使用者id Check if uid:1000:password matches, if not, error message 檢查uid:1000:password是否匹配,如果不匹配,報錯 Ok authenticated! Set "fea5e81ac8ca77622bed1c2132a021f9" (the value of uid:1000:auth) as "auth" cookie 校正通過。設定” fea5e81ac8ca77622bed1c2132a021f9”(uid:1000:auth的)作為cookie”auth”

這是實際的代碼:

{代碼}

這在每次使用者登入的時候執行,但我們還需要一個功能isLoggedIn,以檢查一個使用者是否已經登入了。這是實現isLoggedIn的邏輯步驟: Get the "auth" cookie from the user. If there is no cookie, the user is not logged in, of course. Let's call the value of this cookie <authcookie> 擷取使用者cookie”quth”,如果沒有,使用者未登入,當然我們稱這個cookie值為<authcookie> Check if auth:<authcookie> exists, and what the value (the user id) is (1000 in the exmple). 檢測auth:<authcookie>是否存在,以及他的值 In order to be sure check that uid:1000:auth matches. 檢查與uid:1000:auth是否匹配 Ok the user is authenticated, and we loaded a bit of information in the $User global variable. Ok,使用者校正過了,我們載入一點user的全域資訊。

代碼比描述的要簡單,可能是這樣:

 
{代碼}

loadUserInfo是單獨的方法,對我們的應用來說不好,但是對複雜應用來說這是個很好的模板。校正漏掉的唯一一件事是logout。我們怎麼做logout。簡單,改變uid:1000:auth的隨機串值,移除舊的auth:<oldauthstring>然後放一個新的auth:<newauthstring>。

重要:logout過程說明了為什麼在找到auth:<randomstring>後我們為什麼不是單單校正使用者,而是雙重校正它是否匹配uid:1000:auth。真正的校正串是後一個,而auth:<randomstring>只是一個校正key,可能是易變的,或者如果程式有bug或指令碼中斷,我們會有多個auth:<something>的key指向同一個user id。Logout的代碼如下:(logout.php)

 
{代碼}
這就是剛才描述的,很容易理解。
1.5     Updates

更新,或者說發布,相當的簡單。為了在資料庫建立一個新的更新,我們做了下面的事情:

INCR global:nextPostId => 10343
SET post:10343 "$owner_id|$time|I'm having fun with Retwis"

你可以看到,發布的使用者id和時間都存在這個串裡了,在這個樣本程式中我們不需要根據時間或user id尋找,所以好的做法是把所有資訊打包到發布串裡。

在建立發布後,我們獲得了發布的id。我們需要LPUSH這個id到每個following這個post的使用者的user,當然還有作者的發布list。這是update.php,看看它怎麼做的:

 
{代碼}

功能的核心是foreach,我們用SMEMEBER獲得目前使用者所有的folloer,然後對每一個follower使用LPUSH這個post到uid:<userid>:posts去。

注意我們還維護了一個所有發布的時間軸。這麼做只要LPUSH這個post到global:timeline就可以了。看看它,你不認為通過SQL的ORDER BY去排序一個按時間順序插入的資料很奇怪嗎。我覺得很奇怪。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.