很多文章都會說,redis支援5種常用的資料型別,這其實是存在很大的歧義。redis裡存的都是二進位制資料,其實就是位元組陣列(byte[]),這些位元組資料是沒有資料型別的,只有把它們按照合理的格式解碼後,可以變成一個字串,整數或物件,此時才具有資料型別。
這一點必須要記住。所以任何東西只要能轉化成位元組陣列(byte[])的,都可以存到redis裡。管你是字串、數字、物件、圖片、聲音、視訊、還是檔案,只要變成byte陣列。
因此redis裡的String指的並不是字串,它其實表示的是一種最簡單的資料結構,即一個key只能對應一個value。這裡的key和value都是byte陣列,只不過key一般是由一個字串轉換成的byte陣列,value則根據實際需要而定。
在特定情況下,對value也會有一些要求,比如要進行自增或自減操作,那value對應的byte陣列必須要能被解碼成一個數字才行,否則會報錯。
那麼List這種資料結構,其實表示一個key可以對應多個value,且value之間是有先後順序的,value值可以重複。
Set這種資料結構,表示一個key可以對應多個value,且value之間是沒有先後順序的,value值也不可以重複。
Hash這種資料結構,表示一個key可以對應多個key-value對,此時這些key-value對之間的先後順序一般意義不大,這是一個按照名稱語義來訪問的資料結構,而非位置語義。
Sorted Set這種資料結構,表示一個key可以對應多個value,value之間是有大小排序的,value值不可以重複。每個value都和一個浮點數相關聯,該浮點數叫score。元素排序規則是:先按score排序,再按value排序。
相信現在你對這5種資料結構有了更清晰的認識,那它們的對應命令對你來說就是小case了。
叢集帶來的好處是顯而易見的,比如容量增加、處理能力增強,還可以按需要進行動態的擴容、縮容。但同時也會引入一些新的問題,至少會有下面這兩個。
一是資料分配:存資料時應該放到哪個節點上,取資料時應該去哪個節點上找。二是資料移動:叢集擴容,新增加節點時,該節點上的資料從何處來;叢集縮容,要剔除節點時,該節點上的資料往何處去。
上面這兩個問題有一個共同點就是,如何去描述和儲存資料與節點的對映關係。又因為資料的位置是由key決定的,所以問題就演變為如何建立起各個key和叢集所有節點的關聯關係。\
叢集的節點是相對固定和少數的,雖然有增加節點和剔除節點。但叢集裡儲存的key,則是完全隨機、沒有規律、不可預測、數量龐多,還非常瑣碎。
這就好比一所大學和它的所有學生之間的關係。如果大學和學生直接掛鉤的話,一定會比較混亂。現實是它們之間又加入了好幾層,首先有院系,其次有專業,再者有年級,最後還有班級。經過這四層對映之後,關係就清爽很多了。
這其實是一個非常重要的結論,這個世界上沒有什麼問題是不能通過加入一層來解決的。如果有,那就再加入一層。計算機裡也是這樣的。
redis在資料和節點之間又加入了一層,把這層稱為槽(slot),因該槽主要和雜湊有關,又叫雜湊槽。
最後變成了,節點上放的是槽,槽裡放的是資料。槽解決的是粒度問題,相當於把粒度變大了,這樣便於資料移動。雜湊解決的是對映問題,使用key的雜湊值來計算所在的槽,便於資料分配。
可以這樣來理解,你的學習桌子上堆滿了書,亂的很,想找到某本書非常困難。於是你買了幾個大的收納箱,把這些書按照書名的長度放入不同的收納箱,然後把這些收納箱放到桌子上。
這樣就變成了,桌子上是收納箱,收納箱裡是書籍。這樣書籍移動很方便,搬起一個箱子就走了。尋找書籍也很方便,只要數一數書名的長度,去對應的箱子裡找就行了。
其實我們也沒做什麼,只是買了幾個箱子,按照某種規則把書裝入箱子。就這麼簡單的舉動,就徹底改變了原來一盤散沙的狀況。是不是有點小小的神奇呢。
一個叢集只能有16384個槽,編號0-16383。這些槽會分配給叢集中的所有主節點,分配策略沒有要求。可以指定哪些編號的槽分配給哪個主節點。叢集會記錄節點和槽的對應關係。
接下來就需要對key求雜湊值,然後對16384取餘,餘數是幾key就落入對應的槽裡。slot = CRC16(key) % 16384。
以槽為單位移動資料,因為槽的數目是固定的,處理起來比較容易,這樣資料移動問題就解決了。
使用雜湊函式計算出key的雜湊值,這樣就可以算出它對應的槽,然後利用叢集儲存的槽和節點的對映關係查詢出槽所在的節點,於是資料和節點就對映起來了,這樣資料分配問題就解決了。
我想說的是,一般的人只會去學習各種技術,高手更在乎如何跳出技術,尋求一種解決方案或思路方向,順著這個方向走下去,八九不離十能找到你想要的答案。
客戶端只要和叢集中的一個節點建立連結後,就可以獲取到整個叢集的所有節點資訊。此外還會獲取所有雜湊槽和節點的對應關係資訊,這些資訊資料都會在客戶端快取起來,因為這些資訊相當有用。
客戶端可以向任何節點傳送請求,那麼拿到一個key後到底該向哪個節點發請求呢?其實就是把叢集裡的那套key和節點的對映關係理論搬到客戶端來就行了。
所以客戶端需要實現一個和叢集端一樣的雜湊函式,先計算出key的雜湊值,然後再對16384取餘,這樣就找到了該key對應的雜湊槽,利用客戶端快取的槽和節點的對應關係資訊,就可以找到該key對應的節點了。
接下來傳送請求就可以了。還可以把key和節點的對映關係快取起來,下次再請求該key時,直接就拿到了它對應的節點,不用再計算一遍了。
理論和現實總是有差距的,叢集已經發生了變化,客戶端的快取還沒來得及更新。肯定會出現拿到一個key向對應的節點發請求,其實這個key已經不在那個節點上了。此時這個節點應該怎麼辦?
這個節點可以去key實際所在的節點上拿到資料再返回給客戶端,也可以直接告訴客戶端key已經不在我這裡了,同時附上key現在所在的節點資訊,讓客戶端再去請求一次,類似於HTTP的302重定向。
這其實是個選擇問題,也是個哲學問題。結果就是redis叢集選擇了後者。因此,節點只處理自己擁有的key,對於不擁有的key將返回重定向錯誤,即-MOVED key 127.0.0.1:6381,客戶端重新向這個新節點傳送請求。
所以說選擇是一種哲學,也是個智慧。稍後再談這個問題。先來看看另一個情況,和這個問題有些相同點。
redis有一種命令可以一次帶多個key,如MGET,我把這些稱為多key命令。這個多key命令的請求被髮送到一個節點上,這裡有一個潛在的問題,不知道大家有沒有想到,就是這個命令裡的多個key一定都位於那同一個節點上嗎?
就分為兩種情況了,如果多個key不在同一個節點上,此時節點只能返回重定向錯誤了,但是多個key完全可能位於多個不同的節點上,此時返回的重定向錯誤就會非常亂,所以redis叢集選擇不支援此種情況。
如果多個key位於同一個節點上呢,理論上是沒有問題的,redis叢集是否支援就和redis的版本有關係了,具體使用時自己測試一下就行了。
在這個過程中我們發現了一件頗有意義的事情,就是讓一組相關的key對映到同一個節點上是非常有必要的,這樣可以提高效率,通過多key命令一次獲取多個值。
那麼問題來了,如何給這些key起名字才能讓他們落到同一個節點上,難不成都要先計算個雜湊值,再取個餘數,太麻煩了吧。當然不是這樣了,redis已經幫我們想好了。
可以來簡單推理下,要想讓兩個key位於同一個節點上,它們的雜湊值必須要一樣。要想雜湊值一樣,傳入雜湊函式的字串必須一樣。那我們只能傳進去兩個一模一樣的字串了,那不就變成同一個key了,後面的會覆蓋前面的資料。
這裡的問題是我們都是拿整個key去計算雜湊值,這就導致key和參與計算雜湊值的字串耦合了,需要將它們解耦才行,就是key和參與計算雜湊值的字串有關但是又不一樣。
redis基於這個原理為我們提供了方案,叫做key雜湊標籤。先看例子,{user1000}.following,{user1000}.followers,相信你已經看出了門道,就是僅使用Key中的位於{和}間的字串參與計算雜湊值。
這樣可以保證雜湊值相同,落到相同的節點上。但是key又是不同的,不會互相覆蓋。使用雜湊標籤把一組相關的key關聯了起來,問題就這樣被輕鬆愉快地解決了。
相信你已經發現了,要解決問題靠的是巧妙的奇思妙想,而不是非要用牛逼的技術牛逼的演算法。這就是小強,小而強大。
最後再來談選擇的哲學。redis的核心就是以最快的速度進行常用資料結構的key/value存取,以及圍繞這些資料結構的運算。對於與核心無關的或會拖累核心的都選擇弱化處理或不處理,這樣做是為了保證核心的簡單、快速和穩定。
其實就是在廣度和深度面前,redis選擇了深度。所以節點不去處理自己不擁有的key,叢集不去支援多key命令。這樣一方面可以快速地響應客戶端,另一方面可以避免在叢集內部有大量的資料傳輸與合併。
redis叢集的每個節點裡只有一個執行緒負責接受和執行所有客戶端傳送的請求。技術上使用多路複用I/O,使用Linux的epoll函式,這樣一個執行緒就可以管理很多socket連線。
除此之外,選擇單執行緒還有以下這些原因:
1、redis都是對記憶體的操作,速度極快(10W+QPS)
2、整體的時間主要都是消耗在了網路的傳輸上
3、如果使用了多執行緒,則需要多執行緒同步,這樣實現起來會變的複雜
4、執行緒的加鎖時間甚至都超過了對記憶體操作的時間
5、多執行緒上下文頻繁的切換需要消耗更多的CPU時間
6、還有就是單執行緒天然支援原子操作,而且單執行緒的程式碼寫起來更簡單
事務大家都知道,就是把多個操作捆綁在一起,要麼都執行(成功了),要麼一個也不執行(回滾了)。redis也是支援事務的,但可能和你想要的不太一樣,一起來看看吧。
redis的事務可以分為兩步,定義事務和執行事務。使用multi命令開啟一個事務,然後把要執行的所有命令都依次排上去。這就定義好了一個事務。此時使用exec命令來執行這個事務,或使用discard命令來放棄這個事務。
你可能希望在你的事務開始前,你關心的key不想被別人操作,那麼可以使用watch命令來監視這些key,如果開始執行前這些key被其它命令操作了則會取消事務的。也可以使用unwatch命令來取消對這些key的監視。
redis事務具有以下特點:
1、如果開始執行事務前出錯,則所有命令都不執行
2、一旦開始,則保證所有命令一次性按順序執行完而不被打斷
3、如果執行過程中遇到錯誤,會繼續執行下去,不會停止的
4、對於執行過程中遇到錯誤,是不會進行回滾的
看完這些,真想問一句話,你這能叫事務嗎?很顯然,這並不是我們通常認為的事務,因為它連原子性都保證不了。保證不了原子性是因為redis不支援回滾,不過它也給出了不支援的理由。
不支援回滾的理由:
1、redis認為,失敗都是由命令使用不當造成
2、redis這樣做,是為了保持內部實現簡單快速
3、redis還認為,回滾並不能解決所有問題
哈哈,這就是霸王條款,因此,好像使用redis事務的不太多
客戶端和叢集的互動過程是序列化阻塞式的,即客戶端傳送了一個命令後必須等到響應回來後才能發第二個命令,這一來一回就是一個往返時間。如果你有很多的命令,都這樣一個一個的來進行,會變得很慢。
redis提供了一種管道技術,可以讓客戶端一次傳送多個命令,期間不需要等待伺服器端的響應,等所有的命令都發完了,再依次接收這些命令的全部響應。這就極大地節省了許多時間,提升了效率。
聰明的你是不是意識到了另外一個問題,多個命令就是多個key啊,這不就是上面提到的多key操作嘛,那麼問題來了,你如何保證這多個key都是同一個節點上的啊,哈哈,redis叢集又放棄了對管道的支援。
不過可以在客戶端模擬實現,就是使用多個連線往多個節點同時傳送命令,然後等待所有的節點都返回了響應,再把它們按照傳送命令的順序整理好,返回給使用者程式碼。哎呀,好麻煩呀。
簡單瞭解下redis的協議,知道redis的資料傳輸格式。
傳送請求的協議:
- 引數個數CRLF$引數1的位元組數CRLF引數1的資料CRLF...$引數N的位元組數CRLF引數N的資料CRLF
例如,SET name lixinjie,實際傳送的資料是:
*3\r\n$3\r\nSET\r\n$4\r\nname\r\n$8\r\nlixinjie\r\n\
接受響應的協議:
單行回覆,第一個位元組是+
錯誤訊息,第一個位元組是-
整型數字,第一個位元組是:
批量回復,第一個位元組是$
多個批量回復,第一個位元組是*
例如,
+OK\r\n
-ERR Operation against\r\n
1000\r\n
$6\r\nfoobar\r\n
*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n
可見redis的協議設計的非常簡單。
PS:文章轉載 程式設計新說 作者:李新傑