分享Redis使用全攻略:如何跳出SQL這個坑
隨著資料體積的激增,MySQL+memcache已經滿足不了大型網際網路類應用的需求,許多機構也紛紛選擇Redis作為其架構上的補充,然而Redis的使用門檻並不低,比如不支援SQL等,這裡為大家分享Redis的使用全攻略。
Redis,備受關注的NoSQL資料庫之一,已為眾多知名網際網路公司使用,比如新浪微博、Pinterest及Viacom。然而,天生不支援SQL卻讓他看起來很不容易接近,這裡我們一起看@utopiar的博文——探索Redis。
探索之一:Redis? What is it?
簡而言之,Redis是一種強大的key-value資料庫,之所以強大有兩點:響應速度快(所以資料記憶體儲存,只在必要時寫入磁碟),特性豐富(支援多種資料型別,以及各型別上的複雜操作)。
事實上,Redis的一個重要特性就是它並非通常意義上的資料庫,雖然稱之為資料庫是因為它可以為你儲存和維護資料,但它並不像關係資料庫那樣提供任何的SQL方言。不過不用擔心,Redis並不是吞噬資料的黑洞,它只是不支援SQL及相關功能,但卻提供了穩健的協議用於與之互動。
在Redis中,沒有資料表的概念,也無須關心select、join、view等操作或功能,同時也不提供類似於int或varchar的資料欄位。你面對的將是相對原始的資料集合及資料型別。
探索之二:Available datatypes
下面我們深入看下這個奇怪的資料庫是如何工作的。如上所見,Redis是基於key-value正規化儲存資料,所以先來重點看下"key"的概念。
key本質上就是簡單的字串,諸如"username"、"password"等。在定義key時,除了不能使用空格,你可以隨意的使用普通的字元、數字等,像".",":","_"等在定義key時都能正常使用,所以像"user_name", "user:123:age", "user:123:username"都是不錯的key的定義方式。
不像RDBMS中的欄位名稱,這裡的key是Redis中的重要組成部分,所以我們必須在處理key時多加小心。在下面的講述中,Redis並沒有table的概念,所以像"SELECT username from users WHERE user_id=123;"這種簡單任務都只能換種方式實現,為了達到這種目的,在Redis上,一種方式是通過key "user:123:username"來獲取結果value。如你所見,key的定義中攜帶了神祕資訊(像user ids)。在Redis中,key的重要性可見一斑。(其他key-value資料庫中key的地位也是如此。)
現在你應該對key有了清楚的瞭解,下面帶你進入可用的資料型別的神奇世界。
Strings
String是Redis中最基本的資料型別,它就是普通的二進位制安全的字串,支援最大資料長度為1Gb。
可以通過SET命令給一個key設定String型別的資料,並可通過GET命令根據key取得結果。如果你想儲存數字資訊,像計數器,你也可以把它儲存到String資料中,並可使用INCR和DECR對之做自增和自減操作。
Lists
List是string資料的集合,其中各資料項按插入順序排列。你可以把list當作一個鎖鏈(chain),所以可以在鎖鏈最左邊(鏈頭)或最右邊(鏈尾)新增一個新的鏈結(link);當然也可以加在鎖鏈中間,但卻要打斷某個鏈結。
可能通過LPUSH和RPUSH命令給list新增資料(L:left, R:right),通過LPOP或RPOP命令彈出元素(同時刪除該元素),也可以通過LRANGE獲取指定範圍的元素(僅返回資料,不會刪除任何元素)。另外也可通過LSET在指定位置新增元素,但通常這種操作比簡單的LPUSH或RPUSH要慢很多。
Hashes
Hashes以簡潔的方式儲存關係更為緊密的資料。Hashes為每個儲存的key實現一個內建的key-value對來儲存資料,例如對於"user"這個key,它的值可以是多個欄位以及與每個字元相應的值對組成資料集。如果你熟悉像ruby或javascript等程式語言,這裡的hashes與那些語言中的hash概念大同小異。
Sets
Sets和它在數學上的同名概念"集合"意義相同,是包含不重複元素的集合。在Redis中,這些物件變成了Redis裡的String型別。正如你想,sets與lists不同的地方在於:sets中的元素是無序的,並且不能重複,你不能在sets中放進兩個相同的資料。
可以通過SADD往set中新增資料,SREM刪除資料,或者通過SPOP返回並刪除此資料。此外還可以通過SUNION, SINTER, SDIFF命令分別實現集合上的"並集", "交集", "差集"操作。
Ordered sets
Ordered sets與sets類似,不同地方在於Ordered set中的每個元素都有一個權重,用於與其他元素比較並排序。
當然ordered set與普通sets具有類似的操作,ZADD和ZREM分別是新增和刪除元素。Ordered set也有自己獨有的操作:ZINCR和ZSCORE,前則用於為元素的權重+1,後則則返回元素的權重值。
探索之三:Where are my tables?
使用Redis與我們之前使用的SQL資料表完全不同,沒有語言支援你在伺服器上查詢資料,這裡僅有一些命令幫你操作資料庫中的keys值。Redis中的命令是資料型別敏感型的,也就是說你不能在list上執行set命令,否則你將得到一個執行錯誤的提示。可以通過redis-cli或其他你使用的程式語言中的介面給Redis server傳送命令。在下面的示例中,我們只強調命令本身,而不關注你通過哪種方式提交給Redis server。
想像一下,一個簡單的SQL資料庫表,像一些應用中會用到的儲存使用者資料的表:
儲存資料
假如我們想把上面的資料儲存到Redis中,你會如何在Redis中設計資料庫方案呢?也許以應用的視覺來看會更直觀一些。使用SQL,我們在SELECT中通過指定使用者id來獲得一個使用者資訊,換句話說就是需要有用於區分不同資料實體的方式,所以我們可以通過一個唯一的標識來標識和獲取使用者資訊。所以如果在redis的key中加入使用者的id資訊,那麼我們的查詢需求就解決了,在redis中,資料被儲存成如下形式:
那麼,給出任一個使用者id,我們就可以通過key user:id:username,user:id:password,user:id:name,user:id:surname的形式讀出使用者資訊。
使用者登入
上面的儲存形式也能用於使用者登入,但需要一種方式能根據username來查詢使用者的id。也就是說我們還需要在username和id之間建立聯絡。這可以通過新增另外一個redis key"user:username:id"來實現。
現在如果Mario Rossi想要登入進來,我們可以通過key"user:user2:id"先查出username,進而獲得使用者的所有資訊。
主鍵
在Redis中如何保證id值的唯一性呢。在SQL中,可以通過"id int primary key auto_increment"定義自增主鍵來實現,現在我們也需要一種類似的方式為每個使用者生成一個不同的id。根據前面可用的資料型別中提到的數字資料,Redis中的方案是這樣的:建立一個key"user:next_id",並把它作為計數器,每當要新增新使用者時,就對key"user:next_id"執行INCR命令。
SELECT * FROM users;
下一個面臨的問題是查詢使用者列表。也許你認為我們上面的資料儲存已經足以查詢出使用者列表:可以先獲得"user:next_id"的當前值counter,然後通過一步或多步遍歷0到counter獲得使用者資料。但如果某個使用者從系統中刪除(下面會講到刪除操作),而我們會遍歷0到counter中的所有id,這時就會有些id查詢不到任何資料。
儘管這通常不是問題,但我們不想在不存在的使用者資料上浪費時間,所以需要建立另外一個key"user:list",其value為list或set型別,用於儲存每一個新增的使用者id,並在必要的時候從"user:list"中刪除該id。我更傾向於使用list,因為它可能通過LRANGE命令實現分頁功能。
刪除使用者
還有一個要面臨的問題是"資料完整性",看看我們在刪除使用者時會發生什麼吧。我們需要刪除每一個對此使用者的引用,也就是說,需要刪除下面所有的key"user:id:*","user:username:id",以及"user:list"中的使用者id。
探索之四:A Simple use case
為了學習致用,我們嘗試設計一個虛擬圖書館,並能根據主題對圖書分組。下面的例子比上面的使用者表會稍微複雜些,但你將學會如何在Redis中處理關聯關係。
在應用中,我們需要收集圖書,並存儲他們的title,author(s), topic(s), pages, price, ISBN和description。顯然有些圖書的作者不止一個,並且它也許會涵蓋不同的主題(例如一本書可以是程式設計主題,也可以是描述的ruby程式設計)。另外一個作者可能寫了很多本書,而一個主題必然會包含很多本書。可以看出,這裡出現了作者和圖書、主題和圖書的多對多關係。
SQL場景
首先,我們嘗試使用SQL資料表為此種場景建資料模型,以便於我們更直觀的在Redis領域中模擬:
Redis場景
前面已經介紹瞭如何在Redis中儲存資料,所以這裡理解Books,Authors和Topics這三張表應該不成問題。但當面對Books-Authors和Book-Topics這種表之間的多對多關聯時,問題變得複雜起來。下面以Topic為例來看如何解決Book與Topic之間的關聯,一旦對這個關係清楚了,Book與Author之間的關係也就迎刃而解了。
對於每本book,我們需要知道它屬於哪些topics;同樣對於每個topic,也要處理它包含的每本book。換句話說,對每本book,需要一個儲存它所關聯的topic的id列表,對於每個topic,同樣需要一個儲存它關聯的book的id列表。這正是set大展身手的地方。我們將建立兩個sets:"book:id:topic"和"topic:id:books",前者儲存book的topics'id列表,後者儲存topic的books'id列表。以前面SQL場景中的資料為例,圖書"Programming Erlang"(books表中的id為2),將有一個key為"book:2:topics",value為set型別且資料為(1,3)的資料資訊;而主題"programming"則會有一個key為"topic:1:books",值為(1,2)的資料集。
經過分析,就得出了Redis場景下的資料模型:
可以看出,在SQL中的多對多關聯,在Redis中可以通過兩個set來實現。你會發現這種實現相當有用,它給我們提供了一種可以自由獲得其他資訊的能力:可以通過對所有感興趣的"topic:id:books"集合中交集操作,從而獲得隸屬於多個主題的圖書。例如對集合"topic:1:books"(programming主題)和"topic:2:books"(ruby主題)做交集,會得到只有一個元素(1)的集合,從而獲得id=1的圖書:programming Ruby。
對於這種實現,你必須特別關注對資料的刪除操作。因為topics裡有對books的引用,同樣books裡有對topics的引用,那刪除如何操作?以刪除books中的資料為例,首先想到的是要刪除每個key為"book:id:*"的資料,但執行此操作前需要先遍歷topics中的所有key為"topic:id:books"的集合,並從中刪除待刪除圖書的id,當然也要從books中key為"book:list"的列表中刪除此id。如果想刪除一個topic,操作也很類似:從topics中刪除所有key為"topic:id:*"資訊之前,需要先遍歷books中的key為"books:id:topics"的topic id集,並從中刪除待刪除topic的id,同時從"topic:list"列表中也刪除此id。同樣的操作對於authors一樣適用。
探索之五:Back home
對於Redis的探索到一段落,現在回頭看看我們的旅行包裡收穫了哪些精彩。
我們學習了Redis中的資料型別及操作命令,還有一些其他有趣的東西。 是不是還有幾段記憶深刻的故事呢:
通過對String資料執行INCR命令解決唯一自增主鍵問題
通過含義豐富的key:"user:username:id"處理使用者登入場景
通過set實現資料間的多對多關聯
到此為止,Redis之旅已經結束,希望未給你帶來不快。最後送上一副良濟:having fun coding free software!