Redis 必知概念

zhzcc發表於2024-10-13

Redis 為什麼快

  • 基於記憶體實現:Redis 將資料儲存在記憶體中,讀寫操作不會受到磁碟 IO 速度限制;

    CPU 不是 Redis 的瓶頸,Redis 的瓶頸在於機器記憶體的大小或者網路頻寬

  • I/O多路複用模型的使用:Redis 執行緒不會阻塞在某一個特定的客戶端請求處理上;
    可以同時和多個客戶端連線並處理請求,從而提升了併發性

  • 採用單執行緒模型:Redis 的網路 IO 以及鍵值對指令讀寫是由一個執行緒來執行的;
    對於 Redis 的持久化、叢集資料同步、非同步刪除等都是其他執行緒執行的
    單執行緒避免了執行緒切換和竟態產生的消耗,對於服務端開發來說,鎖和執行緒切換 通常為效能累贅

  • 高效的資料結構:不同資料型別使用不同的資料結構得以提升速度

資料結構

資料型別

  • string 字串
  • list 列表
  • hash 雜湊
  • set 集合
  • zset 有序集合
image-20241010212152369
string list hash set zset
概念 1、可以儲存任意型別的資料,比如文字、數字、圖片或者序列化物件
2、一個 string 型別的鍵最大可以儲存 512 MB 的資料
1、一個有序的字串列表,ta 按照插入順序排序,並且支援在兩端插入或刪除元素
2、一個 list 型別的鍵最多可以儲存 2^32-1 個元素
1、一個鍵值對集合,ta 可以儲存多個欄位和值,類似於java 的 map 物件
2、一個 hash 型別的鍵最多可以儲存 2^32-1 個欄位
1、set 是一個無序的字串集合,ta 不允許元素重複
2、一個 set 型別的鍵最多可以儲存 2^32-1 個元素
1、redis 中的 zset 是一種有序集合型別,ta 可以儲存不重複的字串元素,並且給每個元素賦予一個排序權重值(score);redis 透過權重值來為集合中的元素進行從小到大的排序
2、zset 的成員是唯一的,但權重值可以重複
3、一個 zset 型別的鍵最多可以儲存 2^32-1 個元素
底層實現 string 型別的底層實現是 SDS, ta 是一個動態字串結構,由長度、空閒空間和位元組數據組三部分組成
SDS 有 3 中編碼型別:
1、embstr:佔用64 Bytes 的空間,儲存 44 Bytes 的資料
2、raw:儲存大於 44 Bytes 的資料
3、int:儲存整數型別
embstr 和 raw 儲存字串資料,int 儲存整型資料
redis3.2 以後,list 型別的底層實現只有一種結構:quicklist

分析:
1、在 Redis 3.2之前,list 使用的是 linkedlist 和 ziplist;在 Redis3.2-Redis7.0之間,list 使用的是 quickList,是 linkedlist 和 ziplist 的結合;在 Redis7.0 之後,list 使用的也是 quickList ,只不過將 ziplist 轉換為 listpack ,ta 是 listpack、linkedlist 結合版
2、ziplist(壓縮列表):當列表的元素個數小於 list-max-ziplist-entries 配置,同時列表中每個元素的值都小於 list-max-ziplist-value 配置時使用
3、linkedlist(連結串列):當列表型別無法滿足 ziplist 的條件時,Redis 會使用 linkedlist 作為列表的內部實現
hash 型別的底層實現有三種:
1、ziplist :壓縮列表,當 hash 達到一定的閾值時,會自動轉換為 hashtable 結構
2、listpack :緊湊列表,在 redis7.0 之後,listpack 正式取代 ziplist;同樣的,當 hash 達到一定的閾值時,會自動轉換為 hashtable 結構
3、hashtable :雜湊表,類似 map

分析:
1、ziplist (壓縮表):當雜湊型別元素小於 hash-maxx-ziplist-entries 配置,同時所有值都小於 hash-max-ziplist-value 配置時使用;
ziplist 使用更加緊湊的結構實現多個元素的連續儲存,在節省記憶體方面比 hashtable 更有優勢
2、hashtable (雜湊表):當雜湊型別無法滿足 ziplist 的條件時,Redis 會使用 hashtable 作為雜湊的內部實現;原因是 ziplist 的讀寫效率下降,而 hashtable 的讀寫的複雜度為 O(1)
set 型別的底層實現有兩種:
1、intset,整數集合
2、hashtable 雜湊表;雜湊表和 hash 型別的雜湊表相同,ta 將元素儲存在一個陣列中,並透過雜湊函式計算元素在陣列中的索引

