吃透這些Redis知識點,面試官一定覺得你很NB(乾貨

是數據結構而非類型


很多文章都會說,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、還有就是單線程天然支持原子操作,而且單線程的代碼寫起來更簡單

事務

如果想學習Java工程化、高性能及分佈式、深入淺出。微服務、Spring,MyBatis,Netty源碼分析的朋友可以加我的Java高級交流:787707172,群裡有阿里大牛直播講解技術,以及Java大型互聯網技術的視頻免費分享給大家。

事務大家都知道,就是把多個操作捆綁在一起,要麼都執行(成功了),要麼一個也不執行(回滾了)。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的協議設計的非常簡單。

關注並私信我“架構”,免費送一套Java架構資料,先到先得!


分享到:


相關文章: