Redis知識網絡


Redis知識網絡

作者:運維君莫笑
鏈接:https://www.zhihu.com/question/470465324/answer/2006650219

Redis為什么這么快?

  • 根據官方數據。官方的基准程序測試,Redis 的 QPS 可以達到約 100000(每秒請求數)

  • 基於內存實現 。Redis 是基於內存的數據庫,不論讀寫操作都是在內存上完成的,跟磁盤數據庫相比,完全吊打磁盤的速度。

  • 高效的數據結構。MySQL為了提高檢索效率用的是B+樹數據結構。而Redis提供5種數據類型String、List、Hash、Set、SortedSet。

img

String:存儲數字的話,采用int類型的編碼,如果是非數字的話,采用 raw 編碼;

1、簡單動態字符串

這個名詞可能你不熟悉,換成 SDS肯定就知道了。 這是用來處理字符串的。了解 C 語言的都知道,它是有處理字符串方法的。而 Redis 就是 C 語言實現的,那為什么還要重復造輪子?我們從以下幾點來看:

(1)字符串長度處理

img

這個圖是字符串在 C 語言中的存儲方式,想要獲取 「Redis」的長度,需要從頭開始遍歷,直到遇到 '0' 為止。

img

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 更多是被當作隊列或棧來使用的。隊列和棧的特性一個先進先出,一個先進后出。雙端鏈表很好的支持了這些特性。

img

(1)前后節點

img

鏈表里每個節點都帶有兩個指針,prev 指向前節點,next 指向后節點。這樣在 時間復雜度為 O(1)內就能獲取到前后節點。

(2)頭尾節點

img

你可能注意到了,頭節點里有 head 和 tail 兩個參數,分別指向頭節點和尾節點。這樣的設計能夠對雙端節點的處理時間復雜度降至 O(1),對於隊列和棧來說再適合不過。同時鏈表迭代時從兩端都可以進行。

(3)鏈表長度

頭節點里同時還有一個參數 len,和上邊提到的 SDS 里類似,這里是用來記錄鏈表長度的。因此獲取鏈表長度時不用再遍歷整個鏈表,直接拿到 len 值就可以了,這個時間復雜度是 O(1)。

你看,這些特性都降低了 List 使用時的時間開銷。

Hash:hash 對象保存的鍵值對內的鍵和值字符串長度小於一定值及鍵值對;

Set:保存元素為整數及元素個數小於一定范圍使用 intset 編碼,任意條件不滿足,則使用 hashtable 編碼;

Zset:zset 對象中保存的元素個數小於及成員長度小於一定值使用 ziplist 編碼,任意條件不滿足,則使用 skiplist 編碼。

作為 Redis 中特有的數據結構-跳躍表,其在鏈表的基礎上增加了多級索引來提升查找效率。

img

這是跳躍表的簡單原理圖,每一層都有一條有序的鏈表,最底層的鏈表包含了所有的元素。這樣跳躍表就可以支持在 O(logN) 的時間復雜度里查找到對應的節點。

下面這張是跳表真實的存儲結構,和其它數據結構一樣,都在頭節點里記錄了相應的信息,減少了一些不必要的系統開銷。

img![i

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

    1. 不會因為線程創建導致的性能消耗;
    2. 避免上下文切換引起的 CPU 消耗,沒有多線程切換的開銷;
    3. 避免了線程之間的競爭問題,比如添加鎖、釋放鎖、死鎖等,不需要考慮各種鎖問題。
    4. 代碼更清晰,處理邏輯簡單。
  • I/O多路服用模型

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

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

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 內存快照?

RDB就是把某一刻的數據以文件的形式拍下來,寫到磁盤上。這個快照文件叫做 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 持久化功能的效率和安全性。

  • always:同步寫回,寫指令執行完畢立馬將 aof_buf緩沖區中的內容刷寫到 AOF 文件。
  • everysec:每秒寫回,寫指令執行完,日志只會寫到 AOF 文件緩沖區,每隔一秒就把緩沖區內容同步到磁盤。
  • no: 操作系統控制,寫執行執行完畢,把日志寫到 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 主從架構數據同步 主從之間數據如何保證一致性?

  • 讀操作:主、從庫都可以執行;
  • 寫操作:主庫先執行,之后將寫操作同步到從庫;

主從復制還有其他作用么?

  1. 故障恢復:當主節點宕機,其他節點依然可以提供服務;
  2. 負載均衡:Master 節點提供寫服務,Slave 節點提供讀服務,分擔壓力;
  3. 高可用基石:是哨兵和 cluster 實施的基礎,是高可用的基石。

主從復制如何實現的?

  1. 第一次主從庫全量復制;
  2. 主從正常運行期間的同步;
  3. 主從庫間網絡斷開重連同步。

第一次同步怎么實現?

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

Image

  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記錄已經讀取到的偏移量。

Imagerepl_backlog_buffer

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

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

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

Image

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

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

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

哨兵是 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 實例上呢?

  1. 根據鍵值對的 key,使用 CRC16 算法,計算出一個 16 bit 的值;
  2. 將 16 bit 的值對 16384 執行取模,得到 0 ~ 16383 的數表示 key 對應的哈希槽。
  3. 根據該槽信息定位到對應的實例。

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.永不過期,返回錯誤


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM