Redis核心技術筆記21-25

IT小馬發表於2023-01-16

21 緩衝區

緩衝區就是用一塊記憶體空間來暫時存放命令資料,以免出現因為資料和命令的處理速度慢於傳送速度而導致的資料丟失和效能問題。

緩衝器溢位:
緩衝區空間有限,當寫入速度持續大於讀取速度,佔用記憶體超出設定上限時,發生緩衝區溢位。

使用場景:

  1. 在客戶端和服務端通訊時,暫存客戶端命令資料,和服務端返回結果
  2. 在主從同步時,用來暫存主節點接收的寫命令和資料

客戶端

伺服器端給每個連線的客戶端都設定了一個輸入緩衝區和輸出緩衝區,我們稱之為客戶端輸入緩衝區和輸出緩衝區。

  1. 輸入緩衝區會先把客戶端傳送過來的命令暫存起來,Redis 主執行緒再從輸入緩衝區中讀取命令,進行處理。
    2.當 Redis 主執行緒處理完資料後,會把結果寫入到輸出緩衝區,再透過輸出緩衝區返回給客戶端。

輸入緩衝區

Redis 的客戶端輸入緩衝區大小的上限閾值,在程式碼中就設定為了 1GB,無法調整。

溢位原因:

  1. 寫入了 bigkey,比如一下子寫入了多個百萬級別的集合型別資料;
  2. 伺服器端處理請求的速度過慢,例如,Redis 主執行緒出現了間歇性阻塞,無法及時處理正常傳送的請求,導致客戶端傳送的請求在緩衝區越積越多。

檢視使用情況:

CLIENT LIST

id=5 addr=127.0.0.1:50487 fd=9 name= age=4 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client
  • qbuf,表示輸入緩衝區已經使用的大小。
  • qbuf-free,表示輸入緩衝區尚未使用的大小。

輸出緩衝區

Redis 為每個客戶端設定的輸出緩衝區也包括兩部分:

  • 一部分,是一個大小為 16KB 的固定緩衝空間,用來暫存 OK 響應和出錯資訊;
  • 另一部分,是一個可以動態增加的緩衝空間,用來暫存大小可變的響應結果。

溢位原因:

  1. 伺服器端返回 bigkey 的大量結果;
  2. 執行了 MONITOR 命令;
  3. 緩衝區大小設定得不合理。

MONITOR 命令是用來監測 Redis 執行的,輸出會持續佔用緩衝區,不要在生產環境使用:

MONITOR

設定緩衝區大小:client-output-buffer-limit 配置項

client-output-buffer-limit normal 0 0 0

normal 表示當前設定的是普通客戶端,
第 1 個 0 設定的是緩衝區大小限制,
第 2 個 0 和第 3 個 0 分別表示緩衝區持續寫入量限制和持續寫入時間限制。

客戶端型別:

  • 常規和 Redis 伺服器端進行讀寫命令互動的普通客戶端
  • 訂閱了 Redis 頻道的訂閱客戶端。

對於普通客戶端來說,它每傳送完一個請求,會等到請求結果返回後,再傳送下一個請求,這種傳送方式稱為阻塞式傳送。
所以,我們通常把普通客戶端的緩衝區大小限制,以及持續寫入量限制、持續寫入時間限制都設定為 0,也就是不做限制。

訂閱客戶端和伺服器間的訊息傳送方式,不屬於阻塞式傳送。
因此,我們會給訂閱客戶端設定緩衝區大小限制、緩衝區持續寫入量限制,以及持續寫入時間限制,可以在 Redis 配置檔案中這樣設定:

client-output-buffer-limit pubsub 8mb 2mb 60

pubsub 參數列示當前是對訂閱客戶端進行設定;8mb 表示輸出緩衝區的大小上限為 8MB,一旦實際佔用的緩衝區大小要超過 8MB,伺服器端就會直接關閉客戶端的連線;2mb 和 60 表示,如果連續 60 秒內對輸出緩衝區的寫入量超過 2MB 的話,伺服器端也會關閉客戶端連線。

主從叢集緩衝區

主從節點的全量和增量複製都會用到緩衝區。

全量複製

在全量複製過程中,主節點在向從節點傳輸 RDB 檔案的同時,會繼續接收客戶端傳送的寫命令請求。這些寫命令就會先儲存在複製緩衝區中,等 RDB 檔案傳輸完成後,再傳送給從節點去執行。主節點上會為每個從節點都維護一個複製緩衝區,來保證主從節點間的資料同步。

問題:
在全量複製時,從節點接收和載入 RDB 較慢,同時主節點接收到了大量的寫命令,寫命令在複製緩衝區中就會越積越多,最終導致溢位。
複製緩衝區一旦發生溢位,主節點也會直接關閉和從節點進行復制操作的連線,導致全量複製失敗。

