深入淺出帶你走進Redis!

語言: CN / TW / HK

導語 |  本文推選自騰訊雲開發者社群-【技思廣益 · 騰訊技術人原創集】專欄。該專欄是騰訊雲開發者社群為騰訊技術人與廣泛開發者打造的分享交流視窗。欄目邀約騰訊技術人分享原創的技術積澱,與廣泛開發者互啟迪共成長。 本文作者是 騰訊後臺開發工程師劉波。

本文主要講述Redis的基礎知識和常識性內容,幫助大家瞭解和熟悉Redis;後續通過閱讀原始碼、實踐Redis後會總結相關的知識點,再繼續分享給大家。

什麼是Redis

Redis是一個開源、基於記憶體、使用C語言編寫的key-value資料庫,並提供了多種語言的API。它的資料結構十分豐富,基礎資料型別包括:string(字串)、list(列表,雙向連結串列)、hash(雜湊,鍵值對集合)、set(集合,不重複)和sorted set(有序集合)。主要可以用於資料庫、快取、分散式鎖、訊息佇列等...

以上的資料型別是Redis鍵值的資料型別,其實就是資料的儲存形式,但是資料型別的底層實現是最重要的,底層的資料結構主要分為6種,分別是 簡單動態字串、雙向連結串列、壓縮連結串列、雜湊表、跳錶和整數陣列 。各個資料型別和底層結構的對應關係如下:

各個底層實現的時間複雜度如下:

可以看出除了string型別的底層實現只有一種資料結構,其他四種均有兩種底層實現,這四種類型為集合型別,其中一個鍵對應了一個集合的資料。

(一)Redis鍵值是如何儲存的呢?

Redis為了快速訪問鍵值對,採用了 雜湊表 來儲存所有的鍵值對,一個雜湊表對應了多個 雜湊桶 ,所謂的雜湊桶是指雜湊表陣列中的每一個元素,當然雜湊表中儲存的不是值本身,是指向值的指標,如下圖。

其中雜湊桶中的entry元素中儲存了*key和*value指標,分別指向了實際的鍵和值。通過Redis可以在O(1)的時間內找到鍵值對,只需要計算key的雜湊值就可以定位位置,但從下圖可以看出,在4號位置出現了衝突,兩個key對映到了同一個位置,這就產生了雜湊衝突,會導致雜湊表的操作變慢。雖然Redis通過鏈式衝突解決該問題,但如果資料持續增多,產生的雜湊衝突也會越來越多,會加重Redis的查詢時間。

Redis儲存資料示意圖

為了解決上述的雜湊衝突問題,Redis會對雜湊表進行 rehash 操作,也就是增加目前的雜湊桶數量,使得key更加分散,進而減少雜湊衝突的問題,主要流程如下:

  • 採用兩個hash表進行操作,當雜湊表A需要進行擴容時,給雜湊表B分配兩倍的空間。

  • 將雜湊表A的資料重新對映並拷貝給雜湊表B。

  • 釋放A的空間。

上述的步驟可能會存在一個問題,當雜湊表A向B複製的時候,是需要一定的時間的,可能會造成Redis的執行緒阻塞,就無法服務其他的請求了。

針對上述問題,Redis採用了 漸進式rehash ,主要的流程是:Redis還是繼續處理客戶端的請求,每次處理一個請求的時候,就會將該位置所有的entry都拷貝到雜湊表B中,當然也會存在某個位置一直沒有被請求。Redis也考慮了這個問題,通過設定一個定時任務進行rehash,在一些鍵值對一直沒有操作的時候,會週期性的搬移一些資料到雜湊表B中,進而縮短rehash的過程。

(二)Redis為什麼採用單執行緒呢?

首先要明確的是Redis單執行緒指的是 網路IO 鍵值 對讀寫 是由一個執行緒來完成的,但Redis持久化、叢集資料等是由額外的執行緒執行的。瞭解Redis使用單執行緒之前可以先了解一下多執行緒的開銷。

