一篇對使用Redis在NoSQL的世界中冒險之旅的總結。
像每次出發一樣,先對我們這次的旅程路線做個介紹:
Redis? What is it?Available datatypesWhere are my tables?A simple use caseBack home簡而言之,Redis是一種強大的key-value數據庫,之所以強大有兩點:響應速度快(所以數據內存存儲,只在必要時寫入磁盤),特性豐富(支持多種數據類型,以及各類型上的復雜操作)。
事實上,Redis的一個重要特性就是它并非通常意義上的數據庫,雖然稱之為數據庫是因為它可以為你存儲和維護數據,但它并不像關系數據庫那樣提供任何的SQL方言。不過不用擔心,Redis并不是吞噬數據的黑洞,它只是不支持SQL及相關功能,但卻提供了穩健的協議用于與之交互。
在Redis中,沒有數據表的概念,也無須關心select,join,view等操作或功能,同時也不提供類似于int或varchar的數據字段。你面對的將是相對原始的數據集合及數據類型。
下面我們深入看下這個奇怪的數據庫是如何工作的。如上所見,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有了清楚的了解,下面帶你進入可用的數據類型的神奇世界。
String是Redis中最基本的數據類型,它就是普通的二進制安全的字符串,支持最大數據長度為1Gb??梢酝ㄟ^SET命令給一個key設置String類型的數據,并可通過GET命令根據key取得結果。如果你想存儲數字信息,像計數器,你也可以把它存儲到String數據中,并可使用INCR和DECR對之做自增和自減操作。
List是string數據的集合,其中各數據項按插入順序排列。你可以把list當作一個鎖鏈(chain),所以可以在鎖鏈最左邊(鏈頭)或最右邊(鏈尾)添加一個新的鏈結(link);當然也可以加在鎖鏈中間,但卻要打斷某個鏈結。
可能通過LPUSH和RPUSH命令給list添加數據(L:left, R:right),通過LPOP或RPOP命令彈出元素(同時刪除該元素),也可以通過LRANGE獲取指定范圍的元素(僅返回數據,不會刪除任何元素)。另外也可通過LSET在指定位置添加元素,但通常這種操作比簡單的LPUSH或RPUSH要慢很多。
Hashes以簡潔的方式存儲關系更為緊密的數據。Hashes為每個存儲的key實現一個內置的key-value對來存儲數據,例如對于"user"這個key,它的值可以是多個字段以及與每個字符相應的值對組成數據集。如果你熟悉像ruby或javascript等編程語言,這里的hashes與那些語言中的hash概念大同小異。
Sets和它在數學上的同名概念"集合"意義相同,是包含不重復元素的集合。在Redis中,這些對象變成了Redis里的String類型。正如你想,sets與lists不同的地方在于:sets中的元素是無序的,并且不能重復,你不能在sets中放進兩個相同的數據。
可以通過SADD往set中添加數據,SREM刪除數據,或者通過SPOP返回并刪除此數據。此外還可以通過SUNION, SINTER, SDIFF命令分別實現集合上的"并集", "交集", "差集"操作。
Ordered sets與sets類似,不同地方在于Ordered set中的每個元素都有一個權重,用于與其他元素比較并排序。
當然ordered set與普通sets具有類似的操作,ZADD和ZREM分別是添加和刪除元素。Ordered set也有自己獨有的操作:ZINCR和ZSCORE,前則用于為元素的權重+1,后則則返回元素的權重值。
使用Redis與我們之前使用的SQL數據表完全不同,沒有語言支持你在服務器上查詢數據,這里僅有一些命令幫你操作數據庫中的keys值。Redis中的命令是數據類型敏感型的,也就是說你不能在list上執行set命令,否則你將得到一個執行錯誤的提示??梢酝ㄟ^redis-cli或其他你使用的編程語言中的接口給Redis server發送命令。在下面的示例中,我們只強調命令本身,而不關注你通過哪種方式提交給Redis server。
想像一下,一個簡單的SQL數據庫表,像一些應用中會用到的保存用戶數據的表:
id username password name surname1 user1 pass1 Bob Smith2 user2 pass2 Mario Rossi
存儲數據
假如我們想把上面的數據存儲到Redis中,你會如何在Redis中設計數據庫方案呢?也許以應用的視覺來看會更直觀一些。使用SQL,我們在SELECT中通過指定用戶id來獲得一個用戶信息,換句話說就是需要有用于區分不同數據實體的方式,所以我們可以通過一個唯一的標識來標識和獲取用戶信息。所以如果在redis的key中加入用戶的id信息,那么我們的查詢需求就解決了,在redis中,數據被存儲成如下形式:
Key Valueuser:1:username user1user:1:password pass1user:1:name Bobuser:1:surname Smithuser:2:username user2user:2:password pass2user:2:name Mariouser:2:surname Rossi
那么,給出任一個用戶id,我們就可以通過key user:id:username,user:id:password,user:id:name,user:id:surname的形式讀出用戶信息。
用戶登錄
上面的存儲形式也能用于用戶登錄,但需要一種方式能根據username來查詢用戶的id。也就是說我們還需要在username和id之間建立聯系。這可以通過添加另外一個redis key"user:username:id"來實現。
key valueuser:user1:id 1user:user2:id 2
現在如果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領域中模擬:
Books
id title pages price isbn description1 Programming Ruby 829 $26 0974514055 ruby programming language2 Erlang Programming 496 $42 0596518188 an introduction to erlang
Authors
id name surname1 Dave Thomas2 Chad Fowler3 Andy Hunt4 Francesco Cesarini5 Simon Thompson
Topics
id name description1 programming Books about programming2 ruby Books about ruby3 erlang Books about erlang
Books-Authors
book_id author_id1 11 21 32 42 5
Books-Topics
book_id topic_id1 11 22 12 3
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場景下的數據模型:
Authors
Strings- author:id- author:id:name- author:id:surname
Sets- author:id:books
Lists- authorlist
Books
Strings- book:id- book:id:title- book:id:pages- book:id:price- book:id:isbn- book:id:description
Sets- books:id:authors - books:id:topics
Lists- book:list
Topics
String- topic:id- topic:id:name- topic:id:description
Sets- topic:id:books
Lists- topic:list
可以看出,在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!
譯者注:本文是翻譯而來,個人感覺是通過與SQL做對比,來強調Redis的不同,進而讓讀者能跳出SQL的枷鎖,以實現對Redis的理解。
在真實場景中,Redis未必如文中描述的方式使用,我自己對Redis的使用場景也在學習中,歡迎指導。
新聞熱點
疑難解答