分析:
1、在 Redis7.2 之前,set 使用的是 intset 和 hashtable;在 Redis7.2 之後,set 使用的是 intset、listpack、hashtable
2、intset(整數集合):當集合中的元素都是整數且元素個數小於 set-max-intset-entries 配置時使用
3、hashtable(雜湊表):當集合型別無法滿足 intset 的條件時,Redis 使用 hashtable 作為集合的內部實現
1、ziplist(redis7.0前)和 listpack(redis7.0後)
2、skiplist

分析:
1、當有序集合的元素個數小於 zset-max-ziplist-entries(預設為 128 個),並且每個元素成員的長度小於 zset-max-ziplist-value(預設為 64 位元組)時,使用壓縮列表作為有序集合的內部實現;
每個集合元素由兩個緊挨在一起的兩個壓縮列表節點組成,其中第一個節點儲存元素成員,第二個節點儲存元素的分支;
壓縮列表中的元素按照分數從小到大一次緊挨著排列,有效減少了記憶體空間的使用
2、當有序集合的元素大於等於 zset-max-ziplist-entries(預設為 128 個),或者每個元素成員的長度大於等於 zset-max-ziplist-value(預設為 64 位元組)時,使用跳躍表作為有序集合的內部實現;
在跳躍表中,所有元素按照從小到大的順序排序;
跳躍表的節點中的 object 指標指向元素成員的字串物件,score 儲存元素的分數;
透過跳躍表,Redis 可以快速d e 對有序集合進行分數範圍、排名等操作

3、當雜湊表中,為有序集合建立了一個從元素成員到元素分數的對映:鍵值對中的鍵指向元素成員的字串物件,鍵值對中的值儲存了元素的分數,透過雜湊表,Redis 可以快速查詢指定元素的分數;
雖然有序集合同時使用跳躍表和雜湊表,但是著兩種資料結構都是用指標共享元素的成員和分數,不會額外的記憶體浪費
應用場景 1、快取資料,提高訪問速度和降低資料庫壓力
2、計數器,利用 incr 和 decr 命令實現原子性的加減操作
3、分散式鎖,利用 setnx 命令實現互斥訪問
4、限流,利用 expire 命令實現時間視窗內的訪問控制
1、訊息佇列,利用 lpush 和 rpop 命令實現生產者消費者模式
2、最新訊息,利用 lpush 和 ltrim 命令實現固定長度的時間線
3、歷史記錄,利用 lpush 和 lrange 命令實現瀏覽記錄或者搜尋記錄
hash 型別的應用場景主要是儲存物件,比如:
1、使用者資訊,利用 hset 和 hget 命令實現物件屬性的增刪改查
2、購物車,利用 hincrby 命令實現商品數量的增減
3、配置資訊,利用 hmset 和 hmget 命令實現批次設定和獲取配置項
1、去重,利用 sadd 和 scard 命令實現元素的新增和計數
2、交集,並集,差集,利用 sinter,sunion 和 sdiff 命令實現集合間的運算
3、隨機抽取,利用 srandmember 命令實現隨機抽獎或者抽樣
1、排行榜,利用 zadd 和 zrange 命令實現分數的更新和排名的查詢
2、延時佇列,利用 zadd 和 zpopmin 命令實現任務的新增和執行,並且可以定期 de 獲取已經到期的任務
3、訪問統計,可以使用 zset 來儲存網站或者文章的訪問次數,並且可以按照訪問量進行排序和篩選

image-20241010214801846

為什麼加入 listpack?

在 redis7.2 之前,sds 型別的資料會直接放入到編碼結構為 hashtable 的set 中

  • 其中,sds 其實就是 redis 中的 string 型別

