【原創】分布式之redis復習精講


復習要點?

本文圍繞以下幾點進行闡述
1、為什么使用redis
2、使用redis有什么缺點
3、單線程的redis為什么這么快
4、redis的數據類型,以及每種數據類型的使用場景
5、redis的過期策略以及內存淘汰機制
6、redis和數據庫雙寫一致性問題
7、如何應對緩存穿透和緩存雪崩問題
8、如何解決redis的並發競爭問題

正文

1、為什么使用redis

分析:博主覺得在項目中使用redis,主要是從兩個角度去考慮:性能並發。當然,redis還具備可以做分布式鎖等其他功能,但是如果只是為了分布式鎖這些其他功能,完全還有其他中間件(如zookpeer等)代替,並不是非要使用redis。因此,這個問題主要從性能和並發兩個角度去答。
回答:如下所示,分為兩點
(一)性能
如下圖所示,我們在碰到需要執行耗時特別久,且結果不頻繁變動的SQL,就特別適合將運行結果放入緩存。這樣,后面的請求就去緩存中讀取,使得請求能夠迅速響應
image
題外話忽然想聊一下這個迅速響應的標准。其實根據交互效果的不同,這個響應時間沒有固定標准。不過曾經有人這么告訴我:"在理想狀態下,我們的頁面跳轉需要在瞬間解決,對於頁內操作則需要在剎那間解決。另外,超過一彈指的耗時操作要有進度提示,並且可以隨時中止或取消,這樣才能給用戶最好的體驗。"
那么瞬間、剎那、一彈指具體是多少時間呢?
根據《摩訶僧祗律》記載

一剎那者為一念,二十念為一瞬,二十瞬為一彈指,二十彈指為一羅預,二十羅預為一須臾,一日一夜有三十須臾。

那么,經過周密的計算,一瞬間為0.36 秒,一剎那有 0.018 秒.一彈指長達 7.2 秒。
(二)並發
如下圖所示,在大並發的情況下,所有的請求直接訪問數據庫,數據庫會出現連接異常。這個時候,就需要使用redis做一個緩沖操作,讓請求先訪問到redis,而不是直接訪問數據庫。
image

2、使用redis有什么缺點

分析:大家用redis這么久,這個問題是必須要了解的,基本上使用redis都會碰到一些問題,常見的也就幾個。
回答:主要是四個問題
(一)緩存和數據庫雙寫一致性問題
(二)緩存雪崩問題
(三)緩存擊穿問題
(四)緩存的並發競爭問題
這四個問題,我個人是覺得在項目中,比較常遇見的,具體解決方案,后文給出。

3、單線程的redis為什么這么快

redis是單線程工作模型,redis集群的每個節點里只有一個線程負責接受和執行所有客戶端發送的請求。技術上使用多路復用I/O,使用Linux的epoll函數,這樣一個線程就可以管理很多socket連接。


除此之外,選擇單線程還有以下這些原因:

1、redis都是對內存的操作,速度極快(10W+QPS)

2、整體的時間主要都是消耗在了網絡的傳輸上

3、如果使用了多線程,則需要多線程同步,這樣實現起來會變的復雜

4、線程的加鎖時間甚至都超過了對內存操作的時間

5、多線程上下文頻繁的切換需要消耗更多的CPU時間

6、還有就是單線程天然支持原子操作,而且單線程的代碼寫起來更簡單

題外話我們現在要仔細的說一說I/O多路復用機制,因為這個說法實在是太通俗了,通俗到一般人都不懂是什么意思。博主打一個比方:小曲在S城開了一家快遞店,負責同城快送服務。小曲因為資金限制,雇佣了一批快遞員,然后小曲發現資金不夠了,只夠買一輛車送快遞。
經營方式一
客戶每送來一份快遞,小曲就讓一個快遞員盯着,然后快遞員開車去送快遞。慢慢的小曲就發現了這種經營方式存在下述問題

  • 幾十個快遞員基本上時間都花在了搶車上了,大部分快遞員都處在閑置狀態,誰搶到了車,誰就能去送快遞
  • 隨着快遞的增多,快遞員也越來越多,小曲發現快遞店里越來越擠,沒辦法雇佣新的快遞員了
  • 快遞員之間的協調很花時間

