Redis硬核篇:從高頻問題橫掃核心知識原理

      網友投稿 992 2022-05-28

      前言

      本篇主要將Redis核心內容過了一遍,涉及到數據結構、內存模型、IO模型、持久化RDB和AOF、主從復制原理、哨兵原理、cluster原理。

      Redis為什么這么快?

      很多人只知道是K/V NoSQl內存數據庫,單線程……這都是沒有全面理解Redis導致無法繼續深問下去。

      這個問題是基礎摸底,我們可以從Redis不同數據類型底層的數據結構實現、完全基于內存、IO多路復用網絡模型、線程模型、漸進式rehash……

      總結了一張Redis知識圖譜分享給大家

      到底有多快?

      我們可以先說到底有多快,根據官方數據,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。

      不同的數據類型底層使用了一種或者多種數據結構來支撐,目的就是為了追求更快的速度。

      碼哥寄語:我們可以分別說明每種數據類型底層的數據結構優點,很多人只知道數據類型,而說出底層數據結構就能讓人眼前一亮。

      SDS簡單動態字符串優勢

      C語言字符串與SDS

      1.SDS中len保存這字符串的長度,O(1) 時間復雜度查詢字符串長度信息。

      2.空間預分配:SDS被修改后,程序不僅會為SDS分配所需要的必須空間,還會分配額外的未使用空間。

      3.惰性空間釋放:當對SDS進行縮短操作時,程序并不會回收多余的內存空間,而是使用free字段將這些字節數量記錄下來不釋放,后面如果需要 append 操作,則直接使用free中未使用的空間,減少了內存的分配。

      zipList 壓縮列表

      壓縮列表是 List 、hash、 sorted Set 三種數據類型底層實現之一。

      當一個列表只有少量數據的時候,并且每個列表項要么就是小整數值,要么就是長度比較短的字符串,那么 Redis 就會使用壓縮列表來做列表鍵的底層實現。

      ziplist

      這樣內存緊湊,節約內存。

      quicklist后續版本對列表數據結構進行了改造,使用 quicklist 代替了 ziplist 和 linkedlist。

      quicklist 是 ziplist 和 linkedlist 的混合體,它將 linkedlist 按段切分,每一段使用 ziplist 來緊湊存儲,多個 ziplist 之間使用雙向指針串接起來。

      skipList 跳躍表

      sorted set 類型的排序功能便是通過「跳躍列表」數據結構來實現。

      跳躍表(skiplist)是一種有序數據結構,它通過在每個節點中維持多個指向其他節點的指針,從而達到快速訪問節點的目的。

      跳表在鏈表的基礎上,增加了多層級索引,通過索引位置的幾個跳轉,實現數據的快速定位,如下圖所示:

      跳躍表

      整數數組(intset)當一個集合只包含整數值元素,并且這個集合的元素數量不多時,Redis 就會使用整數集合作為集合鍵的底層實現,節省內存。

      單線程模型

      碼哥寄語:我們需要注意的是,Redis 的單線程指的是 Redis 的網絡 IO (6.x 版本后網絡 IO 使用多線程)以及鍵值對指令讀寫是由一個線程來執行的。 對于 Redis 的持久化、集群數據同步、異步刪除等都是其他線程執行。

      千萬別說 Redis 就只有一個線程。

      單線程指的是 Redis 鍵值對讀寫指令的執行是單線程。

      先說官方答案,讓人覺得足夠嚴謹,而不是人云亦云去背誦一些博客。

      官方答案:因為 Redis 是基于內存的操作,CPU 不是 Redis 的瓶頸,Redis 的瓶頸最有可能是機器內存的大小或者網絡帶寬。既然單線程容易實現,而且 CPU 不會成為瓶頸,那就順理成章地采用單線程的方案了。原文地址:https://redis.io/topics/faq。

      為啥不用多線程執行充分利用 CPU 呢?

      在運行每個任務之前,CPU 需要知道任務在何處加載并開始運行。也就是說,系統需要幫助它預先設置 CPU 寄存器和程序計數器,這稱為 CPU 上下文。

      切換上下文時,我們需要完成一系列工作,這是非常消耗資源的操作。

      引入多線程開發,就需要使用同步原語來保護共享資源的并發讀寫,增加代碼復雜度和調試難度。

      單線程有什么好處?

      不會因為線程創建導致的性能消耗;

      避免上下文切換引起的 CPU 消耗,沒有多線程切換的開銷;

      避免了線程之間的競爭問題,比如添加鎖、釋放鎖、死鎖等,不需要考慮各種鎖問題。

      代碼更清晰,處理邏輯簡單。

      I/O 多路復用模型

      Redis 采用 I/O 多路復用技術,并發處理連接。采用了 epoll + 自己實現的簡單的事件框架。

      epoll 中的讀、寫、關閉、連接都轉化成了事件,然后利用 epoll 的多路復用特性,絕不在 IO 上浪費一點時間。

      高性能 IO 多路復用

      Redis 線程不會阻塞在某一個特定的監聽或已連接套接字上,也就是說,不會阻塞在某一個特定的客戶端請求處理上。正因為此,Redis 可以同時和多個客戶端連接并處理請求,從而提升并發性。

      Redis 全局 hash 字典

      Redis 整體就是一個 哈希表來保存所有的鍵值對,無論數據類型是 5 種的任意一種。哈希表,本質就是一個數組,每個元素被叫做哈希桶,不管什么數據類型,每個桶里面的 entry 保存著實際具體值的指針。

      Redis 全局哈希表

      而哈希表的時間復雜度是 O(1),只需要計算每個鍵的哈希值,便知道對應的哈希桶位置,定位桶里面的 entry 找到對應數據,這個也是 Redis 快的原因之一。

      Redis 使用對象(redisObject)來表示數據庫中的鍵值,當我們在 Redis 中創建一個鍵值對時,至少創建兩個對象,一個對象是用做鍵值對的鍵對象,另一個是鍵值對的值對象。

      也就是每個 entry 保存著 「鍵值對」的 redisObject 對象,通過 redisObject 的指針找到對應數據。

      typedef struct redisObject{ //類型 unsigned type:4; //編碼 unsigned encoding:4; //指向底層數據結構的指針 void *ptr; //... }robj;

      1

      2

      3

      4

      5

      6

      7

      8

      9

      Hash 沖突怎么辦?

      Redis 通過鏈式哈希解決沖突:也就是同一個 桶里面的元素使用鏈表保存。但是當鏈表過長就會導致查找性能變差可能,所以 Redis 為了追求快,使用了兩個全局哈希表。用于 rehash 操作,增加現有的哈希桶數量,減少哈希沖突。

      開始默認使用 「hash 表 1 」保存鍵值對數據,「hash 表 2」 此刻沒有分配空間。當數據越來越多觸發 rehash 操作,則執行以下操作:

      1.給 「hash 表 2 」分配更大的空間;

      2.將 「hash 表 1 」的數據重新映射拷貝到 「hash 表 2」 中;

      3.釋放 「hash 表 1」 的空間。

      值得注意的是,將 hash 表 1 的數據重新映射到 hash 表 2 的過程中并不是一次性的,這樣會造成 Redis 阻塞,無法提供服務。

      而是采用了漸進式 rehash,每次處理客戶端請求的時候,先從「 hash 表 1」 中第一個索引開始,將這個位置的 所有數據拷貝到 「hash 表 2」 中,就這樣將 rehash 分散到多次請求過程中,避免耗時阻塞。

      Redis 如何實現持久化?宕機后如何恢復數據?

      Redis 的數據持久化使用了「RDB 數據快照」的方式來實現宕機快速恢復。但是 過于頻繁的執行全量數據快照,有兩個嚴重性能開銷:

      1.頻繁生成 RDB 文件寫入磁盤,磁盤壓力過大。會出現上一個 RDB 還未執行完,下一個又開始生成,陷入死循環。

      2.fork 出 bgsave 子進程會阻塞主線程,主線程的內存越大,阻塞時間越長。

      所以 Redis 還設計了 AOF 寫后日志記錄對內存進行修改的指令記錄。

      面試官:什么是 RDB 內存快照?

      在 Redis 執行「寫」指令過程中,內存數據會一直變化。所謂的內存快照,指的就是 Redis 內存中的數據在某一刻的狀態數據。

      好比時間定格在某一刻,當我們拍照的,通過照片就能把某一刻的瞬間畫面完全記錄下來。

      Redis 跟這個類似,就是把某一刻的數據以文件的形式拍下來,寫到磁盤上。這個快照文件叫做 RDB 文件,RDB 就是 Redis DataBase 的縮寫。

      RDB內存快照

      在做數據恢復時,直接將 RDB 文件讀入內存完成恢復。

      面試官:在生成 RDB 期間,Redis 可以同時處理寫請求么?

      可以的,Redis 使用操作系統的多進程寫時復制技術 COW(Copy On Write) 來實現快照持久化,保證數據一致性。

      Redis 在持久化時會調用 glibc 的函數fork產生一個子進程,快照持久化完全交給子進程來處理,父進程繼續處理客戶端請求。

      當主線程執行寫指令修改數據的時候,這個數據就會復制一份副本, bgsave 子進程讀取這個副本數據寫到 RDB 文件。

      這既保證了快照的完整性,也允許主線程同時對數據進行修改,避免了對正常業務的影響。

      寫時復制技術保證快照期間數據可修改

      面試官:那 AOF 又是什么?

      AOF 日志記錄了自 Redis 實例創建以來所有的修改性指令序列,那么就可以通過對一個空的 Redis 實例順序執行所有的指令,也就是「重放」,來恢復 Redis 當前實例的內存數據結構的狀態。

      Redis 提供的 AOF 配置項appendfsync寫回策略直接決定 AOF 持久化功能的效率和安全性。

      沒有兩全其美的策略,我們需要在性能和可靠性上做一個取舍。

      面試官:既然 RDB 有兩個性能問題,那為何不用 AOF 即可。

      AOF 寫前日志,記錄的是每個「寫」指令操作。不會像 RDB 全量快照導致性能損耗,但是執行速度沒有 RDB 快,同時日志文件過大也會造成性能問題。

      所以,Redis 設計了一個殺手锏「AOF 重寫機制」,Redis 提供了 bgrewriteaof指令用于對 AOF 日志進行瘦身。

      Redis硬核篇:從高頻問題橫掃核心知識原理

      其原理就是開辟一個子進程對內存進行遍歷轉換成一系列 Redis 的操作指令,序列化到一個新的 AOF 日志文件中。序列化完畢后再將操作期間發生的增量 AOF 日志追加到這個新的 AOF 日志文件中,追加完畢后就立即替代舊的 AOF 日志文件了,瘦身工作就完成了。

      AOF重寫機制(3條變一條)

      面試官:如何實現數據盡可能少丟失又能兼顧性能呢?

      重啟 Redis 時,我們很少使用 rdb 來恢復內存狀態,因為會丟失大量數據。我們通常使用 AOF 日志重放,但是重放 AOF 日志性能相對 rdb 來說要慢很多,這樣在 Redis 實例很大的情況下,啟動需要花費很長的時間。

      Redis 4.0 為了解決這個問題,帶來了一個新的持久化選項——混合持久化。將 rdb 文件的內容和增量的 AOF 日志文件存在一起。這里的 AOF 日志不再是全量的日志,而是自持久化開始到持久化結束的這段時間發生的增量 AOF 日志,通常這部分 AOF 日志很小。

      于是在 Redis 重啟的時候,可以先加載 rdb 的內容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重啟效率因此大幅得到提升。

      Redis主從架構數據同步

      Redis 提供了主從模式,通過主從復制,將數據冗余一份復制到其他 Redis 服務器。

      面試官:主從之間數據如何保證一致性?

      為了保證副本數據的一致性,主從架構采用了讀寫分離的方式。

      讀操作:主、從庫都可以執行;

      寫操作:主庫先執行,之后將寫操作同步到從庫;

      Redis讀寫分離

      面試官:主從復制還有其他作用么?

      故障恢復:當主節點宕機,其他節點依然可以提供服務;

      負載均衡:Master 節點提供寫服務,Slave 節點提供讀服務,分擔壓力;

      高可用基石:是哨兵和 cluster 實施的基礎,是高可用的基石。

      面試官:主從復制如何實現的?

      同步分為三種情況:

      第一次主從庫全量復制;

      主從正常運行期間的同步;

      主從庫間網絡斷開重連同步。

      面試官:第一次同步怎么實現?

      主從庫第一次復制過程大體可以分為 3 個階段:連接建立階段(即準備階段)、主庫同步數據到從庫階段、發送同步期間新寫命令到從庫階段;

      Redis全量同步

      1.建立連接:從庫會和主庫建立連接,從庫執行 replicaof 并發送 psync 命令并告訴主庫即將進行同步,主庫確認回復后,主從庫間就開始同步了。

      2.主庫同步數據給從庫:master 執行 bgsave命令生成 RDB 文件,并將文件發送給從庫,同時主庫為每一個 slave 開辟一塊 replication buffer 緩沖區記錄從生成 RDB 文件開始收到的所有寫命令。從庫保存 RDB 并清空數據庫再加載 RDB 數據到內存中。

      3.發送 RDB 之后接收到的新寫命令到從庫:在生成 RDB 文件之后的寫操作并沒有記錄到剛剛的 RDB 文件中,為了保證主從庫數據的一致性,所以主庫會在內存中使用一個叫 replication buffer 記錄 RDB 文件生成后的所有寫操作。并將里面的數據發送到 slave。

      面試官:主從庫間的網絡斷了咋辦?斷開后要重新全量復制么?

      在 Redis 2.8 之前,如果主從庫在命令傳播時出現了網絡閃斷,那么,從庫就會和主庫重新進行一次全量復制,開銷非常大。

      從 Redis 2.8 開始,網絡斷了之后,主從庫會采用增量復制的方式繼續同步。

      增量復制:用于網絡中斷等情況后的復制,只將中斷期間主節點執行的寫命令發送給從節點,與全量復制相比更加高效。

      斷開重連增量復制的實現奧秘就是 repl_backlog_buffer 緩沖區,不管在什么時候 master 都會將寫指令操作記錄在 repl_backlog_buffer 中,因為內存有限, repl_backlog_buffer 是一個定長的環形數組,如果數組內容滿了,就會從頭開始覆蓋前面的內容。

      master 使用 master_repl_offset記錄自己寫到的位置偏移量,slave 則使用 slave_repl_offset記錄已經讀取到的偏移量。

      repl_backlog_buffer

      當主從斷開重連后,slave 會先發送 psync 命令給 master,同時將自己的 runID,slave_repl_offset發送給 master。

      master 只需要把 master_repl_offset與 slave_repl_offset之間的命令同步給從庫即可。

      增量復制執行流程如下圖:

      Redis增量復制

      面試官:那完成全量同步后,正常運行過程中如何同步數據呢?

      當主從庫完成了全量復制,它們之間就會一直維護一個網絡連接,主庫會通過這個連接將后續陸續收到的命令操作再同步給從庫,這個過程也稱為基于長連接的命令傳播,使用長連接的目的就是避免頻繁建立連接導致的開銷。

      哨兵原理連環問

      面試官:可以呀,知道這么多,你知道 哨兵集群原理么?

      哨兵是 Redis 的一種運行模式,它專注于對 Redis 實例(主節點、從節點)運行狀態的監控,并能夠在主節點發生故障時通過一系列的機制實現選主及主從切換,實現故障轉移,確保整個 Redis 系統的可用性。

      他的架構圖如下:

      Redis哨兵集群

      Redis哨兵具備的能力有如下幾個:

      監控:持續監控master 、slave是否處于預期工作狀態。

      自動切換主庫:當 Master 運行故障,哨兵啟動自動故障恢復流程:從 slave 中選擇一臺作為新 master。

      通知:讓 slave 執行 replicaof ,與新的 master 同步;并且通知客戶端與新 master 建立連接。

      面試官:哨兵之間是如何知道彼此的?

      哨兵與 master 建立通信,利用 master 提供發布/訂閱機制發布自己的信息,比如身高體重、是否單身、IP、端口……

      master 有一個 sentinel:hello 的專用通道,用于哨兵之間發布和訂閱消息。這就好比是 sentinel:hello 微信群,哨兵利用 master 建立的微信群發布自己的消息,同時關注其他哨兵發布的消息。

      面試官:哨兵之間雖然建立連接了,但是還需要和 slave 建立連接,不然沒法監控他們呀,如何知道 slave 并監控他們的?

      關鍵還是利用 master 來實現,哨兵向 master 發送 INFO 命令, master 掌門自然是知道自己門下所有的 salve 小弟的。所以 master 接收到命令后,便將 slave 列表告訴哨兵。

      哨兵根據 master 響應的 slave 名單信息與每一個 salve 建立連接,并且根據這個連接持續監控哨兵。

      INFO命令獲取slave信息

      Cluster集群連環炮

      面試官:除了哨兵以外,還有其他的高可用手段么?

      有 Cluster 集群實現高可用,哨兵集群監控的 Redis 集群是主從架構,無法橫向拓展。使用 Redis Cluster 集群,主要解決了大數據量存儲導致的各種慢問題,同時也便于橫向拓展。

      在面向百萬、千萬級別的用戶規模時,橫向擴展的 Redis 切片集群會是一個非常好的選擇。

      面試官:什么是 Cluster 集群?

      Redis 集群是一種分布式數據庫方案,集群通過分片(sharding)來進行數據管理(「分治思想」的一種實踐),并提供復制和故障轉移功能。

      將數據劃分為 16384 的 slots,每個節點負責一部分槽位。槽位的信息存儲于每個節點中。

      它是去中心化的,如圖所示,該集群由三個 Redis 節點組成,每個節點負責整個集群的一部分數據,每個節點負責的數據多少可能不一樣。

      Redis集群架構

      三個節點相互連接組成一個對等的集群,它們之間通過Gossip協議相互交互集群信息,最后每個節點都保存著其他節點的slots分配情況。

      面試官:哈希槽又是如何映射到Redis實例上呢?

      根據鍵值對的key,使用 CRC16 算法,計算出一個16 bit的值;

      將16 bit的值對16384執行取模,得到0 ~ 16383的數表示key對應的哈希槽。

      根據該槽信息定位到對應的實例。

      鍵值對數據、哈希槽、Redis實例之間的映射關系如下:

      數據、Slot與實例的映射

      面試官:Cluster 如何實現故障轉移?

      Redis 集群節點采用 Gossip 協議來廣播自己的狀態以及自己對整個集群認知的改變。比如一個節點發現某個節點失聯了 (PFail),它會將這條信息向整個集群廣播,其它節點也就可以收到這點失聯信息。

      如果一個節點收到了某個節點失聯的數量 (PFail Count) 已經達到了集群的大多數,就可以標記該節點為確定下線狀態 (Fail),然后向整個集群廣播,強迫其它節點也接收該節點已經下線的事實,并立即對該失聯節點進行主從切換。

      面試官:客戶端又怎么確定訪問的數據分布在哪個實例上呢?

      Redis 實例會將自己的哈希槽信息通過 Gossip 協議發送給集群中其他的實例,實現了哈希槽分配信息的擴散。

      這樣,集群中的每個實例都有所有哈希槽與實例之間的映射關系信息。

      當客戶端連接任何一個實例,實例就將哈希槽與實例的映射關系響應給客戶端,客戶端就會將哈希槽與實例映射信息緩存在本地。

      當客戶端請求時,會計算出鍵所對應的哈希槽,再通過本地緩存的哈希槽實例映射信息定位到數據所在實例上,再將請求發送給對應的實例。

      Redis客戶端定位數據所在節點

      面試官:什么是 Redis 重定向機制?

      哈希槽與實例之間的映射關系由于新增實例或者負載均衡重新分配導致改變了,客戶端將請求發送到實例上,這個實例沒有相應的數據,該 Redis 實例會告訴客戶端將請求發送到其他的實例上。

      Redis 通過 MOVED 錯誤和 ASK 錯誤告訴客戶端。

      MOVED

      MOVED 錯誤(負載均衡,數據已經遷移到其他實例上):當客戶端將一個鍵值對操作請求發送給某個實例,而這個鍵所在的槽并非由自己負責的時候,該實例會返回一個 MOVED 錯誤指引轉向正在負責該槽的節點。

      同時,客戶端還會更新本地緩存,將該 slot 與 Redis 實例對應關系更新正確。

      MOVED指令

      ASK

      如果某個 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錯誤

      注意:ASK 錯誤指令并不會更新客戶端緩存的哈希槽分配信息。

      最后

      我這邊整理了一份Redis數據庫資料文檔Java的系統化資料:(包括Java核心知識點、Spring系列全家桶、面試專題和21年最新的互聯網真題、電子書等)有需要的朋友可以關注公眾號【程序媛小琬】即可獲取。

      Redis 數據結構

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:關系型數據庫vs非關系型數據庫
      下一篇:數據中心運維管理方案(超詳細)
      相關文章
      亚洲AV无码XXX麻豆艾秋| 亚洲一区电影在线观看| 久久精品国产亚洲AV麻豆不卡| 亚洲永久在线观看| 亚洲人成电影青青在线播放| 亚洲综合久久成人69| 人人狠狠综合久久亚洲婷婷| 亚洲色成人WWW永久网站| 亚洲国产精品狼友中文久久久| 亚洲AV无码AV吞精久久| 亚洲国产成人无码AV在线| 亚洲人成电影网站色| 亚洲丶国产丶欧美一区二区三区| 亚洲欧美日韩中文二区| 亚洲欧美日韩综合久久久久| 亚洲色大成网站WWW国产| 亚洲人成自拍网站在线观看 | 国产黄色一级毛片亚洲黄片大全| 亚洲国产av玩弄放荡人妇 | 亚洲大成色www永久网址| 亚洲人成电影青青在线播放| 精品亚洲成在人线AV无码| 亚洲日产2021三区| 亚洲Av高清一区二区三区| 亚洲综合小说另类图片动图| 亚洲国产精品无码中文lv| 国产亚洲精品AAAA片APP| 国产亚洲人成在线影院| 亚洲乱码国产一区网址| 国产亚洲精品影视在线产品| 好看的亚洲黄色经典| 亚洲成在人天堂一区二区| 久久精品国产亚洲AV麻豆王友容 | 亚洲视频国产视频| 亚洲国产成人精品无码一区二区| 亚洲va成无码人在线观看| 亚洲综合无码无在线观看| 亚洲av中文无码乱人伦在线观看| 五月婷婷亚洲综合| 亚洲中文字幕在线观看| 亚洲AV日韩精品久久久久|