1. 程式人生 > 其它 >掌握 Redis這些 知識點,面試官一定覺得你很 NB

掌握 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、還有就是單執行緒天然支援原子操作,而且單執行緒的程式碼寫起來更簡單

  事務

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

  $3\r

  SET\r

  $4\r

  name\r

  $8\r

  lixinjie\r

  \

  接受響應的協議:

  單行回覆,第一個位元組是 +

  錯誤訊息,第一個位元組是 -

  整型數字,第一個位元組是:

  批量回復,第一個位元組是 $

  多個批量回復,第一個位元組是 *例如,

  +OK\r

  -ERR Operation against\r

  1000\r

  $6\r

  foobar\r

  *2\r

  $3\r

  foo\r

  $3\r

  bar\r

  可見 redis 的協議設計的非常簡單。

  點選這裡領取PHP程式設計師1-7年進階資料

  以上內容希望幫助到大家,很多PHPer在進階的時候總會遇到一些問題和瓶頸,業務程式碼寫多了沒有方向感,不知道該從那裡入手去提升,對此我整理了一些資料,包括但不限於:分佈