深入淺出帶你走進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: 點擊 「閲讀原文」 註冊成為社 區創作者,認識大咖,打造你的技術影響力!