在 redis7.2 之後,sds 型別的資料,首先會使用 listpack 結構,當 set 達到一定的閾值時,才會自動轉換為 hashtable。新增 listpack 結構是為了提高記憶體利用率和操作效率,因為 hashtable 的空間開銷和碰撞機率都比較高

記憶體機制

記憶體回收策略

Redis 的記憶體回收機制主要表現為以下兩方面:

  • 刪除到達過期時間的鍵物件
  • 記憶體使用達到 Maxmemory 上限,觸發記憶體溢位控制策略

刪除過期物件:Redis 所有的鍵都可以設定過期屬性,內部儲存在過期字典中

  • 惰性刪除:當客戶端讀取帶有超時屬性鍵時,如果已經超過鍵設定的過期時間,將執行刪除操作,並返回空
  • 定時任務刪除:Redis 內部維護了一個定時任務,預設每秒執行 10 次

記憶體溢位策略

當 Redis 所有記憶體達到 Maxmemory 上限時會觸發相應的溢位策略:

name describe
noeviction 預設策略,不會刪除任何資料,拒絕所有寫入操作並返回客戶端錯誤資訊,此時 Redis 只響應讀操作
volatile-lru 根據 LRU 演算法,刪除設定了超時屬性的鍵
如果沒有可刪除的鍵物件,回退到 noeviction 策略
allkeys-lru 根據 lru 演算法刪除鍵,不管資料有沒有設定超時屬性
allkeys-random 隨機刪除所有鍵
volatile-random 隨機刪除過期鍵
volatile-ttl 根據鍵值物件的 ttl 屬性,刪除最近將要過期資料,如果沒有 ,回退到 noeviction 策略

優先使用 allkeys-lru 策略:業務資料中有明顯的冷熱資料區分,建議使用 allkeys-lru 策略

業務應用訪問頻率相差不大,沒有明顯的冷熱資料區分,建議使用 allkeys-random 策略

業務中有置頂的需求,比如置頂影片、新聞,可以使用 volatile-lru 策略

持久化

RDB 持久化

概覽

將記憶體中的資料生成快照儲存到磁碟裡面,儲存的檔案字尾是 .rdb

rdb 檔案是一個經過壓縮的二進位制檔案,當 Redis 重新啟動時,可以讀取 rdb 快照檔案恢復資料

其中,包括 rdbSave 和 rdbLoad 兩個函式

  • rdbSave 用於生成 RDB 檔案並儲存到磁碟
  • rdbLoad 用於將 RDB 檔案中的資料載入到記憶體中

RDB 檔案是一個單檔案的全量資料,適合資料的容災備份與恢復

  • 透過 RDB 檔案恢復資料庫耗時較短,通常 1G 的快照檔案載入到記憶體只需要 20s 左右

RDB 檔案生成方式

  1. 手動觸發快照生成,透過 SAVE 和 BGSAVE 命令
  • SAVE 是一個同步式的命令,ta 會阻塞 Redis 伺服器程序,直到 RDB 檔案建立完成為止
    • 在伺服器阻塞期間,伺服器不能處理任何其他的命令請求
  • BGSAVE 是一個非同步式的命令,會派生一個子程序,由子程序負責建立 RDB 檔案,伺服器程序(父程序)繼續處理客戶的命令
    • 基本過程
      • 客戶端發起 BGSAVE 命令,Redis 主程序判斷當前是否存在正在執行備份的子程序,如果存在則直接返回
      • 父程序 fork 一個子程序(fork 的過程中會造成阻塞的情況)
      • fork 建立的子程序開始根據父程序的記憶體資料生成臨時的快照檔案,然後替換原始檔
      • 子程序備份完畢後會向父程序傳送完成資訊
  1. 自動觸發儲存
    透過 save 選項設定多個儲存條件,只要其中任意一個條件被滿足,伺服器就會執行 BGSAVE 命令
    只要滿足以下 3 個條件中的任意一個,BGSAVE 命令就會被自動執行:
    • 伺服器在 900s 之內,對資料庫進行了至少 1次 修改
    • 伺服器在 300s 之內,對資料庫進行了至少 10次 修改
    • 伺服器在 60s 之內,對資料庫進行了至少 10000次 修改

AOF 持久化

概覽

AOF 會把 Redis 伺服器每次執行的寫命令記錄到一個日誌檔案中,當伺服器重啟時,再次執行 AOF 檔案中的命令來恢復資料