綜合上述缺點,小曲痛定思痛,提出了下面的經營方式
經營方式二
小曲只雇佣一個快遞員。然后呢,客戶送來的快遞,小曲按送達地點標注好,然后依次放在一個地方。最后,那個快遞員依次的去取快遞,一次拿一個,然后開着車去送快遞,送好了就回來拿下一個快遞。

對比
上述兩種經營方式對比,是不是明顯覺得第二種,效率更高,更好呢。在上述比喻中:

  • 每個快遞員------------------>每個線程
  • 每個快遞-------------------->每個socket(I/O流)
  • 快遞的送達地點-------------->socket的不同狀態
  • 客戶送快遞請求-------------->來自客戶端的請求
  • 小曲的經營方式-------------->服務端運行的代碼
  • 一輛車---------------------->CPU的核數

於是我們有如下結論
1、經營方式一就是傳統的並發模型,每個I/O流(快遞)都有一個新的線程(快遞員)管理。
2、經營方式二就是I/O多路復用。只有單個線程(一個快遞員),通過跟蹤每個I/O流的狀態(每個快遞的送達地點),來管理多個I/O流。

下面類比到真實的redis線程模型,如圖所示
image
參照上圖,簡單來說,就是。我們的redis-client在操作的時候,會產生具有不同事件類型的socket。在服務端,有一段I/0多路復用程序,將其置入隊列之中。然后,文件事件分派器,依次去隊列中取,轉發到不同的事件處理器中。
需要說明的是,這個I/O多路復用機制,redis還提供了select、epoll、evport、kqueue等多路復用函數庫,大家可以自行去了解。

4、redis的數據類型,以及每種數據類型的使用場景

分析:是不是覺得這個問題很基礎,其實我也這么覺得。然而根據面試經驗發現,至少百分八十的人答不上這個問題。建議,在項目中用到后,再類比記憶,體會更深,不要硬記。基本上,一個合格的程序員,五種類型都會用到。
回答:一共五種
(一)String
這個其實沒啥好說的,最常規的set/get操作,value可以是String也可以是數字。一般做一些復雜的計數功能的緩存。
(二)hash
這里value存放的是結構化的對象,比較方便的就是操作其中的某個字段。博主在做單點登錄的時候,就是用這種數據結構存儲用戶信息,以cookieId作為key,設置30分鍾為緩存過期時間,能很好的模擬出類似session的效果。
(三)list
使用List的數據結構,可以做簡單的消息隊列的功能。另外還有一個就是,可以利用lrange命令,做基於redis的分頁功能,性能極佳,用戶體驗好。
(四)set
因為set堆放的是一堆不重復值的集合。所以可以做全局去重的功能。為什么不用JVM自帶的Set進行去重?因為我們的系統一般都是集群部署,使用JVM自帶的Set,比較麻煩,難道為了一個做一個全局去重,再起一個公共服務,太麻煩了。
另外,就是利用交集、並集、差集等操作,可以計算共同喜好,全部的喜好,自己獨有的喜好等功能
(五)sorted set
sorted set多了一個權重參數score,集合中的元素能夠按score進行排列。可以做排行榜應用,取TOP N操作。另外,參照另一篇《分布式之延時任務方案解析》,該文指出了sorted set可以用來做延時任務。最后一個應用就是可以做范圍查找

5、redis的過期策略以及內存淘汰機制

