是資料結構而非型別
很多文章都會說,redis支援5種常用的資料類型,這其實是存在很大的歧義。 redis裡存的都是二進位數據,其實就是字節數組(byte[]),這些字節數據是沒有數據類型的,只有把它們按照合理的格式解碼後,可以變成一個字符串,整數或對象,此時才具有資料型態。
這一點必須記住。所以任何東西只要能轉換成位元組數組(byte[])的,都可以存到redis裡。只要轉換成位元組數組,無論是字串、數字、物件、圖片、聲音、視訊或文件,都可以處理。
因此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),因該槽主要和哈希有關,又叫哈希槽。
***變成了,節點上放的是槽,槽裡放的是資料。槽解決的是粒度問題,相當於把粒度變大了,這樣方便資料移動。哈希技術用於解決映射問題,它利用鍵的哈希值計算其所在的槽,以便於資料的分配。
你的學習桌上書本堆積如山,極為凌亂,要想找到其中一本十分艱難。你買了一些大的收納箱,根據書名長度將書籍分類後放入不同的收納箱,並將它們放置在桌子上。
這樣就變成了,桌上是收納箱,收納箱裡是書。這樣書籍移動很方便,搬起一個箱子就走了。只需測量書名的長度,然後前往相應的箱子,就可以輕鬆找到所需的書籍。
其實我們也沒做什麼,只是買了幾個箱子,按照某種規則把書裝入箱子。就這麼簡單的舉動,就徹底改變了原來一盤散沙的狀況。是不是有點小小的神奇呢。
一個集群只能有16384個槽,編號0-16383。這些槽會分配給叢集中的所有主節點,分配策略沒有要求。可以指定哪些編號的槽分配給哪個主節點。群集會記錄節點和槽的對應關係。
接下來需要對鍵進行雜湊計算,將結果除以16384並取餘,得到的餘數將決定鍵落入哪個槽中。 slot = CRC16(key) % 16384。
以槽為單位移動數據,因為槽的數目是固定的,處理起來比較容易,這樣數據移動問題就解決了。
使用雜湊函數計算出key的雜湊值,這樣就可以算出它對應的槽,然後利用叢集儲存的槽和節點的映射關係查詢出槽所在的節點,於是資料和節點就映射起來了,這樣資料分配問題就解決了。
我想說的是,一般的人只會去學習各種技術,高手更在乎如何跳出技術,尋求一種解決方案或思路方向,順著這個方向走下去,八九不離十能找到你想要的答案。
叢集對指令操作的取捨
客戶端只要和叢集中的一個節點建立連結後,就可以取得到整個叢集的所有節點資訊。此外還會取得所有雜湊槽和節點的對應關係訊息,這些資訊資料都會在客戶端快取起來,因為這些資訊相當有用。
客戶端可以向任何節點發送請求,那麼拿到一個key後到底該向哪個節點發請求呢?其實就是把集群裡的那套key和節點的映射關係理論搬到客戶端來就行了。
所以客戶端需要實作一個和叢集端一樣的雜湊函數,先計算出key的雜湊值,然後再對16384取餘,這樣就找到了該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和參與計算哈希值的字串有關但是又不一樣。
redis基於這個原理為我們提供了方案,叫做key哈希標籤。先看例子,{user1000}.following,{user1000}.followers,相信你已經看出了門道,就是僅使用Key中的位於{和}間的字符串參與計算哈希值。
這樣可以保證哈希值相同,落到相同的節點上。但key又是不同的,不會互相覆蓋。透過使用哈希標籤將一組相關的鍵關聯起來,問題輕鬆愉快地解決。
解決問題所依賴的是巧妙的創意和想法,而非必須採用高超的技術和演算法。這就是小強,小而強大。
最後再來談選擇的哲學。 Redis的主要特點是在最短的時間內實現常用資料結構的鍵值儲存和訪問,以及在這些資料結構上執行相關運算。對於與核心無關的或會拖累核心的都選擇弱化處理或不處理,這樣做是為了確保核心的簡單、快速和穩定。
其實就是在廣度和深度面前,redis選擇了深度。因此,節點不會處理其未擁有的鍵,叢集也不支援多個鍵的命令。這樣一方面可以快速地回應客戶端,另一方面可以避免在叢集內部有大量的資料傳輸與合併。
單執行緒模型
redis叢集的每個節點裡只有一個執行緒負責接受和執行所有客戶端發送的請求。技術上使用多路復用I/O,使用Linux的epoll函數,這樣一個執行緒就可以管理很多socket連線。
除此之外,選擇單執行緒還有以下這些原因:
1、redis都是對記憶體的操作,速度極快(10W QPS)
#2 、整體的時間主要都是消耗在了網路的傳輸上
3、如果使用了多線程,則需要多線程同步,這樣實現起來會變的複雜
4、線程的加鎖時間甚至都超過了對記憶體操作的時間
5、多執行緒上下文頻繁的切換需要消耗更多的CPU時間
6、還有就是單執行緒天然支援原子操作,而且單線程的程式碼寫起來更簡單
事務
事務大家都知道,就是把多個操作捆綁在一起,要么都執行(成功了),要嘛一個也不執行(回滾了)。 redis也是支援事務的,但可能和你想要的不太一樣,一起來看看吧。
redis的事務可以分成兩步,定義事務和執行事務。開啟一個事務後,依照順序新增所有待執行的命令。這就定義好了一個事務。 You can execute the transaction using the exec command at this point, or abandon it with the discard command.。
你可能希望在你的事務開始前,你關心的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的協定設計的非常簡單。
以上是如何進行Redis知識點的分析的詳細內容。更多資訊請關注PHP中文網其他相關文章!