如果 Redis 伺服器開啟了 AOF 持久化,會優先使用 AOF 檔案來還原資料庫狀態

只有在 AOF 的持久化功能處於關閉狀態時,伺服器才會使用 RDB 檔案還原資料庫狀態

AOF 優先順序大於 RDB

執行流程

AOF 不需要設定任何觸發條件,對 Redis 伺服器的所有寫命令都會自動記錄到 AOF 檔案中

AOF 檔案的寫入流程可以分為 3個 步驟:

  1. 命令追加(append):將 Redis 執行的寫命令追加到 AOF 的快取區 aof_buf
  2. 檔案寫入(write)和檔案同步(fsync):AOF 根據對應的策略將 aof_buf 的資料同步到硬碟
  3. 檔案重寫(rewrite):定期對 AOF 進行重寫,從而實現對寫命令的壓縮

AOF 快取區的檔案同步策略

  • appendfysnc always:每執行一次命令儲存一次
    • 命令寫入 aof_buf 快取區後立即呼叫系統 fsync 函式同步到 AOF 檔案,fsync 操作完成後執行緒返回,整個過程是阻塞的
  • appendfysnc no:不儲存
    • 命令寫入 aof_buf 快取區呼叫系統 write 操作,不對 AOF 檔案做 fsync 同步
    • 同步由作業系統負責,通常同步週期為 30s
  • appendfysnc everysec:每秒鐘儲存一次
    • 命令寫入 aof_buf 快取區後呼叫系統 write 操作,write 完成後執行緒立刻返回,fsync 同步檔案操作由單獨的程序每秒呼叫一次
檔案同步策略 write 阻塞 fsync 阻塞 當機時的資料丟失量
always 阻塞 阻塞 最多隻丟失一個命令的資料
no 阻塞 不阻塞 作業系統最後一次對 AOF 文愛你 fsync 後的資料
everysec 阻塞 不阻塞 一般不超過 1s 的資料

檔案重寫

把對 AOF 檔案中的寫命令進行合併,壓縮檔案體積,同步到新的 AOF 檔案中,然後使用新的 AOF 檔案覆蓋舊的 AOF 檔案

觸發機制

  • 手動觸發:呼叫 bgrewriteaof 命令,執行與 bgsave 有些類似
  • 自動觸發
    • 根據 auto-aof-rewrite-min-size 和 auto-aof-rewrite-percentage 配置項,以及 aof_current_size 和 aof_base_size 的狀態確定觸發時機
    • auto-aof-rewrite-min-size:執行 AOF 重寫時,檔案的最小體積,預設值為 64MB
    • auto-aof-rewrite-percentage:執行 AOF 時,當前 AOF 大小(aof_current_size)和上一次重寫時 AOF 大小(aof_base_size) 的比值

重寫流程

  • 客戶端透過 bgrewriteaof 命令對 Redis 主程序發起 AOF 重寫請求
  • 主程序透過 fork 操作建立子程序,這個過程主程序是阻塞的
  • 主程序的 fork 操作完成後,繼續處理其他命令,把新的命令同時追加到 aof_buf 和 aof_rewrite_buf 緩衝區中
    • 在檔案重寫完成之前,主程序會繼續把命令追加到 aof_buf 緩衝區,這樣可以避免 AOF 重寫失敗造成資料丟失,保證原有的 AOF 檔案的正確性
    • 由於 fork 操作運用寫時複製技術,子程序只能共享 fork 操作時的記憶體資料,主程序會把新命令追加到一個 aof_rewrite_buf 緩衝區中,避免 AOF 重寫時丟失這部分資料
  • 子程序讀取 Redis 程序中的資料快照,生成寫入命令並按照命令合併規則批次寫入到新的 AOF 我呢間
  • 子程序寫完新的 AOF 的檔案後,向主程序發訊號(怎麼進行的訊號傳送????)
  • 主程序接收到子程序的訊號後,將 aof_rewrite_buf 緩衝區中的寫命令追加到 AOF 檔案
  • 主程序使用新的 AOF 檔案替換舊的 AOF 檔案,AOF 重寫過程完成

RDB&AOF

RDB的優缺點

  • 優點:

    • RDB 是一個壓縮過的非常緊湊的檔案,儲存著某個時間點的資料集,適合做資料的備份、災難恢復
    • 與 AOF 持久化相比,恢復大資料集會更快些
  • 缺點:

    • 資料安全性不入 AOF,儲存整個資料集是個重量級的過程,可能幾分鐘一次持久化,如果伺服器當機,可能丟失幾分鐘的資料
    • Redis 資料集較大時,fork 的子程序要完成快照會比較耗費 cpu 和時間

AOF 的優缺點

  • 優點:
    • 資料更完整,安全性更高,秒級資料丟失
    • AOF 檔案是一個只進行追加的命令檔案,且寫入操作是以 Redis 協議的格式儲存,內容是可讀的,適合誤刪緊急恢復
  • 缺點:
    • 對於相同的資料集,AOF 檔案的體積要遠大於 RDB 檔案,資料恢復也會比較慢

RDB&AOF 混合持久化

Redis 4.0 版本提供了一套基於 AOF-RDB 的混合持久化機制,保留了兩種持久化機制的優點

然後,重寫的 AOF 檔案由兩部分組成,一部分是 RDB 格式的頭資料,另一部分是 AOF 格式的尾部命令

在 Redis 伺服器啟動的時候:

  • 可以預先載入 AOF 檔案頭部全量的 RDB 資料
  • 然後再重放 AOF 檔案尾部增量的 AOF 命令,從而大大減少重啟過程中資料還原的時間

基本原理

redis 協議

RESP,是一種簡單的文字協議,用於在客戶端和伺服器之間操作和傳輸資料

RESP 協議描述了不同型別資料結構,並且定義了請求和響應之間如何以這些資料結構進行互動

單執行緒模式

Redis 的網路 IO 和鍵值對讀寫是由一個執行緒來完成的

Redis 在處理客戶端請求時包括獲取(讀)、解析、執行、內容返回(寫)等都由一個順序序列的主執行緒處理

由於 Redis 在處理命令的時候是單執行緒作業的,所以會有一個 Socket 佇列

  • 每一個到達 de 服務端命令來了之後不會立馬被執行,而是進入佇列,然後被執行緒的事件分發器逐個執行

image-20241012180832932

Redis 的其他功能,比如持久化、非同步刪除、叢集資料同步等,都是交由額外執行緒執行的

哨兵模式

概覽

Redis 的主從複製模式下,一旦主節點由於故障不能提供服務,需要手動將從節點晉升為主節點,同時還需要通知客戶端更新主節點地址

Redis 2.8 以後提供了 Redis Sentinel 哨兵機制來解決這個問題

(註冊中心 心跳機制)

Redis Sentinel 的主要功能

Sentinel 是一個管理多個 Redis 例項的工具,ta 可以實現對 Redis 的監控、通知、自動故障轉移

  • 監控:Sentinel 會不斷檢查主伺服器和從伺服器是否正常執行
  • 通知:當被監控的某一個 Redis 伺服器出現問題,Sentinel 透過 API 指令碼向管理員或其他的應用程式傳送通知
  • 自動故障轉移:當主節點不能正常工作時,Sentinel 會開始一次自動的故障轉移操作,ta 會將與失效主節點是主從關係的其中一個從節點升級為新的主節點,並且將其他的從節點指向新的主節點
  • 配置提供者:在 Redis Sentinel 模式下,客戶端應用在初始化時連線的是 Sentinel 節點集合,從中獲取主節點的資訊

主觀下線和客觀下線

預設情況下,每個 Sentinel 節點會以每秒一次的頻率對 Redis 節點和其 ta 的 Sentinel 節點傳送 PING 命令,並透過節點的回覆來判斷節點是否線上

主觀下線

  • 適用於所有主節點和從節點
  • 如果 down-after-millisenconds 毫秒內,Sentinel 沒有收到目標節點的有效回覆,則會判定該節點為主觀下線

客觀下線

  • 只適用於主節點
  • 如果主節點出現故障,Sentinel 節點會透過 sentinel is-master-down-by-addr 命令,向其他 Sentinel 節點詢問對該節點的狀態判斷
  • 如果超過 quorum 個數的節點判定主節點不可達,則該 Sentinel 節點會判斷主節點為客觀下線