分析:這個問題其實相當重要,到底redis有沒用到家,這個問題就可以看出來。比如你redis只能存5G數據,可是你寫了10G,那會刪5G的數據。怎么刪的,這個問題思考過么?還有,你的數據已經設置了過期時間,但是時間到了,內存占用率還是比較高,有思考過原因么?
回答:
redis采用的是定期刪除+惰性刪除策略。
為什么不用定時刪除策略?
定時刪除,用一個定時器來負責監視key,過期則自動刪除。雖然內存及時釋放,但是十分消耗CPU資源。在大並發請求下,CPU要將時間應用在處理請求,而不是刪除key,因此沒有采用這一策略.
定期刪除+惰性刪除是如何工作的呢?
定期刪除,redis默認每個100ms檢查,是否有過期的key,有過期key則刪除。需要說明的是,redis不是每個100ms將所有的key檢查一次,而是隨機抽取進行檢查(如果每隔100ms,全部key進行檢查,redis豈不是卡死)。因此,如果只采用定期刪除策略,會導致很多key到時間沒有刪除。
於是,惰性刪除派上用場。也就是說在你獲取某個key的時候,redis會檢查一下,這個key如果設置了過期時間那么是否過期了?如果過期了此時就會刪除。
采用定期刪除+惰性刪除就沒其他問題了么?
不是的,如果定期刪除沒刪除key。然后你也沒即時去請求key,也就是說惰性刪除也沒生效。這樣,redis的內存會越來越高。那么就應該采用內存淘汰機制
在redis.conf中有一行配置

# maxmemory-policy volatile-lru

該配置就是配內存淘汰策略的(什么,你沒配過?好好反省一下自己)
1)noeviction:當內存不足以容納新寫入數據時,新寫入操作會報錯。應該沒人用吧。
2)allkeys-lru:當內存不足以容納新寫入數據時,在鍵空間中,移除最近最少使用的key。推薦使用,目前項目在用這種。
3)allkeys-random:當內存不足以容納新寫入數據時,在鍵空間中,隨機移除某個key。應該也沒人用吧,你不刪最少使用Key,去隨機刪。
4)volatile-lru:當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,移除最近最少使用的key。這種情況一般是把redis既當緩存,又做持久化存儲的時候才用。不推薦
5)volatile-random:當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,隨機移除某個key。依然不推薦
6)volatile-ttl:當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,有更早過期時間的key優先移除。不推薦
ps:如果沒有設置 expire 的key, 不滿足先決條件(prerequisites); 那么 volatile-lru, volatile-random 和 volatile-ttl 策略的行為, 和 noeviction(不刪除) 基本上一致。

6、redis和數據庫雙寫一致性問題

分析:一致性問題是分布式常見問題,還可以再分為最終一致性和強一致性。數據庫和緩存雙寫,就必然會存在不一致的問題。答這個問題,先明白一個前提。就是如果對數據有強一致性要求,不能放緩存。我們所做的一切,只能保證最終一致性。另外,我們所做的方案其實從根本上來說,只能說降低不一致發生的概率,無法完全避免。因此,有強一致性要求的數據,不能放緩存。
回答:《分布式之數據庫和緩存雙寫一致性方案解析》給出了詳細的分析,在這里簡單的說一說。首先,采取正確更新策略,先更新數據庫,再刪緩存。其次,因為可能存在刪除緩存失敗的問題,提供一個補償措施即可,例如利用消息隊列。

7、如何應對緩存穿透和緩存雪崩問題