通常情況下,使用多執行緒可以增加系統吞吐率或者可以增加系統擴充套件性,但多執行緒通常會存在同時訪問某些共享資源,為了保證訪問共享資源的正確性,就需要有額外的機制進行保證,這個機制首先會帶來一定的開銷。其實對於多執行緒併發訪問的控制一直是一個難點問題,如果沒有精細的設計,比如說,只是簡單地採用一個粗粒度互斥鎖,就會出現不理想的結果。即使增加了執行緒,大部分執行緒也在等待獲取訪問共享資源的互斥鎖,並行變序列,系統吞吐率並沒有隨著執行緒的增加而增加。

這也是Redis使用單執行緒的主要原因。

值得注意的是在Redis6.0中引入了多執行緒 。在Redis6.0之前,從網路IO處理到實際的讀寫命令處理都是由單個執行緒完成的,但隨著網路硬體的效能提升,Redis的效能瓶頸有可能會出現在網路IO的處理上,也就是說 單個主執行緒處理網路請求的速度跟不上底層網路硬體的速度 。針對此問題,Redis採用多個IO執行緒來處理網路請求,提高網路請求處理的並行度,但多IO執行緒只用於處理網路請求, 對於讀寫命令,Redis仍然使用單執行緒處理

(三)Redis單執行緒為什麼還這麼快?

IO多路複用機制:使其在網路IO操作中能併發處理大量的客戶端請求從而實現高吞吐率

IO多路複用機制是指一個執行緒處理多個IO流,也就是常說的select/epoll機制。在Redis執行單執行緒的情況下,該機制允許核心中同時存在多個監聽套接字和已連線套接字。核心會一直監聽這些套接字上的連線請求或資料請求。一旦有請求到達,就會交給Redis執行緒處理,這就實現了一個Redis執行緒處理多個IO流的效果,進而提升併發性。

Redis是基於記憶體的,絕大部分請求都是記憶體操作,十分的迅速

Redis具有高效的底層資料結構,為優化記憶體,對每種型別基本都有兩種底層實現方式

主要執行過程是單執行緒,避免了不必要的上下文切換和資源競爭,不存在多執行緒導致的CPU切換和鎖的問題。

Redis資料丟失問題

由上一小節我們大概瞭解了 Redis的儲存和快的主要原因,通常情況下我們會把Redis當作快取使用,將後端資料庫中的資料儲存在記憶體中,然後從記憶體中直接讀取資料,響應速度會非常快。但是如果伺服器宕機了,記憶體中的資料也就會丟失,當然我們可以重新從後端資料庫中恢復這些快取資料,但是頻繁訪問資料庫,會給資料庫帶來一定的壓力;另一方面資料是從慢速的資料庫中讀取的,效能肯定比不上Redis,也會導致這些資料的應用程式響應變慢。

所以對Redis來說,實現資料的持久化,避免從後端恢復資料是至關重要的,目前Redis持久化主要有兩大機制,分別是 AOF(Append Only File)日誌和RDB快照

(一)AOF日誌

AOF日誌是寫後日志,也就是Redis先執行命令,然後將資料寫入記憶體,最後才記錄日誌,如下圖:

Redis AOF操作過程

AOF日誌中記錄的是Redis收到的每一條命令,這些命令都是以文字的形式儲存的,例如我們以Redis收到set key value命令後記錄的日誌為例,AOF檔案中儲存的資料如下圖所示,其中*3代表當前命令分為三部分,每部分都是通過$+數字開頭,其中數字表示該部分的命令、鍵、值一共有多少位元組。

Redis AOF日誌內容

AOF為了避免額外的檢查開銷,並不會檢查命令的正確性,如果先記錄日誌再執行命令,就有可能記錄錯誤的命令,再通過AOF日誌恢復資料的時候,就有可能出錯,而且在執行完命令後記錄日誌也不會阻塞當前的寫操作。但是AOF是存在一定的風險的,首先是如果剛執行一個命令,但是AOF檔案中還沒來得及儲存就宕機了,那麼這個命令和資料就會有丟失的風險,另外AOF雖然可以避免對當前命令的阻塞(因為是先寫入再記錄日誌),但有可能會對下一次操作帶來阻塞風險(可能存在寫入磁碟較慢的情況)。這兩種情況都在於AOF什麼時候寫入磁碟,對於這個問題AOF機制提供了三種選擇(appendfsync的三個可選值),分別是 Always、Everysec、No 具體如下:

我們可以根據不同的場景來選擇不同的方式:

  • Always可靠性較高,資料基本不丟失,但是對效能的影響較大。

  • Everysec效能適中,即使宕機也只會丟失1秒的資料。

  • No效能好,但是如果宕機丟失的資料較多。

