Redis知識網絡
作者:運維君莫笑
鏈接:https://www.zhihu.com/question/470465324/answer/2006650219
Redis為什么這么快?
-
根據官方數據。官方的基准程序測試,Redis 的 QPS 可以達到約 100000(每秒請求數)
-
基於內存實現 。Redis 是基於內存的數據庫,不論讀寫操作都是在內存上完成的,跟磁盤數據庫相比,完全吊打磁盤的速度。
-
高效的數據結構。MySQL為了提高檢索效率用的是B+樹數據結構。而Redis提供5種數據類型String、List、Hash、Set、SortedSet。

String:存儲數字的話,采用int類型的編碼,如果是非數字的話,采用 raw 編碼;
1、簡單動態字符串
這個名詞可能你不熟悉,換成 SDS肯定就知道了。 這是用來處理字符串的。了解 C 語言的都知道,它是有處理字符串方法的。而 Redis 就是 C 語言實現的,那為什么還要重復造輪子?我們從以下幾點來看:
(1)字符串長度處理
這個圖是字符串在 C 語言中的存儲方式,想要獲取 「Redis」的長度,需要從頭開始遍歷,直到遇到 '0' 為止。
Redis 中怎么操作呢?用一個 len 字段記錄當前字符串的長度。想要獲取長度只需要獲取 len 字段即可。你看,差距不言自明。前者遍歷的時間復雜度為 O(n),Redis 中 O(1) 就能拿到,速度明顯提升。
(2)內存重新分配
C 語言中涉及到修改字符串的時候會重新分配內存。修改地越頻繁,內存分配也就越頻繁。而內存分配是會消耗性能的,那么性能下降在所難免。
而 Redis 中會涉及到字符串頻繁的修改操作,這種內存分配方式顯然就不適合了。於是 SDS 實現了兩種優化策略:
- 空間預分配
對 SDS 修改及空間擴充時,除了分配所必須的空間外,還會額外分配未使用的空間。
具體分配規則是這樣的:SDS 修改后,len 長度小於 1M,那么將會額外分配與 len 相同長度的未使用空間。如果修改后長度大於 1M,那么將分配1M的使用空間。
- 惰性空間釋放
當然,有空間分配對應的就有空間釋放。
SDS 縮短時,並不會回收多余的內存空間,而是使用 free 字段將多出來的空間記錄下來。如果后續有變更操作,直接使用 free 中記錄的空間,減少了內存的分配。
(3)二進制安全
你已經知道了 Redis 可以存儲各種數據類型,那么二進制數據肯定也不例外。但二進制數據並不是規則的字符串格式,可能會包含一些特殊的字符,比如 '0' 等。
前面我們提到過,C 中字符串遇到 '0'會結束,那 '0' 之后的數據就讀取不上了。但在 SDS 中,是根據 len 長度來判斷字符串結束的。
看,二進制安全的問題就解決了。
List:字符串長度及元素個數小於一定范圍使用 ziplist 編碼,任意條件不滿足,則轉化為 linkedlist 編碼;
列表 List 更多是被當作隊列或棧來使用的。隊列和棧的特性一個先進先出,一個先進后出。雙端鏈表很好的支持了這些特性。
(1)前后節點
鏈表里每個節點都帶有兩個指針,prev 指向前節點,next 指向后節點。這樣在 時間復雜度為 O(1)內就能獲取到前后節點。
(2)頭尾節點
你可能注意到了,頭節點里有 head 和 tail 兩個參數,分別指向頭節點和尾節點。這樣的設計能夠對雙端節點的處理時間復雜度降至 O(1),對於隊列和棧來說再適合不過。同時鏈表迭代時從兩端都可以進行。
(3)鏈表長度
頭節點里同時還有一個參數 len,和上邊提到的 SDS 里類似,這里是用來記錄鏈表長度的。因此獲取鏈表長度時不用再遍歷整個鏈表,直接拿到 len 值就可以了,這個時間復雜度是 O(1)。
你看,這些特性都降低了 List 使用時的時間開銷。
Hash:hash 對象保存的鍵值對內的鍵和值字符串長度小於一定值及鍵值對;
Set:保存元素為整數及元素個數小於一定范圍使用 intset 編碼,任意條件不滿足,則使用 hashtable 編碼;
Zset:zset 對象中保存的元素個數小於及成員長度小於一定值使用 ziplist 編碼,任意條件不滿足,則使用 skiplist 編碼。
作為 Redis 中特有的數據結構-跳躍表,其在鏈表的基礎上增加了多級索引來提升查找效率。

這是跳躍表的簡單原理圖,每一層都有一條有序的鏈表,最底層的鏈表包含了所有的元素。這樣跳躍表就可以支持在 O(logN) 的時間復雜度里查找到對應的節點。
下面這張是跳表真實的存儲結構,和其它數據結構一樣,都在頭節點里記錄了相應的信息,減少了一些不必要的系統開銷。

- 建立連接:從庫會和主庫建立連接,從庫執行 replicaof 並發送 psync 命令並告訴主庫即將進行同步,主庫確認回復后,主從庫間就開始同步了。
- 主庫同步數據給從庫:master 執行
bgsave命令生成 RDB 文件,並將文件發送給從庫,同時主庫為每一個 slave 開辟一塊 replication buffer 緩沖區記錄從生成 RDB 文件開始收到的所有寫命令。從庫保存 RDB 並清空數據庫再加載 RDB 數據到內存中。 - 發送 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 系統的可用性。
哨兵之間是如何知道彼此的?
哨兵與 master 建立通信,利用 master 提供發布/訂閱機制發布自己的信息,比如身高體重、是否單身、IP、端口……
master 有一個 __sentinel__:hello 的專用通道,用於哨兵之間發布和訂閱消息。這就好比是 __sentinel__:hello 微信群,哨兵利用 master 建立的微信群發布自己的消息,同時關注其他哨兵發布的消息。
關鍵還是利用 master 來實現,哨兵向 master 發送 INFO 命令, master 掌門自然是知道自己門下所有的 salve 小弟的。所以 master 接收到命令后,便將 slave 列表告訴哨兵。
哨兵根據 master 響應的 slave 名單信息與每一個 salve 建立連接,並且根據這個連接持續監控哨兵。
Cluster 集群連環炮
Redis 集群是一種分布式數據庫方案,集群通過分片(sharding)來進行數據管理(「分治思想」的一種實踐),並提供復制和故障轉移功能。
將數據划分為 16384 的 slots,每個節點負責一部分槽位。槽位的信息存儲於每個節點中。
它是去中心化的,每個節點負責整個集群的一部分數據,每個節點負責的數據多少可能不一樣。
節點相互連接組成一個對等的集群,它們之間通過 Gossip協議相互交互集群信息,最后每個節點都保存着其他節點的 slots 分配情況
哈希槽又是如何映射到 Redis 實例上呢?
- 根據鍵值對的 key,使用 CRC16 算法,計算出一個 16 bit 的值;
- 將 16 bit 的值對 16384 執行取模,得到 0 ~ 16383 的數表示 key 對應的哈希槽。
- 根據該槽信息定位到對應的實例。
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 命令,接着發發送操作命令。
redis過期策略?
1.只對過期的key進行LRU算法
2.刪除LRU算法的key
3.隨機刪除即將過期的key
4.隨機刪除
5.刪除即將過期的key
6.永不過期,返回錯誤





