Redis 深度歷險: 核心原理和應用實踐3

Sentinel 基本使用 集羣 1:李代桃僵 —— Sentinel 

目前我們講的 Redis 還只是主從方案,最終一致性。讀者們可思考過,如果主節點凌晨 
3 點突發宕機怎麼辦?就坐等運維從牀上爬起來,然後手工進行從主切換,再通知所有的程
序把地址統統改一遍重新上線麼?毫無疑問,這樣的人工運維效率太低,事故發生時估計得
至少 1 個小時才能緩過來。如果是一個大型公司,這樣的事故足以上新聞了

所以我們必須有一個高可用方案來抵抗節點故障,當故障發生時可以自動進行從主切
換,程序可以不用重啓,運維可以繼續睡大覺,彷彿什麼事也沒發生一樣。Redis 官方提供
了這樣一種方案 —— Redis Sentinel(哨兵)


 我們可以將 Redis Sentinel 集羣看成是一個 ZooKeeper 集羣,它是集羣高可用的心臟,
它一般是由 3~5 個節點組成,這樣掛了個別節點集羣還可以正常運轉

它負責持續監控主從節點的健康,當主節點掛掉時,自動選擇一個最優的從節點切換爲
主節點。客戶端來連接集羣時,會首先連接 sentinel,通過 sentinel 來查詢主節點的地址,
然後再去連接主節點進行數據交互。當主節點發生故障時,客戶端會重新向 sentinel 要地
址,sentinel 會將最新的主節點地址告訴客戶端。如此應用程序將無需重啓即可自動完成節
點切換。
比如上圖的主節點掛掉後,集羣將可能自動調整爲下圖所示結構。 

從這張圖中我們能看到主節點掛掉了,原先的主從複製也斷開了,客戶端和損壞的主節
點也斷開了。從節點被提升爲新的主節點,其它從節點開始和新的主節點建立複製關係。客
戶端通過新的主節點繼續進行交互。Sentinel 會持續監控已經掛掉了主節點,待它恢復後,
集羣會調整爲下面這張圖。 

此時原先掛掉的主節點現在變成了從節點,從新的主節點那裏建立複製關係

消息丟失

Redis 主從採用異步複製,意味着當主節點掛掉時,從節點可能沒有收到全部的同步消
息,這部分未同步的消息就丟失了。如果主從延遲特別大,那麼丟失的數據就可能會特別
多。Sentinel 無法保證消息完全不丟失,但是也儘可能保證消息少丟失。它有兩個選項可以
限制主從延遲過大。

min-slaves-to-write 1

min-slaves-max-lag 10 

第一個參數表示主節點必須至少有一個從節點在進行正常複製,否則就停止對外寫服
務,喪失可用性。 
何爲正常複製,何爲異常複製?這個就是由第二個參數控制的,它的單位是秒,表示如
果 10s 沒有收到從節點的反饋,就意味着從節點同步不正常,要麼網絡斷開了,要麼一直沒
有給反饋。

Sentinel 基本使用 

有個問題是,但 sentinel 進行主從切換時,客戶端如何知道地址變更了 ? 通過分析源
碼,我發現 redis-py 在建立連接的時候進行了主庫地址變更判斷。 
連接池建立新連接時,會去查詢主庫地址,然後跟內存中的主庫地址進行比對,如果變
更了,就斷開所有連接,重新使用新地址建立新連接。如果是舊的主庫掛掉了,那麼所有正
在使用的連接都會被關閉,然後在重連時就會用上新地址

集羣 2:分而治之 —— Codis 

在大數據高併發場景下,單個 Redis 實例往往會顯得捉襟見肘。首先體現在內存上,單
個 Redis 的內存不宜過大,內存太大會導致 rdb 文件過大,進一步導致主從同步時全量同
步時間過長,在實例重啓恢復時也會消耗很長的數據加載時間,特別是在雲環境下,單個實
例內存往往都是受限的。其次體現在 CPU 的利用率上,單個 Redis 實例只能利用單個核
心,這單個核心要完成海量數據的存取和管理工作壓力會非常大