工作原理

  1. 每個 Sentinel 以每秒鐘一次的評率,向 ta 所知的主伺服器、從伺服器以及其 ta Sentinel 例項傳送一個 PING 命令
  2. 如果例項距離最後一次有效回覆 PING 命令的時間超過 down-after-millisenconds 所指定的值,這個例項會被 Sentinel 標記為主觀下線
  3. 如果一個主伺服器被標記為主觀下線,並且有足夠的 Sentinel 在指定的時間範圍內同意這一判斷,那麼這個主伺服器被標記為客觀下線
  4. Sentinel 和其 ta Sentinel 協商主節點的狀態,如果主節點處於 SDOWN 狀態,則投票自動選出新的主節點,將剩餘的從節點指向新的主節點進行資料複製

image-20241013134904978

腦裂問題

在 Redis 哨兵模式或叢集模式中,由於網路原因,導致主節點(Master)與哨兵(Sentinel)和從節點(Slave)的通訊中斷。此時,哨兵就會誤以為主節點已當機,就會 在從節點中選舉出一個新的主節點,此時 Redis 的叢集中就會出現了兩個主節點的問題。

腦裂問題影響

Redis 腦裂問題會導致資料丟失

當舊的 Master 變為 Slave 之後 de 執行流程如下:

  • Slave(舊Master)會向 Master(新)申請全量資料
  • Master 會透過 Bgsave 的 方式生成當前 RDB 快照,並且將 RDB 傳送給 Slave
  • Slave 拿到 RDB 之後,先進行 Flush 清空當前資料(此時第四步舊客戶端給 ta 的傳送的資料就丟失了)
  • 之後再載入 RDB 資料,初始化自己當前的資料

在執行到第三步時,原客戶端在舊 Master 寫入的資料就丟失了

解決腦裂問題

Redis 提供了一下兩個配置,透過一下兩個配置可以儘可能的避免腦裂導致資料丟失的問題:

  • min-slaves-to-write:與主節點通訊的從節點數量必須大於等於該值主節點,否知主節點拒絕寫入
  • min-slaves-max-lag:主節點與從節點通訊 de ACK 訊息延遲必須小於該值,否則主節點拒絕寫入

這兩個配置項必須同時滿足,不然主節點拒絕寫入

叢集

概覽

Redis 3.0 之前,使用哨兵(Sentinel)機制來監控各個節點之間的狀態

在 3.0 版本正式推出,解決了 Redis 在分散式方面的需求

資料分割槽

Redis Cluster 採用虛擬槽分割槽,所有的鍵根據雜湊函式對映到 0~16383 整數槽內

  • 計算公式:slot = CRC16(KEY) & 16383
  • 每個節點負責維護一部分槽以及槽所對映的鍵值資料

為什麼 Redis 叢集的最大槽數是 16384 個

2^14 = 16384、 2^16 = 65536

  • 如果槽位是 65536 個,傳送心跳資訊的訊息頭是 65536 / 8 / 1024 = 8k
  • 如果槽位是 16384 個,傳送心跳資訊的訊息頭是 16384 / 8 / 1024 = 2k

因為 Redis 每秒都會傳送一定資料量的心跳包,如果訊息頭是 8k,有些太大了,浪費網路資源

Redis 的叢集主節點數量一般不會超過 1000 個

  • 叢集中節點越多,心跳包的訊息體內的資料就越多,如果節點過多,也會造成網路擁堵

so,Redis Cluster 的節點建議不超過 1000 個,對於節點數在 1000 個以內的 Redis Cluster,16384 個槽位完全夠用

叢集的功能限制

  • key 批次操作支援有限:類似 mset、mget 操作,目前支援對具有相同 slot 值 key 執行批次操作;對於對映為不同 slot 值的 key 由於執行 mset、mget等操作可能存在於多個節點上,因此不被支援
  • key 事務操作支援有限:只支援多 key 在同一節點上的事務操作,當多個 key 分佈在不同的節點上時,無法使用事務功能;單機下 Redis 可以支援 16個資料庫(db0~db15),叢集模式下只能使用一個資料庫空間,即 db0

相關文章