雖然有一定的寫回策略,但畢竟AOF是通過檔案的形式記錄所有的寫命令,但如果指令越來越多的時候,AOF檔案就會越來越大,可能會超出檔案大小的限制;另外,如果檔案過大再次寫入指令的話效率也會變低;如果發生宕機,需要把AOF所有的命令重新執行,以用於故障恢復,資料過大的話這個恢復過程越漫長,也會影響Redis的使用。

此時, AOF重寫機制 就來了:

AOF重寫就是根據所有的鍵值對建立一個新的AOF檔案,可以減少大量的檔案空間,減少的原因是:AOF對於命令的新增是追加的方式,逐一記錄命令,但有可能存在某個鍵值被反覆更改,產生了一些冗餘資料,這樣在重寫的時候 就可以過濾掉這些指令,從而更新當前的最新狀態。

AOF重寫的過程是通過主執行緒fork後臺的bgrewriteaof子程序來實現的,可以避免阻塞主程序導致效能下降,整個過程如下:

  • AOF每次重寫,fork過程會把主執行緒的記憶體拷貝一份bgrewriteaof子程序,裡面包含了資料庫的資料,拷貝的是父程序的頁表,可以在不影響主程序的情況下逐一把拷貝的資料記入重寫日誌;

  • 因為主執行緒沒有阻塞,仍然可以處理新來的操作,如果這時候存在寫操作,會先把操作先放入緩衝區,對於正在使用的日誌,如果宕機了這個日誌也是齊全的,可以用於恢復;對於正在更新的日誌,也不會丟失新的操作,等到資料拷貝完成,就可以將緩衝區的資料寫入到新的檔案中,保證資料庫的最新狀態。

(二)RDB快照

上一小節裡瞭解了避免Redis資料丟失的AOF方法,這個方法記錄的是操作命令,而不是實際的資料,如果日誌非常多的話,Redis恢復的就很緩慢,會影響到正常的使用。

這一小節主要是講述的另一種Redis資料持久化的方式: 記憶體快照 。即記錄記憶體中的資料在某一時刻的狀態,並以檔案的形式寫到磁碟上,即使伺服器宕機,快照檔案也不會丟失,資料的可靠性也就得到了保證,這個檔案稱為RDB(Redis DataBase)檔案。可以看出RDB記錄的是某一時刻的資料,和AOF不同,所以在資料恢復的時候只需要將RDB檔案讀入到記憶體,就可以完成資料恢復。但為了RDB資料恢復的可靠性,在進行快照的時候是全量快照,會將記憶體中所有的資料都記錄到磁碟中,這就有可能會阻塞主執行緒的執行。Redis提供了兩個命令來生成RDB檔案,分別是 save bgsave

  • save:在主執行緒中執行,會導致阻塞;

  • bgsave:會建立一個子程序,該程序專門用於寫入RDB檔案,可以避免主執行緒的阻塞,也是預設的方式。

我們可以採用bgsave的命令來執行全量快照,提供了資料的可靠性保證,也避免了對Redis的效能影響。執行快照期間資料能不能修改呢?如果不能修改,快照過程中如果有新的寫操作,資料就會不一致,這肯定是不符合預期的。Redis借用了作業系統的 寫時複製 ,在執行快照的期間,正常處理寫操作。

主要流程為:

  • bgsave子程序是由主執行緒fork出來的,可以共享主執行緒的所有記憶體資料。

  • bgsave子程序執行後,開始讀取主執行緒的記憶體資料,並把它們寫入RDB檔案中。

  • 如果主執行緒對這些資料都是讀操作,例如A,那麼主執行緒和bgsave子程序互不影響。

  • 如果主執行緒需要修改一塊資料,如C,這塊資料會被複制一份,生成資料的副本,然主執行緒在這個副本上進行修改;bgsave子程序可以把原來的資料C寫入RDB檔案。

寫時複製機制保證快照期間資料可修改

通過上述方法就可以保證快照的完整性,也可以允許主執行緒處理寫操作,可以避免對業務的影響。 那多久進行一次快照呢

理論上來說快照時間間隔越短越好,可以減少資料的丟失,畢竟fork的子程序不會阻塞主執行緒,但是頻繁的將資料寫入磁碟,會給磁碟帶來很多壓力,也可能會存在多個快照競爭磁碟頻寬(當前快照沒結束,下一個就開始了)。另一方面,雖然fork出的子程序不會阻塞,但fork這個建立過程是會阻塞主執行緒的,當主執行緒需要的記憶體越大,阻塞時間越長。

針對上面的問題,Redis採用了 增量快照 ,在做一次全量快照後,後續的快照只對修改的資料進行記錄,需要記住哪些資料被修改了,可以避免全量快照帶來的開銷。

(三)混合使用AOF日誌和RDB快照

雖然跟AOF相比,RDB快照的恢復速度快,但快照的頻率不好把握,如果頻率太低,兩次快照間一旦宕機,就可能有比較多的資料丟失。如果頻率太高,又會產生額外開銷,那麼,還有什麼方法既能利用 RDB 的快速恢復,又能以較小的開銷做到儘量少丟資料呢?

在Redis4.0提出了 混合使用AOF和RDB快照 的方法,也就是兩次RDB快照期間的所有命令操作由AOF日誌檔案進行記錄。這樣的好處是RDB快照不需要很頻繁的執行,可以避免頻繁fork對主執行緒的影響,而且AOF日誌也只記錄兩次快照期間的操作,不用記錄所有操作,也不會出現檔案過大的情況,避免了重寫開銷。

通過上述方法既可以享受RDB快速恢復的好處,也可以享受AOF記錄簡單命令的優勢。

對於AOF和RDB的選擇問題

  • 資料不能丟失時,記憶體快照和AOF的混合使用是一個很好的選擇。

  • 如果允許分鐘級別的資料丟失,可以只使用RDB。

  • 如果只用AOF,優先使用everysec的配置選項,因為它在可靠性和效能之間取了一個平衡。

Redis資料同步

當Redis發生宕機的時候,可以通過AOF和RDB檔案的方式恢復資料,從而保證資料的丟失從而提高穩定性。但如果Redis例項宕機了,在恢復期間就無法服務新來的資料請求;AOF和RDB雖然可以保證資料儘量的少丟失,但無法保證服務儘量少中斷,這就會影響業務的使用,不能保證Redis的高可靠性。

Redis其實採用了主從庫的模式,以保證資料副本的一致性,主從庫採用讀寫分離的方式:從庫和主庫都可以接受讀操作;對於寫操作,首先要到主庫執行,然後主庫再將寫操作同步到從庫。

只有主庫接收寫操作可以避免客戶端將資料修改到不同的Redis例項上,其他

客戶端進行讀取時可能就會讀取到舊的值;當然,如果非要所有的庫都可以進行寫操作,就要涉及到鎖、例項間協商是否完成修改等一系列操作,會帶來額外的開銷。

(一)主從庫如何進行第一次資料同步

當存在多個Redis例項的時候,可以通過replicaof命令形成主庫和從庫的關係,在從庫中輸入: replicaof主庫ip 6379 就可以在主庫中複製資料,具體有三個階段:

  • 首先是主從庫建立連線、協商同步的過程,具體的從庫向主庫傳送psync命令,代表要進行資料同步;psync中包含了主庫的runID(Redis啟動時生成的隨機ID,初始值為:?)和複製進度offset(設為-1,代表第一次複製)兩個引數,主庫接收到psync命令,會用FULLRESYNC命令返回給從庫,包含兩個引數:主庫runID和複製進度offset;其中FULLRESYNC代表的全量複製,會將主庫所有的資料都複製給從庫。

  • 待從庫接收到資料後,在本地完成資料載入,具體的主庫執行bgsave命令,生成RDB檔案,然後將檔案發給從庫,從庫接收到RDB檔案後,首先清空當前資料,然後再載入RDB檔案;這個過程主庫不會被阻塞,仍然可以接受請求,如果存在寫操作,剛剛生成的RDB檔案中是不包含這些新資料的,此時主庫會在記憶體中用專門的replication buffer記錄RDB檔案生成後所有的寫操作。

  • 最後,主庫會把replication buffer中的修改操作發給從庫,從庫重新執行這些操作,就可以實現主從庫同步了。

如果從庫的例項過多,對於主庫來說有一定的壓力,主庫會頻繁fork子程序以生成RDB檔案,fork這個操作會阻塞主執行緒處理正常請求,導致響應變慢,Redis採用了主-從-從的模式,可以手動選擇一個從庫,用來同步其他從庫的資料,以減少主庫生成RDB檔案和傳輸RDB檔案的壓力;如下圖:

級聯的“主-從-從”模式

這樣從庫就可以知道在進行資料同步的時候,不需要和主庫直接互動,只需要和選擇的從庫進行寫操作同步就可以了,從而減少主庫的壓力。

(二)主庫如果掛了呢?

Redis採用主從庫的模式保證資料副本的一致性,在這個模式下如果從庫發生故障,客戶端可以向其他主庫或者從庫傳送請求,但如果主庫掛了,客戶端就沒法進行寫操作了,也無法對從庫進行相應的資料複製操作。

不管是寫服務中斷還是從庫無法進行資料同步,都是不能接受的,所以當主庫掛了以後,需要一個新的主庫來代替掛掉的主庫,這樣就就會產生三個問題:

  • 怎麼判斷主庫是真的掛了,而不是網路異常?

  • 主庫如果掛了,該選擇哪個從庫作為新的主庫?

  • 怎麼把新主庫的相關資訊通知給從庫和客戶端?

Redis採用了 哨兵機制 應對這些問題,哨兵機制是實現主從庫自動切換的關鍵機制,在主從庫執行的同時,它也在進行 監控、選擇主庫和通知 的操作。

  • 監控。 哨兵在執行時,週期性給所有的主從庫傳送PING命令,檢測是否仍在執行。 如果庫沒有響應哨兵的PING命令,哨兵就會將它標記為下線狀態; 如果主庫沒有在規定時間內響應哨兵的PING命令,哨兵也會判斷主庫下限,然後開始自動切換主庫的流程。

  • 選主。主庫掛了之後,哨兵需要按照一定的規則選擇一個從庫,並將他作為新的主庫。

  • 通知。選取了新的主庫後,哨兵會把新主庫的連線資訊發給其他從庫,讓它們執行replicaof命令和新主庫建立連線,並進行資料複製;同時哨兵也會將新主庫的訊息發給客戶端。

下圖展示了哨兵的幾個操作的任務:

哨兵機制的三項任務與目標

但這樣也會存在一個問題,哨兵判斷主從庫是否下線如果出現失誤呢?

對於庫,下線影響不大,叢集的對外服務也不會間斷。 但是如果哨兵誤判主庫下線,可能是因為網路擁塞或者主庫壓力大的情況,這時候也就需要進行選主並讓從庫和新的主庫進行資料同步,這個過程是有一定的開銷的,所以我們要儘可能地避免誤判的情況。 哨兵機制也考慮了這一點,它通常採用多實組成的叢集模式進行部署,也被稱為哨兵叢集; 通過引入多個哨兵例項一起判斷,就可以儘可能避免單個哨兵產生的誤判問題。 這時候判斷主庫是否下線不是由一個哨兵決定的,只有大多數哨兵認為該主庫下線,主庫才會標記為“客觀下線”。

簡單的來說”客觀下線“的標準是當N個哨兵例項,有N/2+1個例項認為該主庫為下線狀態,該主庫就會被認定為“客觀下線”。這樣就可以儘量的避免單個哨兵產生的誤判問題(N/2+1這個值也可以通過引數改變);

如果判斷了主庫為主觀下線,怎麼選取新的主庫呢?

上面有說,這一部分也是由哨兵機制來完成的,選取主庫的過程分為“ 篩選 打分 ”。 主要是按照一定的規則過濾掉不符合的從庫,再按照一定的規則給其餘的從庫打分,將最高分的從庫作為新的主庫。

  • 篩選。首先從庫一定是正在執行的,還要判斷從庫之前的網路連線狀態,如果總是斷連並且超過了一定的閾值,哨兵會認為該從庫的網路不好,也會 將其篩掉。

  • 打分。哨兵機制根據三個規則依次進行打分: 從庫優先順序、從庫複製進度以及從庫ID號 ;在某一輪有從庫得分最高,那麼它就是新的主庫了,選主 過程結束。如果該輪沒有出現最高的,繼續下一輪。

  • 優先順序最高的從庫。 使用者可以通過slave-priority配置項,給不同的從庫設定優先順序。 選主庫的時候哨兵會給優先順序高的從庫打高分,如果一個從庫優先順序高,那麼就是新主庫。

  • 從庫複製進度最接近。 主庫的slave_repl_offset和從庫master_repl_offset越接近,得分越高。

  • ID小的從庫得分高。 如果上面兩輪也沒有選出新主庫,就會根據從庫例項的ID來判斷,ID越小的從庫得分越高。

由此哨兵可以選擇出一個新的主庫。

由哪個哨兵來執行主從庫切換呢?

這個過程和判斷主庫“客觀下線”類似,也是一個投票的過程。如果某個哨兵判斷了主庫為下線狀態,就會給其他的哨兵例項傳送is-master-down-by-addr的命令,其他例項會根據自己和主庫的連線狀態作出Y或N的響應,Y相當於贊成票,N為反對票。一個哨兵獲得一定的票數後,就可以標記主庫為“客觀下線”,這個票數是由引數quorum設定的。如下圖:

例如:現在有3個哨兵,quorum配置的是2,那麼,一個哨兵需要2張贊成票,就可以標記主庫為“客觀下線”了。這2張贊成票包括哨兵自己的一張贊成票和另外兩個哨兵的贊成票。

這個時候哨兵就可以給其他哨兵傳送訊息,表示希望自己來執行主從切換,並讓所有的哨兵進行投票,這個過程稱為“Leader選舉”,進行主從切換的哨兵稱為Leader。任何一個想成為Leader的哨兵都需要滿足兩個條件:

  • 拿到半數以上的哨兵贊成票。

  • 拿到的票數需要大於等於quorum的值。

以上就可以選出Leader然後進行主從庫切換了。

Redis叢集

(一 )資料量過多如何處理?

當資料量過多的情況下,一種簡單的方式是升級Redis例項的資源配置,包括增加記憶體容量、磁碟容量、更好配置的CPU等,但這種情況下Redis使用RDB進行持久化的時候響應會變慢,Redis通過fork子程序來完成資料持久化,但fork在執行時會阻塞主執行緒,資料量越大,fork的阻塞時間就越長,從而導致Redis響應變慢。

Redis的切片叢集 可以解決這個問題,也就是啟動多個Redis例項來組成一個叢集,再按照一定的規則把資料劃分為多份,每一份用一個例項來儲存,這樣客戶端只需要訪問對應的例項就可以獲取資料。在這種情況下fork子程序一般不會給主執行緒帶來較長時間的阻塞,如下圖:

切片叢集架構圖

將20GB的資料分為4分,每份包含5GB資料,客戶端只需要找到對應的例項就可以獲取資料,從而減少主執行緒阻塞的時間。

當資料量過多的時候,可以通過升級Redis例項的資源配置或者通過切片叢集的方式。前者實現起來簡單粗暴,但這資料量增加的時候,需要的記憶體也在不斷增加,主執行緒fork子程序就有可能會阻塞,而且該方案受到硬體和成本的限制。相比之下第二種方案是一種擴充套件性更好的方案,如果想儲存更多的資料,僅需要增加Redis例項的個數,不用擔心單個例項的硬體和成本限制。 在面向百萬、千萬級別的使用者規模時,橫向擴充套件的 Redis 切片叢集會是一個非常好的選擇

選擇切片叢集也是需要解決一些問題的:

  • 資料切片後,在多個例項之間怎麼分佈?

  • 客戶端怎麼確定想要訪問的例項是哪一個?

Redis採用了Redis Cluster的方案來實現切片叢集,具體的Redis Cluster採用了雜湊槽(Hash Slot)來處理資料和例項之間的對映關係。在Redis Cluster中,一個切片叢集共有16384個雜湊槽( 為什麼Hash Slot的個數是16384 ),這些雜湊槽類似於資料的分割槽,每個鍵值對都會根據自己的key被影射到一個雜湊槽中,對映步驟如下:

  • 根據鍵值對key,按照CRC16演算法計算一個16bit的值。

  • 用計算的值對16384取模,得到0~16383範圍內的模數,每個模數對應一個雜湊槽。

這時候可以得到一個key對應的雜湊槽了,雜湊槽又是如何找到對應的例項的呢?

