「碼哥字節」從高頻面試問題跟你們一塊兒橫掃 Redis 核心知識點,從根本上理解 Redis ,不作八股文的工具人,作扭轉乾坤的大神。c++
碼哥到現在已經寫了 9 篇 Redis 連載,後臺有小夥伴也讓我寫一些關於面試的文章,因而「麪霸」系列便出道了。面試
若是你們用心讀完《Redis 系列》並理解,吊打面試官根本不是事。redis
不少人只知道是 K/V NoSQl 內存數據庫,單線程……這都是沒有全面理解 Redis 致使沒法繼續深問下去。算法
這個問題是基礎摸底,咱們能夠從 Redis 不一樣數據類型底層的數據結構實現、徹底基於內存、IO 多路複用網絡模型、線程模型、漸進式 rehash…...數據庫
咱們能夠先說到底有多快,根據官方數據,Redis 的 QPS 能夠達到約 100000(每秒請求數),有興趣的能夠參考官方的基準程序測試《How fast is Redis?》,地址:https://redis.io/topics/benchmarks數組
橫軸是鏈接數,縱軸是 QPS。緩存
這張圖反映了一個數量級,經過量化讓面試官以爲你有看過官方文檔,很嚴謹。安全
Redis 是基於內存的數據庫,跟磁盤數據庫相比,徹底吊打磁盤的速度。服務器
不論讀寫操做都是在內存上完成的,咱們分別對比下內存操做與磁盤操做的差別。微信
磁盤調用
內存操做
內存直接由 CPU 控制,也就是 CPU 內部集成的內存控制器,因此說內存是直接與 CPU 對接,享受與 CPU 通訊的最優帶寬。
最後以一張圖量化系統的各類延時時間(部分數據引用 Brendan Gregg)
學習 MySQL 的時候我知道爲了提升檢索速度使用了 B+ Tree 數據結構,因此 Redis 速度快應該也跟數據結構有關。
Redis 一共有 5 種數據類型,String、List、Hash、Set、SortedSet
。
不一樣的數據類型底層使用了一種或者多種數據結構來支撐,目的就是爲了追求更快的速度。
碼哥寄語:咱們能夠分別說明每種數據類型底層的數據結構優勢,不少人只知道數據類型,而說出底層數據結構就能讓人眼前一亮。
壓縮列表是 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 不會成爲瓶頸,那就瓜熟蒂落地採用單線程的方案了。原文地址:https://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 原理。
「麪霸」系列會分爲幾篇,分別從核心原理、高可用、實戰、如何避坑等方面全方位拿下 Redis。
你們以爲有所幫助但願能夠動動手指點贊、分享、收藏、留言呀,也能夠加「碼哥」微信「MageByte1024」進入專屬讀者羣一塊兒探討更多面試遇到的問題,碼哥知無不答。