最佳化:

  1. 主節點的資料量控制在 2~4GB,這樣可以讓全量同步執行得更快些,避免複製緩衝區累積過多命令。
  2. 控制和主節點連線的從節點個數,不要使用大規模的主從叢集。

設定複製緩衝區:

config set client-output-buffer-limit slave 512mb 128mb 60

slave 參數列明該配置項是針對複製緩衝區的。512mb 代表將緩衝區大小的上限設定為 512MB;128mb 和 60 代表的設定是,如果連續 60 秒內的寫入量超過 128MB 的話,也會觸發緩衝區溢位。

增量複製

增量複製時使用的緩衝區,稱為複製積壓緩衝區,英文名repl_backlog_buffer。

主節點在把接收到的寫命令同步給從節點時,同時會把這些寫命令寫入複製積壓緩衝區。一旦從節點發生網路閃斷,再次和主節點恢復連線後,從節點就會從複製積壓緩衝區中,讀取斷連期間主節點接收到的寫命令,進而進行增量同步。

複製積壓緩衝區是一個大小有限的環形緩衝區。當主節點把複製積壓緩衝區寫滿後,會覆蓋緩衝區中的舊命令資料。如果從節點還沒有同步這些舊命令資料,就會造成主從節點間重新開始執行全量複製。

最佳化:
為了應對複製積壓緩衝區的溢位問題,我們可以調整複製積壓緩衝區的大小,也就是設定 repl_backlog_size 這個引數的值。

22 常見問題答疑

  1. 如何使用慢查詢日誌和 latency monitor 排查執行慢的操作?
    配置引數:
  2. slowlog-log-slower-than:慢查詢日誌對執行時間大於多少微秒的命令進行記錄。
  3. slowlog-max-len:慢查詢日誌最多能記錄多少條命令記錄,預設128,建議1000。

檢視慢日誌:

SLOWLOG GET 1
1) 1) (integer) 33           //每條日誌的唯一ID編號
   2) (integer) 1600990583   //命令執行時的時間戳
   3) (integer) 20906        //命令執行的時長,單位是微秒
   4) 1) "keys"               //具體的執行命令和引數
      2) "abc*"
   5) "127.0.0.1:54793"      //客戶端的IP和埠號
   6) ""                     //客戶端的名稱,此處為空

設定峰值延遲(1000毫秒):

config set latency-monitor-threshold 1000

檢視峰值延遲:

latency latest
1) 1) "command"
   2) (integer) 1600991500    //命令執行的時間戳
   3) (integer) 2500           //最近的超過閾值的延遲
   4) (integer) 10100          //最大的超過閾值的延遲
  1. 如何排查Redis的bigkey?

    ./redis-cli  --bigkeys
    
    -------- summary -------
    Sampled 32 keys in the keyspace!
    Total key length in bytes is 184 (avg len 5.75)
    
    //統計每種資料型別中元素個數最多的bigkey
    Biggest   list found 'product1' has 8 items
    Biggest   hash found 'dtemp' has 5 fields
    Biggest string found 'page2' has 28 bytes
    Biggest stream found 'mqstream' has 4 entries
    Biggest    set found 'userid' has 5 members
    Biggest   zset found 'device:temperature' has 6 members
    
  2. lists with 15 items (12.50% of keys, avg size 3.75)
  3. hashs with 14 fields (15.62% of keys, avg size 2.80)
  4. strings with 68 bytes (31.25% of keys, avg size 6.80)
  5. streams with 4 entries (03.12% of keys, avg size 4.00)
  6. sets with 19 members (21.88% of keys, avg size 2.71)
  7. zsets with 17 members (15.62% of keys, avg size 3.40)

    注意:掃描資料庫會對Redis效能產生影響,建議在從節點執行,使用-i控制掃描間隔。

    ./redis-cli --bigkeys -i 0.1

  8. 只能返回每種型別最大的bigkey,無法得到多個
  9. 只能統計集合元素個數,而不是實際佔用記憶體

掃描工具:

  1. 使用 SCAN 命令對資料庫掃描,然後用 TYPE 命令獲取返回的每一個 key 的型別。
  2. 對於 String 型別,使用 STRLEN 命令獲取字串的長度,也就是佔用的記憶體空間位元組數。
  3. 集合型別獲取元素個數,乘以平均大小就是佔用記憶體大小。

    • List 型別:LLEN 命令;
    • Hash 型別:HLEN 命令;
    • Set 型別:SCARD 命令;
    • Sorted Set 型別:ZCARD 命令;
  4. 把每種資料型別佔用記憶體前N位的key統計出來,就是bigkey