分析:這兩個問題,說句實在話,一般中小型傳統軟件企業,很難碰到這個問題。如果有大並發的項目,流量有幾百萬左右。這兩個問題一定要深刻考慮。
回答:如下所示
緩存穿透,即黑客故意去請求緩存中不存在的數據,導致所有的請求都懟到數據庫上,從而數據庫連接異常。
解決方案:
(一)利用互斥鎖,緩存失效的時候,先去獲得鎖,得到鎖了,再去請求數據庫。沒得到鎖,則休眠一段時間重試
(二)采用異步更新策略,無論key是否取到值,都直接返回。value值中維護一個緩存失效時間,緩存如果過期,異步起一個線程去讀數據庫,更新緩存。需要做緩存預熱(項目啟動前,先加載緩存)操作。
(三)提供一個能迅速判斷請求是否有效的攔截機制,比如,利用布隆過濾器,內部維護一系列合法有效的key。迅速判斷出,請求所攜帶的Key是否合法有效。如果不合法,則直接返回。
緩存雪崩,即緩存同一時間大面積的失效,這個時候又來了一波請求,結果請求都懟到數據庫上,從而導致數據庫連接異常。
解決方案:
(一)給緩存的失效時間,加上一個隨機值,避免集體失效。
(二)使用互斥鎖,但是該方案吞吐量明顯下降了。
(三)雙緩存。我們有兩個緩存,緩存A和緩存B。緩存A的失效時間為20分鍾,緩存B不設失效時間。自己做緩存預熱操作。然后細分以下幾個小點

  • I 從緩存A讀數據庫,有則直接返回
  • II A沒有數據,直接從B讀數據,直接返回,並且異步啟動一個更新線程。
  • III 更新線程同時更新緩存A和緩存B。

8、如何解決redis的並發競爭key問題

分析:這個問題大致就是,同時有多個子系統去set一個key。這個時候要注意什么呢?大家思考過么。需要說明一下,博主提前百度了一下,發現答案基本都是推薦用redis事務機制。博主不推薦使用redis的事務機制。因為我們的生產環境,基本都是redis集群環境,做了數據分片操作。你一個事務中有涉及到多個key操作的時候,這多個key不一定都存儲在同一個redis-server上。因此,redis的事務機制,十分雞肋。
回答:如下所示
(1)如果對這個key操作,不要求順序
這種情況下,准備一個分布式鎖,大家去搶鎖,搶到鎖就做set操作即可,比較簡單。
(2)如果對這個key操作,要求順序
假設有一個key1,系統A需要將key1設置為valueA,系統B需要將key1設置為valueB,系統C需要將key1設置為valueC.
期望按照key1的value值按照 valueA-->valueB-->valueC的順序變化。這種時候我們在數據寫入數據庫的時候,需要保存一個時間戳。假設時間戳如下

系統A key 1 {valueA 3:00} 系統B key 1 {valueB 3:05} 系統C key 1 {valueC 3:10}

那么,假設這會系統B先搶到鎖,將key1設置為{valueB 3:05}。接下來系統A搶到鎖,發現自己的valueA的時間戳早於緩存中的時間戳,那就不做set操作了。以此類推。

其他方法,比如利用隊列,將set方法變成串行訪問也可以。總之,靈活變通。

 

數據結構而非類型


很多文章都會說,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了。

 

Redis 事務,以及事務失敗的處理

事務命令

  1. Redis事務,會將所有命令放入一個命令隊列中,再由隊列中執行命令。
  2. 事務的命令分為:
  • MULTI 開始事務---->各種命令操作,SET,GET,SADD等等(每一步操作后,會顯示QUEUED,表示已經入隊命令隊列)---->EXEC執行命令隊列
  • DISCARD:取消一個事務,清空事務命令隊列,客戶端調整為非事務狀態
  • WATCH:在進入事務之前執行,監聽任意數量的key,當調用EXEC時,如果這個key被其他用戶修改了,那么事務不在執行,返回失敗。
  • 以下示例展示了一個執行失敗的事務例子:

redis> WATCH name

OK

redis> MULTI

OK

redis> SET name peter

QUEUED

redis> EXEC (nil)

以下執行序列展示了上面的例子是如何失敗的:

時間 客戶端 A 客戶端 B

T1 WATCH name

T2 MULTI

T3SET name peter

T4 SET name john

T5EXEC

在時間 T4 ,客戶端 B 修改了 name 鍵的值, 當客戶端 A 在 T5 執行 EXEC 時,Redis 會發現 name 這個被監視的鍵已經被修改, 因此客戶端 A 的事務不會被執行,而是直接返回失敗。


 

ACID

Redis 的事務保證了 ACID 中的一致性(C)和隔離性(I),但並不保證原子性(A)和持久性(D)。