Codis 是 Redis 集羣方案之一,令我們感到驕傲的是,它是中國人開發並開源的,來自
前豌豆莢中間件團隊。有了 
Codis 技術積累之後,項目「突頭人」劉奇又開發出來中國人自己的開源分佈式數據庫 —— 
TiDB,可以說 6 到飛起。👍 

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

Codis 上掛接的所有 Redis 實例構成一個 Redis 集羣,當集羣空間不足時,可以通過動
態增加 Redis 實例來實現擴容需求

因爲 Codis 是無狀態的,它只是一個轉發代理中間件,這意味着我們可以啓動多個 
Codis 實例,供客戶端使用,每個 Codis 節點都是對等的。因爲單個 Codis 代理能支撐的 
QPS 比較有限,通過啓動多個 Codis 代理可以顯著增加整體的 QPS 需求,還能起到容災功
能,掛掉一個 Codis 代理沒關係,還有很多 Codis 代理可以繼續服務。 

Codis 分片原理 

Codis 要負責將特定的 key 轉發到特定的 Redis 實例,那麼這種對應關係 Codis 是如
何管理的呢? 
Codis 將所有的 key 默認劃分爲 1024 個槽位(slot),它首先對客戶端傳過來的 key 進
行 crc32 運算計算哈希值,再將 hash 後的整數值對 1024 這個整數進行取模得到一個餘
數,這個餘數就是對應 key 的槽位。 

擴容 
剛開始 Codis 後端只有一個 Redis 實例,1024 個槽位全部指向同一個 Redis。然後一
個 Redis 實例內存不夠了,所以又加了一個 Redis 實例。這時候需要對槽位關係進行調整,
將一半的槽位劃分到新的節點。這意味着需要對這一半的槽位對應的所有 key 進行遷移,遷
移到新的 Redis 實例

那 Codis 如果找到槽位對應的所有 key 呢? 
Codis 對 Redis 進行了改造,增加了 SLOTSSCAN 指令,可以遍歷指定 slot 下所有的 
key。Codis 通過 SLOTSSCAN 掃描出待遷移槽位的所有的 key,然後挨個遷移每個 key 到
新的 Redis 節點。 
在遷移過程中,Codis 還是會接收到新的請求打在當前正在遷移的槽位上,因爲當前槽
位的數據同時存在於新舊兩個槽位中,Codis 如何判斷該將請求轉發到後面的哪個具體實例
呢? 
Codis 無法判定遷移過程中的 key 究竟在哪個實例中,所以它採用了另一種完全不同的
思路。當 Codis 接收到位於正在遷移槽位中的 key 後,會立即強制對當前的單個 key 進行
遷移,遷移完成後,再將請求轉發到新的 Redis 實例

自動均衡 

Redis 新增實例,手工均衡 slots 太繁瑣,所以 Codis 提供了自動均衡功能。自動均衡會
在系統比較空閒的時候觀察每個 Redis 實例對應的 Slots 數量,如果不平衡,就會自動進行
遷移

Codis 的代價

Codis 給 Redis 帶來了擴容的同時,也損失了其它一些特性。因爲 Codis 中所有的 key 
分散在不同的 Redis 實例中,所以事務就不能再支持了,事務只能在單個 Redis 實例中完
成。同樣 rename 操作也很危險,它的參數是兩個 key,如果這兩個 key 在不同的 Redis 實
例中,rename 操作是無法正確完成的。Codis 的官方文檔中給出了一系列不支持的命令列

同樣爲了支持擴容,單個 key 對應的 value 不宜過大,因爲集羣的遷移的最小單位是 
key,對於一個 hash 結構,它會一次性使用 hgetall 拉取所有的內容,然後使用 hmset 放置
到另一個節點。如果 hash 內部的 kv 太多,可能會帶來遷移卡頓。官方建議單個集合結構
的總字節容量不要超過 1M。如果我們要放置社交關係數據,例如粉絲列表這種,就需要注
意了,可以考慮分桶存儲,在業務上作折中。 
Codis 因爲增加了 Proxy 作爲中轉層,所有在網絡開銷上要比單個 Redis 大,畢竟數據
包多走了一個網絡節點,整體在性能上要比單個 Redis 的性能有所下降。但是這部分性能損
耗不是太明顯,可以通過增加 Proxy 的數量來彌補性能上的不足。 
Codis 的集羣配置中心使用 zk 來實現,意味着在部署上增加了 zk 運維的代價,不過
大部分互聯網企業內部都有 zk 集羣,可以使用現有的 zk 集羣使用即可。 

MGET 指令的操作過程

架構變遷 

Codis 作爲非官方 Redis 集羣方案,近幾年來它的結構一直在不斷變化,一方面當官方
的 Redis 有變化的時候它要實時去跟進,另一方面它作爲 Redis Cluster 的競爭方案之一,
它還得持續提高自己的競爭力,給自己增加更多的官方集羣所沒有的便捷功能。 
比如 Codis 有個特色的地方在於強大的 Dashboard 功能,能夠便捷地對 Redis 集羣進
行管理。這是 Redis 官方所欠缺的。另外 Codis 還開發了一個 Codis-fe(federation 聯邦) 工
具,可以同時對多個 Codis 集羣進行管理。在大型企業,Codis 集羣往往會有幾十個,有這
樣一個便捷的聯邦工具可以降低不少運維成本。

Codis 的尷尬

Codis 不是 Redis 官方項目,這意味着它的命運會無比曲折,它總是要被官方 Redis 牽
着牛鼻子走。當 Redis 官方提供了什麼功能它欠缺時,Codis 就會感到恐懼,害怕自己被市
場甩掉,所以必須實時保持跟進。官方對重視內核,對工具無暇顧及,只提供基本的工具,其它完全交給第三方去開
發。 

集羣 3:衆志成城 —— Cluster 

RedisCluster 是 Redis 的親兒子,它是 Redis 作者自己提供的 Redis 集羣化方案。 相對於 Codis 的不同,它是去中心化的,如圖所示,該集羣有三個 Redis 節點組成,
每個節點負責整個集羣的一部分數據,每個節點負責的數據多少可能不一樣。這三個節點相
互連接組成一個對等的集羣,它們之間通過一種特殊的二進制協議相互交互集羣信息。 

客戶端爲了可以直接定位某個具體的 key 所在的節點,它就需要緩存槽位相關信息,這樣纔可以準確快速地定位到相應的節點。同時因爲槽位的信息可能會存在客戶端與服務器不
一致的情況,還需要糾正機制來實現槽位信息的校驗調整。

跳轉

當客戶端向一個錯誤的節點發出了指令,該節點會發現指令的 key 所在的槽位並不歸自
己管理,這時它會向客戶端發送一個特殊的跳轉指令攜帶目標操作的節點地址,告訴客戶端
去連這個節點去獲取數據

客戶端收到 MOVED 指令後,要立即糾正本地的槽位映射表。後續所有 key 將使用新
的槽位映射表

遷移 

Redis Cluster 提供了工具 redis-trib 可以讓運維人員手動調整槽位的分配情況,它使用 
Ruby 語言進行開發,通過組合各種原生的 Redis Cluster 指令來實現。這點 Codis 做的更加
人性化,它不但提供了 UI 界面可以讓我們方便的遷移,還提供了自動化平衡槽位工具,無
需人工干預就可以均衡集羣負載。不過 Redis 官方向來的策略就是提供最小可用的工具,其
它都交由社區完成
容錯 
Redis Cluster 可以爲每個主節點設置若干個從節點,單主節點故障時,集羣會自動將其
中某個從節點提升爲主節點。如果某個主節點沒有從節點,那麼當它發生故障時,集羣將完
全處於不可用狀態。不過 Redis 也提供了一個參數 cluster-require-full-coverage 可以允許部分
節點故障,其它節點還可以繼續提供對外訪問

網絡抖動

真實世界的機房網絡往往並不是風平浪靜的,它們經常會發生各種各樣的小問題。比如
網絡抖動就是非常常見的一種現象,突然之間部分連接變得不可訪問,然後很快又恢復正
常。 
爲解決這種問題,Redis Cluster 提供了一種選項 cluster-node-timeout,表示當某個節點持
續 timeout 的時間失聯時,纔可以認定該節點出現故障,需要進行主從切換。如果沒有這個
選項,網絡抖動會導致主從頻繁切換 (數據的重新複製)。 
還有另外一個選項 cluster-slave-validity-factor 作爲倍乘係數來放大這個超時時間來寬鬆容
錯的緊急程度。如果這個係數爲零,那麼主從切換是不會抗拒網絡抖動的。如果這個係數大
於 1,它就成了主從切換的鬆弛係數。 

 可能下線 (PFAIL-Possibly Fail) 與確定下線 (Fail) 