23 旁路快取

快取特徵:

  1. 在分層系統中,資料暫存在快速子系統中有助於加速訪問。
  2. 快取容量有限,快取寫滿時,資料需要被淘汰。

計算機系統快取(兩種):

  • LLC:CPU中的末級快取,用來快取記憶體中的資料,避免每次從記憶體中存取資料;
  • page cache:記憶體中的告訴頁快取,用來快取磁碟中的資料,避免每次從磁碟中存取資料。

構建計算機硬體系統時,已經把 LLC 和 page cache 放在了應用程式的資料訪問路徑上,應用程式訪問資料時直接就能用上快取。

旁路快取

在應用程式中新增快取邏輯處理的程式碼,讀取快取、讀取資料庫和更新快取的操作都在應用程式中來完成。

旁路快取是一個獨立的系統,我們可以單獨對 Redis 快取進行擴容或效能最佳化。而且,只要保持操作介面不變,我們在應用程式中增加的程式碼就不用再修改了。

快取型別

Redis 快取的兩種型別:只讀快取和讀寫快取。

  • 只讀快取能加速讀請求
  • 讀寫快取可以同時加速讀寫請求,有兩種資料寫回策略:

    • 同步直寫保證資料可靠性
    • 非同步回寫低延遲

只讀快取

當 Redis 用作只讀快取時,應用要讀取資料的話,會先呼叫 Redis GET 介面,查詢資料是否存在。而所有的資料寫請求,會直接發往後端的資料庫,在資料庫中增刪改。

對於刪改的資料來說,如果 Redis 已經快取了相應的資料,應用需要把這些快取的資料刪除,Redis 中就沒有這些資料了。當應用再次讀取這些資料時,會發生快取缺失,應用會把這些資料從資料庫中讀出來,並寫到快取中。

只讀快取直接在資料庫中更新資料的好處是,所有最新的資料都在資料庫中,而資料庫是提供資料可靠性保障的,這些資料不會有丟失的風險。當我們需要快取圖片、短影片這些使用者只讀的資料時,就可以使用只讀快取這個型別了。

讀寫快取

讀請求會傳送到快取進行處理(直接在快取中查詢資料是否存在),所有的寫請求也會傳送到快取,在快取中直接對資料進行增刪改操作。

在使用讀寫快取時,最新的資料是在 Redis 中,而 Redis 是記憶體資料庫,一旦出現掉電或當機,記憶體中的資料就會丟失。這也就是說,應用的最新資料可能會丟失,給應用業務帶來風險。

寫回策略:同步直寫,非同步寫回。

同步直寫:
寫請求發給快取的同時,也會發給後端資料庫進行處理,等到快取和資料庫都寫完資料,才給客戶端返回。
優點:即使快取當機或發生故障,最新的資料仍然儲存在資料庫中,這就提供了資料可靠性保證。
缺點:降低訪問效能。

非同步寫回:
所有寫請求都先在快取中處理。等到這些增改的資料要被從快取中淘汰出來時,快取將它們寫回後端資料庫。
優點:效能高。
缺點:可能會丟失。

舉例:在商品大促的場景中,商品的庫存資訊會一直被修改。如果每次修改都需到資料庫中處理,就會拖慢整個應用,此時,我們通常會選擇讀寫快取的模式。

24 快取淘汰

建議把快取容量設定為總資料量的 15% 到 30%,兼顧訪問效能和記憶體空間開銷。

確定了快取最大容量,就可以設定快取的大小了:

CONFIG SET maxmemory 4gb

淘汰策略(8種)

不進行資料淘汰:noeviction(預設)
過期淘汰:volatile-random、volatile-ttl、volatile-lru、volatile-lfu
全範圍淘汰:allkeys-lru、allkeys-random、allkeys-lfu

noeviction:快取寫滿,再請求直接返回錯誤。
volatile-ttl:根據過期時間先後進行刪除
volatile-random、allkeys-random:隨機演算法刪除
volatile-lru、allkeys-lru:LRU演算法刪除
volatile-lfu、allkeys-lfu:LFU演算法刪除

LRU演算法

Least Recently Used
把所有的資料組織成一個連結串列,連結串列的頭和尾分別表示 MRU 端和 LRU 端,分別代表最近最常使用的資料和最近最不常用的資料。

缺點:需要用連結串列管理所有的快取資料,這會帶來額外的空間開銷。而且,當有資料被訪問時,需要在連結串列上把該資料移動到 MRU 端,如果有大量資料被訪問,就會帶來很多連結串列移動操作,會很耗時,進而會降低 Redis 快取效能。