在部署Redis Cluster的時候,可以通過cluster create命令建立叢集,此時Redis會自動把這些槽分佈在叢集例項上,例如一共有N個例項,那麼每個例項包含的槽個數就為16384/N。當然可能存在Redis例項中記憶體大小配置不一的問題,記憶體大的例項具有更大的容量。這種情況下可以通過cluster addslots命令手動分配雜湊槽。

redis-cli -h 33.33.33.3p 6379 cluster addslots 0,1
redis-cli -h 33.33.33.4p 6379 cluster addslots 2,3
redis-cli -h 33.33.33.5p 6379 cluster addslots 4

要注意的是,如果採用cluster addslots的方式手動分配雜湊槽,需要將16384個槽全部分配完,否則Redis叢集無法正常工作。現在通過雜湊槽,切片叢集就實現了資料到雜湊槽、雜湊槽到例項的對應關係,那麼客戶端如何確定需要訪問的例項是哪一個呢?

(二)客戶端定位叢集中的資料

客戶端請求的key可以通過CRC16演算法計算得到,但客戶端還需要知道雜湊槽分佈在哪個例項上。在最開始客戶端和叢集例項建立連線後,例項就會把雜湊槽的分配資訊發給客戶端,例項之間會把自己的雜湊槽資訊發給和它相連的例項,完成雜湊槽的擴散。這樣客戶端訪問任何一個例項的時候,都能獲取所有的雜湊槽資訊。當客戶端收到雜湊槽的資訊後會把雜湊槽對應的資訊快取在本地,當客戶端傳送請求的時候,會先找到key對應的雜湊槽,然後就可以給對應的例項傳送請求了。

但是,雜湊槽和例項的對應關係不是一成不變的,可能會存在新增或者刪除的情況,這時候就需要重新分配雜湊槽;也可能為了負載均衡,Redis需要把所有的例項重新分佈。

雖然例項之間可以互相傳遞訊息以獲取最新的雜湊槽分配資訊,但是客戶端無法感知這個變化,就會導致客戶端訪問的例項可能不是自己所需要的了。

Redis Cluster提供了重定向的機制,當客戶端給例項傳送資料讀寫操作的時候,如果這個例項上沒有找到對應的資料,此時這個例項就會給客戶端返回MOVED命令的相應結果,這個結果中包含了新例項的訪問地址,此時客戶端需要再給新例項傳送操作命令以進行讀寫操作,MOVED命令如下:

GET hello:key
(error) MOVED 33.33.33.33:6379

返回的資訊代表客戶端請求的key所在的雜湊槽為3333,實際是在33.33.33.33這個例項上,此時客戶端只需要向33.33.33.33這個例項傳送請求就可以了。

此時也存在一個小問題,雜湊槽中對應的資料過多,導致還沒有遷移到其他例項,此時客戶端就發起了請求,在這種情況下,客戶端就對例項發起了請求,如果資料還在對應的例項中,會給客戶端返回資料;如果請求的資料已經被轉移到其他例項上,客戶端就會收到例項返回的ASK命令,該命令表示:雜湊槽中資料還在前一種、ASK命令把客戶端需要訪問的新例項返回了。此時客戶端需要給新例項傳送ASKING命令以進行請求操作。

值得注意的是ASK資訊和MOVED資訊不一樣, ASK資訊並不會更新客戶端本地的快取的雜湊槽分配資訊 ,也就是說如果客戶端再次訪問該雜湊槽還是會請求之前的例項,直到資料遷移完成。

參考資料:

Redis核 心技術與實戰

作者簡介

劉波

騰訊後臺開發工程師

騰訊後臺開發工程師,目前負責智慧零售營銷產品部相關開發工作。

推薦閱讀

揭祕KVM年度核心技術突破的背後原理!

避坑指南!如何在TKE上安裝KubeSphere?

一種海量資料安全分類分級架構的實現!

一站式DevOps真的能提速增效嗎?TVP吐槽大會邀您來驗證!

9 月 24 日 CODING DevOps 專題 TVP 吐槽大會火爆開啟 ,一同見證領域大咖巔峰對決!

掃碼立即參會贏好禮:point_down:

:point_down: 點選 「閱讀原文」 註冊成為社 區創作者,認識大咖,打造你的技術影響力!