因爲 Redis Cluster 是去中心化的,一個節點認爲某個節點失聯了並不代表所有的節點都
認爲它失聯了。所以集羣還得經過一次協商的過程,只有當大多數節點都認定了某個節點失
聯了,集羣才認爲該節點需要進行主從切換來容錯。 
Redis 集羣節點採用 Gossip 協議來廣播自己的狀態以及自己對整個集羣認知的改變。比
如一個節點發現某個節點失聯了 (PFail),它會將這條信息向整個集羣廣播,其它節點也就可
以收到這點失聯信息。如果一個節點收到了某個節點失聯的數量 (PFail Count) 已經達到了集
羣的大多數,就可以標記該節點爲確定下線狀態 (Fail),然後向整個集羣廣播,強迫其它節
點也接收該節點已經下線的事實,並立即對該失聯節點進行主從切換

拓展 1:耳聽八方 —— Stream 

Redis5.0 被作者 Antirez 突然放了出來,增加了很多新的特色功能。而 Redis5.0 最大的
新特性就是多出了一個數據結構 Stream,它是一個新的強大的支持多播的可持久化的消息隊
列,作者坦言 Redis Stream 狠狠地借鑑了 Kafka 的設計

Redis Stream 的結構如上圖所示,它有一個消息鏈表,將所有加入的消息都串起來,每
個消息都有一個唯一的 ID 和對應的內容。消息是持久化的,Redis 重啓後,內容還在。 
每個 Stream 都有唯一的名稱,它就是 Redis 的 key,在我們首次使用 xadd 指令追加消
息時自動創建。 
每個 Stream 都可以掛多個消費組,每個消費組會有個遊標 last_delivered_id 在 Stream 
數組之上往前移動,表示當前消費組已經消費到哪條消息了。每個消費組都有一個 Stream 
內唯一的名稱,消費組不會自動創建,它需要單獨的指令 xgroup create 進行創建,需要指定
從 Stream 的某個消息 ID 開始消費,這個 ID 用來初始化 last_delivered_id 變量。 
每個消費組 (Consumer Group) 的狀態都是獨立的,相互不受影響。也就是說同一份 
Stream 內部的消息會被每個消費組都消費到。 

同一個消費組 (Consumer Group) 可以掛接多個消費者 (Consumer),這些消費者之間是
競爭關係,任意一個消費者讀取了消息都會使遊標 last_delivered_id 往前移動。每個消費者有
一個組內唯一名稱

消費者 (Consumer) 內部會有個狀態變量 pending_ids,它記錄了當前已經被客戶端讀取
的消息,但是還沒有 ack。如果客戶端沒有 ack,這個變量裏面的消息 ID 會越來越多,一
旦某個消息被 ack,它就開始減少。這個 pending_ids 變量在 Redis 官方被稱之爲 PEL,也
就是 Pending Entries List,這是一個很核心的數據結構,它用來確保客戶端至少消費了消息一
次,而不會在網絡傳輸的中途丟失了沒處理。 

消息 ID 

消息 ID 的形式是 timestampInMillis-sequence,例如 1527846880572-5,它表示當前的消
息在毫米時間戳 1527846880572 時產生,並且是該毫秒內產生的第 5 條消息。消息 ID 可以
由服務器自動生成,也可以由客戶端自己指定,但是形式必須是整數-整數,而且必須是後面
加入的消息的 ID 要大於前面的消息 ID

消息內容 
消息內容就是鍵值對,形如 hash 結構的鍵值對,這沒什麼特別之處

增刪改查
    1、xadd 追加消息 
    2、xdel 刪除消息,這裏的刪除僅僅是設置了標誌位,不影響消息總長度 
    3、xrange 獲取消息列表,會自動過濾已經刪除的消息 
    4、xlen 消息長度 
    5、del 刪除 Stream 

獨立消費 

我們可以在不定義消費組的情況下進行 Stream 消息的獨立消費,當 Stream 沒有新消
息時,甚至可以阻塞等待。Redis 設計了一個單獨的消費指令 xread,可以將 Stream 當成普
通的消息隊列 (list) 來使用。使用 xread 時,我們可以完全忽略消費組 (Consumer Group) 
的存在,就好比 Stream 就是一個普通的列表 (list)。

客戶端如果想要使用 xread 進行順序消費,一定要記住當前消費到哪裏了,也就是返回
的消息 ID。下次繼續調用 xread 時,將上次返回的最後一個消息 ID 作爲參數傳遞進去,
就可以繼續消費後續的消息。 
block 0 表示永遠阻塞,直到消息到來,block 1000 表示阻塞 1s,如果 1s 內沒有任何
消息到來,就返回 nil。 
 
127.0.0.1:6379> xread block 1000 count 1 streams codehole $ (nil) (1.07s) 
創建消費組 

消費 

Stream 提供了 xreadgroup 指令可以進行消費組的組內消費,需要提供消費組名稱、消
費者名稱和起始消息 ID。它同 xread 一樣,也可以阻塞等待新消息。讀到新消息後,對應
的消息 ID 就會進入消費者的 PEL(正在處理的消息) 結構裏,客戶端處理完畢後使用 xack 
指令通知服務器,本條消息已經處理完畢,該消息 ID 就會從 PEL 中移除。

Stream 消息太多怎麼辦?

Redis 自然考慮到了這一點,所以它提供了一個定長 Stream 功能。在 xadd 的指令提供
一個定長長度 maxlen,就可以將老的消息幹掉,確保最多不超過指定長度

消息如果忘記 ACK 會怎樣?

Stream 在每個消費者結構中保存了正在處理中的消息 ID 列表 PEL,如果消費者收到
了消息處理完了但是沒有回覆 ack,就會導致 PEL 列表不斷增長,如果有很多消費組的
話,那麼這個 PEL 佔用的內存就會放大。

拓展 2:無所不知 —— Info 指令

在使用 Redis 時,時常會遇到很多問題需要診斷,在診斷之前需要了解 Redis 的運行狀
態,通過強大的 Info 指令,你可以清晰地知道 Redis 內部一系列運行參數。 
Info 指令顯示的信息非常繁多,分爲 9 大塊,每個塊都有非常多的參數,這 9 個塊分
別是: 

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

Redis 每秒執行多少次指令

這個信息在 Stats 塊裏,可以通過 info stats 看到\

Redis 連接了多少客戶端

這個信息也是比較有用的,通過觀察這個數量可以確定是否存在意料之外的連接。如果
發現這個數量不對勁,接着就可以使用 client list 指令列出所有的客戶端鏈接地址來確定源
頭。 
關於客戶端的數量還有個重要的參數需要觀察,那就是 rejected_connections,它表示因
爲超出最大連接數限制而被拒絕的客戶端連接次數,如果這個數字很大,意味着服務器的最
大連接數設置的過低需要調整 maxclients 參數。 > redis-cli info stats |grep reject rejected_connections:0 

Redis 內存佔用多大 ? 

這個信息在 Memory 塊裏,可以通過 info memory 看到。 > redis-cli info memory | grep used | grep human 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 腳本引擎佔用的內存大小 
 
如果單個 Redis 內存佔用過大,並且在業務上沒有太多壓縮的空間的話,可以考慮集羣
化了

複製積壓緩衝區多大? 

這個信息在 Replication 塊裏,可以通過 info replication 看到。 > redis-cli info replication |grep backlog repl_backlog_active:0 repl_backlog_size:1048576  # 這個就是積壓緩衝區大小 repl_backlog_first_byte_offset:0 
repl_backlog_histlen:0 

複製積壓緩衝區大小非常重要,它嚴重影響到主從複製的效率。當從庫因爲網絡原因臨
時斷開了主庫的複製,然後網絡恢復了,又重新連上的時候,這段斷開的時間內發生在 
master 上的修改操作指令都會放在積壓緩衝區中,這樣從庫可以通過積壓緩衝區恢復中斷的
主從同步過程

積壓緩衝區是環形的,後來的指令會覆蓋掉前面的內容。如果從庫斷開的時間過長,或
者緩衝區的大小設置的太小,都會導致從庫無法快速恢復中斷的主從同步過程,因爲中間的
修改指令被覆蓋掉了。這時候從庫就會進行全量同步模式,非常耗費 CPU 和網絡資源

如果有多個從庫複製,積壓緩衝區是共享的,它不會因爲從庫過多而線性增長。如果實
例的修改指令請求很頻繁,那就把積壓緩衝區調大一些,幾十個 M 大小差不多了,如果很
閒,那就設置爲幾個 M。 > redis-cli info stats | grep sync sync_full:0 sync_partial_ok:0 sync_partial_err:0  # 半同步失敗次數 
 
通過查看 sync_partial_err 變量的次數來決定是否需要擴大積壓緩衝區,它表示主從半同
步複製失敗的次數。

拓展 3:拾遺漏補 —— 再談分佈式鎖

在第三節,我們細緻講解了分佈式鎖的原理,它的使用非常簡單,一條指令就可以完成
加鎖操作。不過在集羣環境下,這種方式是有缺陷的,它不是絕對安全的。 
比如在 Sentinel 集羣中,主節點掛掉時,從節點會取而代之,客戶端上卻並沒有明顯感
知。原先第一個客戶端在主節點中申請成功了一把鎖,但是這把鎖還沒有來得及同步到從節
點,主節點突然掛掉了。然後從節點變成了主節點,這個新的節點內部沒有這個鎖,所以當
另一個客戶端過來請求加鎖時,立即就批准了。這樣就會導致系統中同樣一把鎖被兩個客戶
端同時持有,不安全性由此產生

 
 不過這種不安全也僅僅是在主從發生 failover 的情況下才會產生,而且持續時間極短,
業務系統多數情況下可以容忍

Redlock 算法

爲了使用 Redlock,需要提供多個 Redis 實例,這些實例之前相互獨立沒有主從關係。
同很多分佈式算法一樣,redlock 也使用「大多數機制」。 
加鎖時,它會向過半節點發送 set(key, value, nx=True, ex=xxx) 指令,只要過半節點 set 
成功,那就認爲加鎖成功。釋放鎖時,需要向所有節點發送 del 指令。不過 Redlock 算法還
需要考慮出錯重試、時鐘漂移等很多細節問題,同時因爲 Redlock 需要向多個節點進行讀
寫,意味着相比單實例 Redis 性能會下降一些

Redlock 使用場景 

如果你很在乎高可用性,希望掛了一臺 redis 完全不受影響,那就應該考慮 redlock。不
過代價也是有的,需要更多的 redis 實例,性能也下降了,代碼上還需要引入額外的 
library,運維上也需要特殊對待,這些都是需要考慮的成本,使用前請再三斟酌

拓展 4:朝生暮死 —— 過期策略


Redis 所有的數據結構都可以設置過期時間,時間一到,就會自動刪除。你可以想象 
Redis 內部有一個死神,時刻盯着所有設置了過期時間的 key,壽命一到就會立即收割。 
 
你還可以進一步站在死神的角度思考,會不會因爲同一時間太多的 key 過期,以至於忙
不過來。同時因爲 Redis 是單線程的,收割的時間也會佔用線程的處理時間,如果收割的太
過於繁忙,會不會導致線上讀寫指令出現卡頓

過期的 key 集合 
redis 會將每個設置了過期時間的 key 放入到一個獨立的字典中,以後會定時遍歷這個
字典來刪除到期的 key。除了定時遍歷之外,它還會使用惰性策略來刪除過期的 key,所謂
惰性策略就是在客戶端訪問這個 key 的時候,redis 對 key 的過期時間進行檢查,如果過期
了就立即刪除。定時刪除是集中處理,惰性刪除是零散處理

定時掃描策略 
Redis 默認會每秒進行十次過期掃描,過期掃描不會遍歷過期字典中所有的 key,而是
採用了一種簡單的貪心策略

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

同時,爲了保證過期掃描不會出現循環過度,導致線程卡死現象,算法還增加了掃描時
間的上限,默認不會超過 25ms

業務開發人員一定要注意過期時間,如果有大批量的 key 過期,要給過期時間設置
一個隨機範圍,而不能全部在同一時間過期。

從庫的過期策略 

從庫不會進行過期掃描,從庫對過期的處理是被動的。主庫在 key 到期時,會在 AOF 
文件裏增加一條 del 指令,同步到所有的從庫,從庫通過執行這條 del 指令來刪除過期的 
key 

拓展 5:優勝劣汰 —— LRU 

當 Redis 內存超出物理內存限制時,內存的數據會開始和磁盤產生頻繁的交換 (swap)。
交換會讓 Redis 的性能急劇下降,對於訪問量比較頻繁的 Redis 來說,這樣龜速的存取效率
基本上等於不可用。 
在生產環境中我們是不允許 Redis 出現交換行爲的,爲了限制最大使用內存,Redis 提
供了配置參數 maxmemory 來限制內存超出期望大小。 
當實際內存超出 maxmemory 時,Redis 提供了幾種可選策略 (maxmemory-policy) 來讓
用戶自己決定該如何騰出新的空間以繼續提供讀寫服務。

 noeviction 不會繼續服務寫請求 (DEL 請求可以繼續服務),讀請求可以繼續進行。這樣
可以保證不會丟失數據,但是會讓線上的業務不能持續進行。這是默認的淘汰策略。 
 volatile-lru 嘗試淘汰設置了過期時間的 key,最少使用的 key 優先被淘汰。沒有設置過
期時間的 key 不會被淘汰,這樣可以保證需要持久化的數據不會突然丟失。 
 volatile-ttl 跟上面一樣,除了淘汰的策略不是 LRU,而是 key 的剩餘壽命 ttl 的值,ttl 
越小越優先被淘汰。 
 volatile-random 跟上面一樣,不過淘汰的 key 是過期 key 集合中隨機的 key。 
 allkeys-lru 區別於 volatile-lru,這個策略要淘汰的 key 對象是全體的 key 集合,而不
只是過期的 key 集合。這意味着沒有設置過期時間的 key 也會被淘汰。 
 allkeys-random 跟上面一樣,不過淘汰的策略是隨機的 key。 
 volatile-xxx 策略只會針對帶過期時間的 key 進行淘汰,allkeys-xxx 策略會對所有的 
key 進行淘汰。如果你只是拿 Redis 做緩存,那應該使用 allkeys-xxx,客戶端寫緩存時
不必攜帶過期時間。如果你還想同時使用 Redis 的持久化功能,那就使用 volatile-xxx 
策略,這樣可以保留沒有設置過期時間的 key,它們是永久的 key 不會被 LRU 算法淘

LRU 算法

實現 LRU 算法除了需要 key/value 字典外,還需要附加一個鏈表,鏈表中的元素按照
一定的順序進行排列。當空間滿的時候,會踢掉鏈表尾部的元素。當字典的某個元素被訪問
時,它在鏈表中的位置會被移動到表頭。所以鏈表的元素排列順序就是元素最近被訪問的時
間順序。 
位於鏈表尾部的元素就是不被重用的元素,所以會被踢掉。位於表頭的元素就是最近剛
剛被人用過的元素,所以暫時不會被踢。

拓展 6:平波緩進 —— 懶惰刪除 

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

Redis 爲什麼要懶惰刪除(lazy free)

刪除指令 del 會直接釋放對象的內存,大部分情況下,這個指令非常快,沒有明顯延
遲。不過如果刪除的 key 是一個非常大的對象,比如一個包含了千萬元素的 hash,那麼刪
除操作就會導致單線程卡頓。 
Redis 爲了解決這個卡頓問題,在 4.0 版本引入了 unlink 指令,它能對刪除操作進行懶
處理,丟給後臺線程來異步回收內存。

> unlink key

OK

flush

Redis 提供了 flushdb 和 flushall 指令,用來清空數據庫,這也是極其緩慢的操作。
Redis 4.0 同樣給這兩個指令也帶來了異步化,在指令後面增加 async 參數就可以將整棵大樹
連根拔起,扔給後臺線程慢慢焚燒。

> flushall async

OK  

異步隊列

P176

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章