1.原子性(一個事務所有操作要么全部成功,要么全部失敗)

redis的單個命令是原子性的,但是事務中的的redis命令,就算后面的命令失敗/終止了(比如KILL進程,主機宕機等),此時事務失敗,前面執行的命令也不會回滾。

2.一致性

(1)不正確入隊命令的事務不會被執行,不會影響數據庫的一致性

(2)命令執行中的錯誤,將錯誤包含在事務結果中,不會中斷事務,不影響已執行結果,也不影響后面命令的執行,對事務的一致性也沒有影響。

(3)進程終結

  • 內存模式:如果 Redis 沒有采取任何持久化機制,那么重啟之后的數據庫總是空白的,所以數據總是一致的。
  • RDB 模式:在執行事務時,Redis 不會中斷事務去執行保存 RDB 的工作,只有在事務執行之后,保存 RDB 的工作才有可能開始。所以當 RDB 模式下的 Redis 服務器進程在事務中途被殺死時,事務內執行的命令,不管成功了多少,都不會被保存到 RDB 文件里。恢復數據庫需要使用現有的 RDB 文件,而這個 RDB 文件的數據保存的是最近一次的數據庫快照(snapshot),所以它的數據可能不是最新的,但只要 RDB 文件本身沒有因為其他問題而出錯,那么還原后的數據庫就是一致的。
  • AOF 模式:因為保存 AOF 文件的工作在后台線程進行,所以即使是在事務執行的中途,保存 AOF 文件的工作也可以繼續進行,因此,根據事務語句是否被寫入並保存到 AOF 文件,有以下兩種情況發生:
    1)如果事務語句未寫入到 AOF 文件,或 AOF 未被 SYNC 調用保存到磁盤,那么當進程被殺死之后,Redis 可以根據最近一次成功保存到磁盤的 AOF 文件來還原數據庫,只要 AOF 文件本身沒有因為其他問題而出錯,那么還原后的數據庫總是一致的,但其中的數據不一定是最新的。
    2)如果事務的部分語句被寫入到 AOF 文件,並且 AOF 文件被成功保存,那么不完整的事務執行信息就會遺留在 AOF 文件里,當重啟 Redis 時,程序會檢測到 AOF 文件並不完整,Redis 會退出,並報告錯誤。需要使用 redis-check-aof 工具將部分成功的事務命令移除之后,才能再次啟動服務器。還原之后的數據總是一致的,而且數據也是最新的(直到事務執行之前為止)。

3.隔離性

Redis 是單進程程序,並且它保證在執行事務時,不會對事務進行中斷,事務可以運行直到執行完所有事務隊列中的命令為止。因此,Redis 的事務是總是帶有隔離性的。

4.持久性

由redis的持久化模式決定:

    • 內存模式-無持久化
    • RDB-服務器可能在事務執行之后、RDB 文件更新之前的這段時間失敗,所以 RDB 模式下的 Redis 事務也是不持久的。
    • AOF-后台每秒fsync策略,不是主線程阻塞執行,如果在保存時宕機,可能有一秒數據丟失,也非持久的。


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

 

集群帶來的問題與解決思路


集群帶來的好處是顯而易見的,比如容量增加、處理能力增強,還可以按需要進行動態的擴容、縮容。但同時也會引入一些新的問題,至少會有下面這兩個。

一是數據分配:存數據時應該放到哪個節點上,取數據時應該去哪個節點上找。二是數據移動:集群擴容,新增加節點時,該節點上的數據從何處來;集群縮容,要剔除節點時,該節點上的數據往何處去。

上面這兩個問題有一個共同點就是,如何去描述和存儲數據與節點的映射關系。又因為數據的位置是由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命令。這樣一方面可以快速地響應客戶端,另一方面可以避免在集群內部有大量的數據傳輸與合並。

https://zhuanlan.zhihu.com/p/78896005

https://zhuanlan.zhihu.com/p/78794114

https://www.cnblogs.com/rjzheng/p/9096228.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM