《Redis深度歷險》讀書筆記

1. Redis 的用途

  1. 緩存
  2. 分佈式鎖
  3. 排行榜

2. 字符串的實現

當字符串長度小於 1M 時,擴容都是加倍現有的空間,如果超過 1M,擴容時一次只會多擴 1M 的空間,字符串最大長度爲 512M。

如果是個整數,那麼可以對其增減,範圍是signed long的最大最小值,超過會報錯。

如果字符串長度小於44時且不是整數類型或整數大於signed long類型時,會使用embstr編碼,因爲只需要一次malloc,減少內存碎片。

3. 列表list

3.1. quicklist

在列表元素較少的情況下會使用一塊連續的內存存儲,這個結構是 ziplist,它將所有的元素緊挨着一起存儲,分配的是一塊連續的內存。。

列表元素較多時採用quicklist,因爲普通的鏈表需要的附加指針空間太大,會比較浪費空間,而且會加重內存的碎片化。 將鏈表和 ziplist 結合起來組成了 quicklist。也就是將多個ziplist 使用雙向指針串起來使用。這樣既滿足了快速的插入刪除性能,又不會出現太大的空間冗餘。

quicklist就相當於塊狀鏈表一樣,將多個ziplist串起來。

struct ziplist {
 ...
}
struct ziplist_compressed {
    int32 size;
    byte[] compressed_data;
}
struct quicklistNode {
    quicklistNode* prev;
    quicklistNode* next;
    ziplist* zl; // 指向壓縮列表
    int32 size; // ziplist 的字節總數
    int16 count; // ziplist 中的元素數量
    int2 encoding; // 存儲形式 2bit,原生字節數組還是 LZF 壓縮存儲
    ...
}
struct quicklist {
    quicklistNode* head;
    quicklistNode* tail;
    long count; // 元素總數
    int nodes; // ziplist 節點的個數
    int compressDepth; // LZF 算法壓縮深度
    ...
}

quicklist 內部默認單個 ziplist 長度爲 8k 字節,超出了這個字節數,就會新起一個ziplist。ziplist 的長度由配置參數 list-max-ziplist-size 決定

4. 字典hash

Redis 的字典的值只能是字符串。

struct dict {
 ...
 dictht ht[2];
}

一個字典結構包含2個hashtable,是爲了擴容縮容時的漸進式rehash。

4.1. rehash

Java 的 HashMap 在字典很大時,rehash 是個耗時的操作,需要一次性全部 rehash。Redis 爲了高性能,不能堵塞服務,所以採用了漸進式 rehash 策略

漸進式 rehash 會在 rehash 的同時,保留新舊兩個 hash 結構,查詢時會同時查詢兩個hash 結構,然後在後續的定時任務中以及 hash 的子指令中,循序漸進地將舊 hash 的內容一點點遷移到新的 hash 結構中。當 hash 移除了最後一個元素之後,該數據結構自動被刪除

5. 對象的過期時間

Redis 所有的數據結構都可以設置過期時間,且是以對象爲單位。

如果一個字符串已經設置了過期時間,然後調用set 方法修改了它,它的過期時間會消失。

6. 應用:分佈式鎖

佔坑一般是使用 setnx(set if not exists) 指令,只允許被一個客戶端佔坑。先來先佔, 用完了,再調用 del 指令釋放茅坑

要想完美解決,要使用Redlock 算法。不使用可能因爲集羣情況下主從複製時主節點宕機,從節點沒有及時收到數據而出現同一把鎖被多次獲取。

7. 應用:限流(zset,漏斗限流)

  1. zset限流,score和value爲時間,將時間窗口之外的score和value刪除,統計剩下的數量,超過就要限流

  2. 漏斗限流

8. 使用標準結構存儲的閾值

Redis 規定在小對象存儲結構的限制條件如下:

  1. hash-max-zipmap-entries 512 # hash 的元素個數超過 512 就必須用標準結構存儲
  2. hash-max-zipmap-value 64 # hash 的任意元素的 key/value 的長度超過 64 就必須用標準結構存儲
  3. list-max-ziplist-entries 512 # list 的元素個數超過 512 就必須用標準結構存儲
  4. list-max-ziplist-value 64 # list 的任意元素的長度超過 64 就必須用標準結構存儲
  5. zset-max-ziplist-entries 128 # zset 的元素個數超過 128 就必須用標準結構存儲
  6. zset-max-ziplist-value 64 # zset 的任意元素的長度超過 64 就必須用標準結構存儲
  7. set-max-intset-entries 512 # set 的整數元素個數超過 512 就必須用標準結構存儲

9. Redis分佈式與CAP定理

一句話概括 CAP 原理就是——網絡分區發生時,一致性可用性兩難全。

分佈式系統的節點往往都是分佈在不同的機器上進行網絡隔離開的,這意味着必然會有網絡斷開的風險,這個網絡斷開的場景的專業詞彙叫着「網絡分區」。

在網絡分區發生時,兩個分佈式節點之間無法進行通信,我們對一個節點進行的修改操作將無法同步到另外一個節點,所以數據的「一致性」將無法滿足,因爲兩個分佈式節點的數據不再保持一致。除非我們犧牲「可用性」,也就是暫停分佈式節點服務,在網絡分區發生時,不再提供修改數據的功能,直到網絡狀況完全恢復正常再繼續對外提供服務

Redis 的主從數據是異步同步的,所以分佈式的 Redis 系統並不滿足「一致性」要求

當客戶端在 Redis 的主節點修改了數據後,立即返回,即使在主從網絡斷開的情況下,主節點依舊可以正常對外提供修改服務,所以 Redis 滿足「可用性」。

Redis 保證「最終一致性」,從節點會努力追趕主節點,最終從節點的狀態會和主節點的狀態將保持一致。如果網絡斷開了,主從節點的數據將會出現大量不一致,一旦網絡恢復,從節點會採用多種策略努力追趕上落後的數據,繼續盡力保持和主節點一致。

Redis 的複製是異步進行的,wait 指令可以讓異步複製變身同步複製,確保系統的強一
致性 (不嚴格)。wait 指令是 Redis3.0 版本以後纔出現的

10. Sentinel

Sentinel 無法保證消息完全不丟失,但是也儘可能保證消息少丟失。它有兩個選項可以
限制主從延遲過大。

min-slaves-to-write 1 # 表示主節點必須至少有一個從節點在進行正常複製,否則就停止對外寫服務,喪失可用性
min-slaves-max-lag 10  # 如果 10s 沒有收到從節點的反饋,就意味着從節點同步不正常

11. 集羣

11.1. Codis

Codis 是 Redis 集羣方案之一。Codis 使用 Go 語言開發,它是一個代理中間件,它和 Redis 一樣也使用 Redis 協議對外提供服務,當客戶端向 Codis 發送指令時,Codis 負責將指令轉發到後面的 Redis 實例來執行,並將返回結果再轉回給客戶端。

Codis 將所有的 key 默認劃分爲 1024 個槽位(slot),它首先對客戶端傳過來的 key 進行 crc32 運算計算哈希值,再將 hash 後的整數值對 1024 這個整數進行取模得到一個餘數,這個餘數就是對應 key 的槽位。每個槽位都會唯一映射到後面的多個 Redis 實例之一。

11.2. Redis-Cluster的實現

將所有數據劃分爲 16384 的 slots

Redis Cluster 可以爲每個主節點設置若干個從節點,單主節點故障時,集羣會自動將其中某個從節點提升爲主節點。如果某個主節點沒有從節點,那麼當它發生故障時,集羣將完全處於不可用狀態

12. Info詳解

Info 指令顯示的信息分爲 9 大塊:

  1. Server 服務器運行的環境參數
  2. Clients 客戶端相關信息
  3. Memory 服務器運行內存統計數據
  4. Persistence 持久化信息
  5. Stats 通用統計數據
  6. Replication 主從複製相關信息
  7. CPU CPU 使用情況
  8. Cluster 集羣信息
  9. KeySpace 鍵值對統計數量信息

12.1. 常見info

  • instantaneous_ops_per_sec:每秒執行多少次指令info stat

  • 佔用內存info memory

    used_memory_human:827.46K # 內存分配器 (jemalloc) 從操作系統分配的內存總量
    used_memory_rss_human:3.61M # 操作系統看到的內存佔用 ,top 命令看到的內存
    used_memory_peak_human:829.41K # Redis 內存消耗的峯值
    used_memory_lua_human:37.00K # lua 腳本引擎佔用的內存大小
    
  • repl_backlog_size:1048576 :積壓緩衝區大小info replication

13. 過期策略

13.1. 定時掃描策略