Redis最佳化:
Redis 預設會記錄每個資料的最近一次訪問的時間戳(由鍵值對資料結構 RedisObject 中的 lru 欄位記錄)。然後,Redis 在決定淘汰的資料時,第一次會隨機選出 N 個資料,把它們作為一個候選集合。接下來,Redis 會比較這 N 個資料的 lru 欄位,把 lru 欄位值最小的資料從快取中淘汰出去。

Redis 提供了一個配置引數 maxmemory-samples,這個引數就是 Redis 選出的資料個數 N。

CONFIG SET maxmemory-samples 100

當需要再次淘汰資料時,Redis 需要挑選資料進入第一次淘汰時建立的候選集合。這兒的挑選標準是:能進入候選集合的資料的 lru 欄位值必須小於候選集合中最小的 lru 值。候選集是一個連結串列,當有新資料進入候選資料集後,如果候選資料集中的資料個數達到了 maxmemory-samples,Redis 就把候選資料集中 lru 欄位值最小的資料淘汰出去。

使用建議:

  • 資料有明顯的冷熱區分,優先使用 allkeys-lru 策略。
  • 資料訪問頻率相差不到,建議allkeys-random
  • 有置頂需求的資料,使用volatile-lru,同時置頂資料不設定過期時間,其他資料過期後進行LRU篩選

處理淘汰資料

一旦被淘汰的資料選定後,如果這個資料是乾淨資料,那麼我們就直接刪除;如果這個資料是髒資料,我們需要把它寫回資料庫

乾淨資料和髒資料的區別就在於,和最初從後端資料庫裡讀取時的值相比,有沒有被修改過。乾淨資料一直沒有被修改,所以後端資料庫裡的資料也是最新值。在替換時,它可以被直接刪除。

對於 Redis 來說,它決定了被淘汰的資料後,會把它們刪除。即使淘汰的資料是髒資料,Redis 也不會把它們寫回資料庫。

所以,我們在使用 Redis 快取時,如果資料被修改了,需要在資料修改時就將它寫回資料庫。否則,這個髒資料被淘汰時,會被 Redis 刪除,而資料庫裡也沒有最新的資料了。

25 快取異常

快取中有資料,那麼,快取的資料值需要和資料庫中的值相同;快取中本身沒有資料,那麼,資料庫中的值必須是最新值。不符合這兩種情況的,就屬於快取和資料庫的資料不一致問題了。

讀寫快取:

  • 對於讀寫快取來說,要想保證快取和資料庫中的資料一致,就要採用同步直寫策略。同時更新快取和資料庫,需要我們在業務應用中使用事務機制,來保證快取和資料庫的更新具有原子性。
  • 對於資料一直性要求不高的資料,如商品的非關鍵屬性,可使用非同步寫回策略。

只讀快取:
如果有資料新增,會直接寫入資料庫;而有資料刪改時,就需要把只讀快取中的資料標記為無效。

重試機制

把要刪除的快取值或者是要更新的資料庫值暫存到訊息佇列中(例如使用 Kafka 訊息佇列)。當應用沒有能夠成功地刪除快取值或者是更新資料庫值時,可以從訊息佇列中重新讀取這些值,然後再次進行刪除或更新。

如果能夠成功地刪除或更新,我們就要把這些值從訊息佇列中去除,以免重複操作,此時,我們也可以保證資料庫和快取的資料一致了。否則的話,我們還需要再次進行重試。如果重試超過的一定次數,還是沒有成功,我們就需要向業務層傳送報錯資訊了。

分散式鎖

對於寫請求,需要配合分散式鎖使用。

寫請求進來時,針對同一個資源的修改操作,先加分散式鎖,這樣同一時間只允許一個執行緒去更新資料庫和快取,沒有拿到鎖的執行緒把操作放入到佇列中,延時處理。用這種方式保證多個執行緒操作同一資源的順序性,以此保證一致性。

小結

快取和資料庫的資料不一致一般是由兩個原因導致的,我給你提供了相應的解決方案。

  1. 刪除快取值或更新資料庫失敗而導致資料不一致,你可以使用重試機制確保刪除或更新操作成功。
  2. 在刪除快取值、更新資料庫的這兩步操作中,有其他執行緒的併發操作,應對方案是分散式鎖。

總結:
使用讀寫快取同時運算元據庫和快取時,因為其中一個操作失敗導致不一致的問題,可以透過訊息佇列重試來解決。

而在併發的場景下,讀+寫併發對業務沒有影響或者影響較小,而寫+寫併發時需要配合分散式鎖的使用,才能保證快取和資料庫的一致性。

參考

快取和一致性問題

相關文章