這篇文章跟大家整理分享一些Redis高頻面試題,帶大家過一遍Redis核心知識點,涉及到資料結構、記憶體模型、 IO 模型、持久化 RDB等,希望對大家有幫助!
很多人只知道是 K/V NoSQl 記憶體資料庫,單執行緒…這都是沒有全面理解 Redis 導致無法繼續深問下去。
這個問題是基礎摸底,我們可以從Redis 不同資料型別底層的資料結構實作、完全基於記憶體、IO 多路復用網路模型、執行緒模型、漸進式rehash…...
我們可以先說到底有多快,根據官方數據,Redis 的 QPS 可以達到約 100000(每秒請求數),有興趣的可以參考官方的基準程序測試《How fast is Redis? 》,地址:redis.io/topics/benc…
#橫軸是連接數,縱軸是 QPS。
這張圖反映了一個數量級,透過量化讓面試官覺得你看過官方文檔,很嚴謹。
Redis 是基於記憶體的資料庫,跟磁碟資料庫相比,完全吊打磁碟的速度。
不論讀寫操作都是在記憶體上完成的,我們分別比較下記憶體運算與磁碟運算的差異。
磁碟呼叫
記憶體運算
記憶體直接由CPU 控制,也就是CPU 內部整合的記憶體控制器,所以說記憶體是直接與CPU 對接,享受與CPU 通訊的最適頻寬。
最後以一張圖量化系統的各種延時時間(部分資料引用Brendan Gregg)
學習MySQL 的時候我知道為了提高檢索速度使用了B Tree 資料結構,所以Redis 速度快應該也跟資料結構有關。
Redis 總共有 5 種資料類型,String、List、Hash、Set、SortedSet
。
不同的資料類型底層使用了一種或多種資料結構來支撐,目的就是為了追求更快的速度。
碼哥寄語:我們可以分別說明每種資料型態底層的資料結構優點,很多人只知道資料類型,而說出底層資料結構就能讓人眼前一亮。
SDS 中len 保存這字串的長度,O(1) 時間複雜度查詢字串長度資訊。
空間預先分配:SDS 被修改後,程式不僅會為 SDS 分配所需的必須空間,還會分配額外的未使用空間。
惰性空間釋放:當SDS 進行縮短操作時,程式不會回收多餘的記憶體空間,而是使用free 欄位將這些位元組數量記錄下來不釋放,後面如果需要append 操作,則直接使用free 中未使用的空間,減少了記憶體的分配。
壓縮清單是 List 、hash、 sorted Set 三種資料型別底層實作之一。
當一個列表只有少量資料的時候,而每個列表項要不是小整數值,就是長度比較短的字串,那麼 Redis 就會使用壓縮列表來做列表鍵的底層實作。
這樣記憶體緊湊,節約記憶體。
後續版本對清單資料結構進行了改造,使用 quicklist 取代了 ziplist 和 linkedlist。
quicklist 是 ziplist 和 linkedlist 的混合體,它將 linkedlist 按段切分,每一段使用 ziplist 來緊湊存儲,多個 ziplist 之間使用雙向指標串接起來。
sorted set 類型的排序功能就是透過「跳躍清單」資料結構來實現。
跳躍表(skiplist)是一種有序資料結構,它透過在每個節點中維持多個指向其他節點的指針,從而達到快速存取節點的目的。
跳表在鍊錶的基礎上,增加了多層級索引,透過索引位置的幾個跳轉,實現資料的快速定位,如下圖所示:
當一個集合只包含整數值元素,而這個集合的元素數量不多時,Redis 就會使用整數集合作為集合鍵的底層實現,節省記憶體。
碼哥寄語:我們需要注意的是,Redis 的單執行緒指的是Redis 的網路IO (6.x 版本後網路IO使用多執行緒)以及鍵值對指令讀寫是由一個執行緒來執行的。 對於 Redis 的持久化、叢集資料同步、非同步刪除等都是其他執行緒執行。
千萬別說 Redis 就只有一個執行緒。
單執行緒指的是 Redis 鍵值對讀寫指令的執行是單執行緒。
先說官方答案,讓人覺得夠嚴謹,而不是人云亦雲去背誦一些部落格。
官方答案:因為 Redis 是基於記憶體的操作,CPU 不是 Redis 的瓶頸,Redis 的瓶頸最有可能是機器記憶體的大小或網路頻寬#。既然單執行緒容易實現,而且 CPU 不會成為瓶頸,那就順理成章地採用單執行緒的方案了。原文網址:redis.io/topics/faq。
為啥不用多執行緒執行充分利用 CPU 呢?
在執行每個任務之前,CPU 需要知道任務在何處載入並開始運行。也就是說,系統需要幫助它預先設定 CPU 暫存器和程式計數器,稱為 CPU 上下文。
切換上下文時,我們需要完成一系列工作,這是非常消耗資源的操作。
引入多執行緒開發,就需要使用同步原語來保護共享資源的並發讀寫,增加程式碼複雜度和偵錯難度。
單執行緒又什麼好處?
Redis 採用 I/O 多工技術,並發處理連線。採用了 epoll 自己實作的簡單的事件框架。
epoll 中的讀、寫、關閉、連接都轉換成了事件,然後利用 epoll 的多工特性,絕不在 IO 上浪費一點時間。
Redis 執行緒不會阻塞在某一個特定的監聽或已連接套接字上,也就是說,不會阻塞在某一個特定的客戶端請求處理上。正因為此,Redis 可以同時和多個客戶端連線並處理請求,從而提升並發性。
Redis 整體就是一個 雜湊表來保存所有的鍵值對,無論資料型別是 5 種的任一種。哈希表,本質就是一個數組,每個元素被叫做哈希桶,不管什麼資料型,每個桶裡面的 entry 保存著實際具體值的指標。
而雜湊表的時間複雜度是O(1),只需要計算每個鍵的雜湊值,就知道對應的雜湊桶位置,定位桶裡面的entry 找到對應數據,這也是Redis 快的原因之一。
Redis 使用物件(redisObject)來表示資料庫中的鍵值,當我們在Redis 中建立一個鍵值對時,至少建立兩個對象,一個物件是用做鍵值對的鍵對象,另一個是鍵值對的值物件。
也就是每個 entry 保存著 「鍵值對」的 redisObject 對象,透過 redisObject 的指標來找到對應資料。
typedef struct redisObject{ //类型 unsigned type:4; //编码 unsigned encoding:4; //指向底层数据结构的指针 void *ptr; //... }robj;
Redis 透過鍊式雜湊解決衝突:也就是同一個 桶裡面的元素使用鍊錶保存。但是當鍊錶過長就會導致查找效能變差可能,所以 Redis 為了追求快,使用了兩個全域雜湊表。用於 rehash 操作,增加現有的哈希桶數量,減少哈希衝突。
開始預設使用 “hash 表 1 ”儲存鍵值對數據,“hash 表 2” 此刻沒有分配空間。當資料越來多觸發 rehash 操作,則執行以下操作:
值得注意的是,將 hash 表 1 的資料重新對應到 hash 表 2 的過程中並不是一次性的,這會造成 Redis 阻塞,無法提供服務。
而是採用了漸進式rehash,每次處理客戶端請求的時候,先從「 hash 表1」 中第一個索引開始,將這個位置的所有資料拷貝到「hash 表2」 中,就這樣將rehash 分散到多次請求過程中,避免耗時阻塞。
Redis 的資料持久化使用了「RDB 資料快照」的方式來實現宕機快速復原。但是 過於頻繁的執行全量資料快照,有兩個嚴重效能開銷:
所以 Redis 也設計了 AOF 寫後日誌記錄對記憶體進行修改的指令記錄。
面試官:什麼是 RDB 記憶體快照?
在 Redis 執行「寫」指令過程中,記憶體資料會一直改變。所謂的記憶體快照,指的就是 Redis 記憶體中的資料在某一刻的狀態資料。
好比時間定格在某一刻,當我們拍照的,透過照片就能把某一刻的瞬間畫面完全記錄下來。
Redis 跟這個類似,就是把某一刻的資料以檔案的形式拍下來,寫到磁碟上。這個快照檔案叫做 RDB 文件,RDB 就是 Redis DataBase 的縮寫。
在做資料復原時,直接將 RDB 檔案讀入記憶體完成復原。
面試官:在產生 RDB 期間,Redis 可以同時處理寫入請求麼?
可以的,Redis 使用作業系統的多進程寫入時複製技術 COW(Copy On Write) 來實現快照持久化,確保資料一致性。
Redis 在持久化時會呼叫 glibc 的函數fork
產生子程序,快照持久化完全交給子程序來處理,父程序繼續處理客戶端請求。
當主執行緒執行寫入指令修改資料的時候,這個資料就會複製一份副本, bgsave
子程序讀取這個副本資料寫到 RDB 檔案。
這既保證了快照的完整性,也允許主執行緒同時對資料進行修改,避免了對正常業務的影響。
面試官:那 AOF 又是什麼?
AOF 日誌記錄了自Redis 實例創建以來所有的修改性指令序列,那麼就可以透過對一個空的Redis 實例順序執行所有的指令,也就是「重播」,來恢復Redis 目前實例的記憶體資料結構的狀態。
Redis 提供的 AOF 配置項目appendfsync
寫回策略直接決定 AOF 持久化功能的效率和安全性。
aof_buf
#緩衝區中的內容刷寫到 AOF 檔案。 沒有兩全其美的策略,我們需要在性能和可靠性上做一個取捨。
訪談員:既然 RDB 有兩個效能問題,那為何不用 AOF 即可。
AOF 寫前日誌,記錄的是每個「寫」指令操作。不會像 RDB 全量快照導致效能損耗,但執行速度沒有 RDB 快,同時日誌檔案過大也會造成效能問題。
所以,Redis 設計了一個殺手鐧“AOF 重寫機制”,Redis 提供了 bgrewriteaof
指令用於對 AOF 日誌進行瘦身。
其原理是開啟一個子程序對記憶體進行遍歷轉換成一系列 Redis 的操作指令,序列化到一個新的 AOF 日誌檔。序列化完畢後再將操作期間發生的增量 AOF 日誌追加到這個新的 AOF 日誌檔中,追加完畢後就立即替代舊的 AOF 日誌檔了,瘦身工作就完成了。
面試官:如何實現 資料盡可能少遺失又能兼顧效能?
重啟 Redis 時,我們很少使用 rdb 來恢復記憶體狀態,因為會遺失大量資料。我們通常使用 AOF 日誌重播,但重播 AOF 日誌效能相對 rdb 來說要慢很多,這樣在 Redis 實例很大的情況下,啟動需要花費很長的時間。
Redis 4.0 為了解決這個問題,帶來了一個新的持久化選項—混合持久化。將 rdb 檔案的內容和增量的 AOF 日誌檔案存在一起。這裡的 AOF 日誌不再是全量的日誌,而是自持久化開始到持久化結束的這段時間發生的增量 AOF 日誌,通常這部分 AOF 日誌很小。
於是在Redis 重啟的時候,可以先載入rdb 的內容,然後再重播增量AOF 日誌就可以完全取代先前的AOF 全量檔案重放,重啟效率因此大幅提升。
Redis 提供了主從模式,透過主從複製,將資料冗餘一份複製到其他 Redis 伺服器。
面試官:主從之間資料如何保證一致性?
為了確保副本資料的一致性,主從架構採用了讀寫分離的方式。
#面試官:主從複製還有其他作用麼?
面試官:主從複製如何實現的?
同步有三種情況:
面試官:第一次同步怎麼實現?
主從庫第一次複製過程大體可以分為3 個階段:連接建立階段(即準備階段)、主庫同步資料到從庫階段、發送同步期間新寫命令到從庫階段;
bgsave
指令產生RDB 文件,並將文件傳送給從函式庫,同時主函式庫為每一個slave 開闢一塊replication buffer 緩衝區記錄從產生RDB 檔案開始收到的所有寫入指令。從庫保存 RDB 並清空資料庫再載入 RDB 資料到記憶體中。 面試官:主從庫間的網路斷了咋辦?斷開後要重新全量複製麼?
在 Redis 2.8 之前,如果主從函式庫在指令傳播時出現了網路閃斷,那麼,從函式庫就會和主函式庫重新進行一次全量複製,開銷非常大。
從 Redis 2.8 開始,網路斷了之後,主從函式庫會採用增量複製的方式繼續同步。
增量複製:用於網路中斷等情況後的複製,只將中斷期間主節點執行的寫入命令傳送給從節點,與全量複製相比更有效率。
斷開重連增量複製的實作奧秘就是repl_backlog_buffer
緩衝區,不管在什麼時候master 都會將寫指令操作記錄在repl_backlog_buffer
中,因為記憶體有限, repl_backlog_buffer
是一個定長的環形數組,如果數組內容滿了,就會從頭開始覆蓋前面的內容。
master 使用 master_repl_offset
記錄自己寫到的位置偏移量,slave 則使用 slave_repl_offset
記錄已經讀取到的偏移量。
當主從斷開重連後,slave 會先發送psync 指令給master,同時將自己的runID
,slave_repl_offset
傳送給master。
master 只要把 master_repl_offset
與 slave_repl_offset
之間的指令同步到從函式庫即可。
增量複製執行流程如下圖:
面試官:那完成全量同步後,正常運作過程中如何同步資料呢?
當主從庫完成了全量複製,它們之間就會一直維護一個網路連接,主庫會透過這個連接將後續陸續收到的命令操作再同步給從庫,這個過程也稱為基於長連接的命令傳播,使用長連接的目的是避免頻繁建立連接導致的開銷。
面試官:可以呀,知道這麼多,你知道 哨兵群集原理麼?
哨兵是Redis 的一種運作模式,它專注於對Redis 實例(主節點、從節點)運行狀態的監控,並且能夠在主節點發生故障時通過一系列的機制實現選主及主從切換,實現故障轉移,確保整個Redis 系統的可用性。
他的架構圖如下:
Redis 哨兵具備的能力有以下幾個:
面試官:哨兵之間是如何知道彼此的?
哨兵與master 建立通信,利用master 提供發布/訂閱機制發布自己的信息,例如身高體重、是否單身、IP、端口……
master 有一個__sentinel__:hello
的專用通道,用於哨兵之間發佈和訂閱訊息。 這就好比是 __sentinel__:hello
微信群,哨兵利用 master 建立的微信群發布自己的消息,同時關注其他哨兵發布的消息。
面試官:哨兵之間雖然建立連接了,但是還需要和 slave 建立連接,不然沒法監控他們呀,如何知道 slave 並監控他們的?
關鍵還是利用 master 來實現,哨兵向 master 發送 INFO
指令, master 掌門自然是知道自己門下所有的 salve 小弟的。所以 master 接收到指令後,便將 slave 清單告訴哨兵。
哨兵根據 master 回應的 slave 名單資訊與每一個 salve 建立連接,並且根據這個連接持續監控哨兵。
面試官:除了哨兵以外,還有其他的高可用手段麼?
有 Cluster 叢集實作高可用,哨兵叢集監控的 Redis 叢集是主從架構,無法很想拓展。 使用 Redis Cluster 集群,主要解決了大數據量儲存導致的各種慢問題,同時也便於橫向拓展。
在面向百萬、千萬等級的使用者規模時,橫向擴展的 Redis 切片叢集會是一個非常好的選擇。
面試官:什麼是 Cluster 叢集?
Redis 叢集是一種分散式資料庫方案,叢集透過分片(sharding)來進行資料管理(「分治思想」的一種實踐),並提供複製和故障轉移功能。
將資料劃分為 16384 的 slots,每個節點負責一部分插槽。槽位的資訊儲存於每個節點中。
它是去中心化的,如圖所示,該叢集有三個 Redis 節點組成,每個節點負責整個叢集的一部分數據,每個節點負責的資料多少可能不一樣。
三個節點相互連接組成一個對等的集群,它們之間透過Gossip
協議相互交互集群訊息,最後每個節點都保存著其他節點的slots 分配狀況。
面試官:哈希槽又如何映射到 Redis 實例上呢?
鍵值對資料、雜湊槽、Redis 實例之間的對應關係如下:
面試官:Cluster 如何實現故障轉移?
Redis 叢集節點採用 Gossip
協定來廣播自己的狀態以及自己對整個叢集認知的改變。例如一個節點發現某個節點失聯了 (PFail),它會將這訊息向整個叢集廣播,其它節點也就可以收到這點失聯訊息。
如果一個節點收到了某個節點失聯的數量(PFail Count) 已經達到了叢集的大多數,就可以標記該節點為確定下線狀態(Fail),然後向整個叢集廣播,強迫其它節點也接收該節點已經下線的事實,並立即對該失聯節點進行主從切換。
面試官:客戶端又怎麼決定存取的資料到底分佈在哪個實例上呢?
Redis 實例會將自己的雜湊槽資訊透過 Gossip 協定傳送給叢集中其他的實例,實現了哈希槽分配資訊的擴散。
這樣,叢集中的每個實例都有所有雜湊槽與實例之間的映射關係資訊。
當客戶端連接任何一個實例,實例就將哈希槽與實例的映射關係回應給客戶端,客戶端就會將哈希槽與實例映射資訊緩存在本地。
當客戶端請求時,會計算出鍵所對應的雜湊槽,再透過本機快取的雜湊槽實例對應資訊定位到資料所在實例上,再將請求傳送給對應的實例。
面試官:什麼是 Redis 重定向機制?
哈希槽與實例之間的映射關係由於新增實例或者負載平衡重新分配導致改變了,客戶端將請求發送到實例上,這個實例沒有相應的數據,該Redis 實例會告訴客戶端將請求傳送到其他的實例上。
Redis 透過 MOVED 錯誤和 ASK 錯誤告訴客戶端。
MOVED 錯誤(負載平衡,資料已經移轉到其他實例上):當客戶端將一個鍵值對操作請求傳送給某個實例,而這個鍵所在的槽並非由自己負責的時候,該實例會傳回一個MOVED 錯誤指引轉向正在負責該槽的節點。
同時,客戶端也會更新本機緩存,將該 slot 與 Redis 實例對應關係更新正確。
如果某個 slot 的資料比較多,部分遷移到新實例,還有一部分沒有遷移。
如果請求的 key 在目前節點找到就直接執行指令,否則時候就需要 ASK 錯誤回應了。
槽部分遷移未完成的情況下,如果需要存取的key 所在Slot 正在從實例1 遷移到實例2(如果key 已經不在實例1),實例1 會傳回客戶端一條ASK 報錯訊息: 客戶端請求的key 所在的雜湊槽正在遷移到實例2 上,你先給實例2 傳送一個ASKING 指令,接著發送操作指令。
例如客戶端請求定位到key = “公眾號:碼哥字節”的槽16330 在實例172.17.18.1 上,節點1 如果找得到就直接執行命令,否則響應ASK 錯誤信息,並指引客戶端轉向正在遷移的目標節點172.17.18.2。
注意:ASK 錯誤指令並不會更新客戶端快取的雜湊槽分配資訊。
本篇主要將Redis 核心內容過了一遍,涉及資料結構、記憶體模型、 IO 模型、持久化RDB 和AOF 、主從複製原理、哨兵原理、cluster原理。
原文網址:https://juejin.cn/post/6976257378094481444
作者:碼哥字節
#更多程式相關知識,請造訪:程式設計影片! !
以上是Redis高頻面試題分享,帶你橫掃核心知識點!的詳細內容。更多資訊請關注PHP中文網其他相關文章!