原文:ALCA in Redis-land
一篇對使用Redis在NoSQL的世界中冒險之旅的總結。
The legs of our journey
像每次出發一樣,先對我們這次的旅程路線做個介紹:
探索之一: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數據庫表,像一些應用中會用到的保存用戶數據的表:
id username password name surname
1 user1 pass1 Bob Smith
2 user2 pass2 Mario Rossi
存儲數據
假如我們想把上面的數據存儲到Redis中,你會如何在Redis中設計數據庫方案呢?也許以應用的視覺來看會更直觀一些。使用SQL,我們在SELECT中通過指定用戶id來獲得一個用戶信息,換句話說就是需要有用於區分不同數據實體的方式,所以我們可以通過一個唯一的標識來標識和獲取用戶信息。所以如果在redis的key中加入用戶的id信息,那么我們的查詢需求就解決了,在redis中,數據被存儲成如下形式:
Key Value
user:1:username user1
user:1:password pass1
user:1:name Bob
user:1:surname Smith
user:2:username user2
user:2:password pass2
user:2:name Mario
user: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 value
user:user1:id 1
user: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 description
1 Programming Ruby 829 $26 0974514055 ruby programming language
2 Erlang Programming 496 $42 0596518188 an introduction to erlang
Authors
id name surname
1 Dave Thomas
2 Chad Fowler
3 Andy Hunt
4 Francesco Cesarini
5 Simon Thompson
Topics
id name description
1 programming Books about programming
2 ruby Books about ruby
3 erlang Books about erlang
Books-Authors
book_id author_id
1 1
1 2
1 3
2 4
2 5
Books-Topics
book_id topic_id
1 1
1 2
2 1
2 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:surnameSets
- author:id:booksLists
- authorlist
Books
Strings
- book:id
- book:id:title
- book:id:pages
- book:id:price
- book:id:isbn
- book:id:descriptionSets
- books:id:authors - books:id:topicsLists
- book:list
Topics
String
- topic:id
- topic:id:name
- topic:id:descriptionSets
- topic:id:booksLists
- 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的使用場景也在學習中,歡迎指導。