Redis 默認會每秒進行十次過期掃描,過期掃描不會遍歷過期字典中所有的 key,而是
採用了一種簡單的貪心策略如下,同時掃描時間的上限,默認不會超過 25ms。

  1. 從過期字典中隨機 20 個 key;
  2. 刪除這 20 個 key 中已經過期的 key;
  3. 如果過期的 key 比率超過 1/4,那就重複步驟 1;

但是如果所有的key同一時間過期,就會導致Redis持續掃描過期字典,這會導致卡頓。

也許你會爭辯說“掃描不是有 25ms 的時間上限了麼,怎麼會導致卡頓呢”?這裏打個比方,假如有 101 個客戶端同時將請求發過來了,然後前 100 個請求的執行時間都是25ms,那麼第 101 個指令需要等待多久才能執行?2500ms,這個就是客戶端的卡頓時間,是由服務器不間斷的小卡頓積少成多導致的。

如果有大批量的 key 過期,要給過期時間設置一個隨機範圍,而不能全部在同一時間過期。

# 在目標過期時間上增加一天的隨機時間
redis.expire_at(key, random.randint(86400) + expire_ts)

13.2. 從庫的過期策略

從庫不會進行過期掃描,主庫在key到期時,會在AOF文件裏增加一條del指令,同步到所有從庫。由於指令是異步進行的,所以會出現主從不一致。

14. 超出內存的策略-LRU

當 Redis 內存超出物理內存限制時,內存的數據會開始和磁盤產生頻繁的交換 (swap)。交換會讓 Redis 的性能急劇下降,對於訪問量比較頻繁的 Redis 來說,這樣龜速的存取效率基本上等於不可用。

在生產環境中我們是不允許 Redis 出現交換行爲的,爲了限制最大使用內存,Redis 提供了配置參數 maxmemory 來限制內存超出期望大小。

當實際內存超出 maxmemory 時,有如下策略:

  1. noeviction 不會繼續服務寫請求 (DEL 請求可以繼續服務),讀請求可以繼續進行。這樣可以保證不會丟失數據,但是會讓線上的業務不能持續進行。這是默認的淘汰策略。
  2. volatile-lru 嘗試淘汰設置了過期時間的 key,最少使用的 key 優先被淘汰。沒有設置過期時間的 key 不會被淘汰,這樣可以保證需要持久化的數據不會突然丟失。
  3. volatile-ttl 跟上面一樣,除了淘汰的策略不是 LRU,而是 key 的剩餘壽命 ttl 的值,ttl 越小越優先被淘汰。
  4. volatile-random 跟上面一樣,不過淘汰的 key 是過期 key 集合中隨機的 key。
  5. allkeys-lru 區別於 volatile-lru,這個策略要淘汰的 key 對象是全體的 key 集合,而不只是過期的 key 集合。這意味着沒有設置過期時間的 key 也會被淘汰。
  6. allkeys-random 跟上面一樣,不過淘汰的策略是隨機的 key。

volatile-xxx 策略只會針對帶過期時間的 key 進行淘汰,allkeys-xxx 策略會對所有的key 進行淘汰。如果你只是拿 Redis 做緩存,那應該使用 allkeys-xxx,客戶端寫緩存時不必攜帶過期時間。如果你還想同時使用 Redis 的持久化功能,那就使用 volatile-xxx 策略,這樣可以保留沒有設置過期時間的 key,它們是永久的 key 不會被 LRU 算法淘汰。

15. 懶惰刪除-Redis的多線程

一直以來我們認爲 Redis 是單線程的,單線程爲 Redis 帶來了代碼的簡潔性和豐富多樣的數據結構。不過 Redis 內部實際上並不是只有一個主線程,它還有幾個異步線程專門用來處理一些耗時的操作。

del大對象時會出現卡頓,爲了解決這個問題,4.0版引入了unlink指令,對刪除操作懶處理,丟給後臺線程異步回收內存。不是所有的 unlink 操作都會延後處理,如果對應 key 所佔用的內存很小,則跟del指令一樣。

同樣的還有flushdb、flushall指令,在後面加async參數即可丟給後臺線程處理。

16. listpack的結構

listpack沒有級聯更新的行爲,設計是用來取代ziplist的,目前只用在了Stream的數據結構中。

元素結構根ziplist很像。

struct listpack<T> {
    int32 total_bytes; // 佔用的總字節數
    int16 size; // 元素個數
    T[] entries; // 緊湊排列的元素列表
    int8 end; // 同 zlend 一樣,恆爲 0xFF
}
struct lpentry {
    int<var> encoding;
    optional byte[] content;
    int<var> length;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章