關於Redis,你所需要知道的一切

前言

Redis 作爲一個開源的,高級的鍵值存儲和一個適用的解決方案,已經越來越在構建 「高性能」「可擴展」 的 Web 應用上發揮着舉足輕重的作用。

當今互聯網技術架構中 Redis 已然成爲了應用得最廣泛的中間件之一,它也是中高級後端工程 技術面試 中面試官最喜歡問的工程技能之一,不僅僅要求着我們對 基本的使用 進行掌握,更要深層次地理解 Redis 內部實現 的細節原理。

熟練掌握 Redis,甚至可以毫不誇張地說已經半隻腳踏入心儀的公司了。下面我們一起來盤點回顧一下 Redis 的面試經典問題,就不要再被面試官問得 臉都綠了 呀!

  • Ps:我把 重要的知識點 都做成了 圖片,希望各位 "用餐愉快"

一、基礎篇

■什麼是 Redis ?

先解釋 Redis 基本概念

Redis (Remote Dictionary Server) 是一個使用 C 語言 編寫的,開源的 (BSD許可) 高性能非關係型 (NoSQL) 的 鍵值對數據庫

簡單提一下 Redis 數據結構

Redis 可以存儲  和 不同類型數據結構值 之間的映射關係。鍵的類型只能是字符串,而值除了支持最 基礎的五種數據類型 外,還支持一些 高級數據類型

一定要說出一些高級數據結構 (當然你自己也要了解.. 下面會說到的別擔心),這樣面試官的眼睛纔會亮。

Redis 小總結

與傳統數據庫不同的是 Redis 的數據是 存在內存 中的,所以 讀寫速度 非常 ,因此 Redis 被廣泛應用於 緩存 方向,每秒可以處理超過 10 萬次讀寫操作,是已知性能最快的 Key-Value 數據庫。另外,Redis 也經常用來做 分佈式鎖

除此之外,Redis 支持事務 、持久化、LUA腳本、LRU驅動事件、多種集羣方案。

■Redis 優缺點

優點

  • 讀寫性能優異, Redis能讀的速度是 110000 次/s,寫的速度是 81000 次/s。

  • 支持數據持久化,支持 AOF 和 RDB 兩種持久化方式。

  • 支持事務,Redis 的所有操作都是原子性的,同時 Redis 還支持對幾個操作合併後的原子性執行。

  • 數據結構豐富,除了支持 string 類型的 value 外還支持 hash、set、zset、list 等數據結構。

  • 支持主從複製,主機會自動將數據同步到從機,可以進行讀寫分離。

缺點

  • 數據庫 容量受到物理內存的限制,不能用作海量數據的高性能讀寫,因此 Redis 適合的場景主要侷限在較小數據量的高性能操作和運算上。

  • Redis 不具備自動容錯和恢復功能,主機從機的宕機都會導致前端部分讀寫請求失敗,需要等待機器重啓或者手動切換前端的 IP 才能恢復。

  • 主機宕機,宕機前有部分數據未能及時同步到從機,切換 IP 後還會引入數據不一致的問題,降低了 系統的可用性

  • Redis 較難支持在線擴容,在集羣容量達到上限時在線擴容會變得很複雜。爲避免這一問題,運維人員在系統上線時必須確保有足夠的空間,這對資源造成了很大的浪費。

■爲什麼要用緩存?爲什麼使用 Redis?

提一下現在 Web 應用的現狀

在日常的 Web 應用對數據庫的訪問中,讀操作的次數遠超寫操作,比例大概在 1:9 到 3:7,所以需要讀的可能性是比寫的可能大得多的。當我們使用 SQL 語句去數據庫進行讀寫操作時,數據庫就會 去磁盤把對應的數據索引取回來,這是一個相對較慢的過程。

使用 Redis or 使用緩存帶來的優勢

如果我們把數據放在 Redis 中,也就是直接放在內存之中,讓服務端直接去讀取內存中的數據,那麼這樣 速度 明顯就會快上不少 (高性能),並且會 極大減小數據庫的壓力 (特別是在高併發情況下)

記得是 兩個角度 啊.. 高性能 和 高併發..

也要提一下使用緩存的考慮

但是使用內存進行數據存儲開銷也是比較大的,限於成本 的原因,一般我們只是使用 Redis 存儲一些 常用和主要的數據,比如用戶登錄的信息等。

一般而言在使用 Redis 進行存儲的時候,我們需要從以下幾個方面來考慮:

  • 業務數據常用嗎?命中率如何? 如果命中率很低,就沒有必要寫入緩存;

  • 該業務數據是讀操作多,還是寫操作多? 如果寫操作多,頻繁需要寫入數據庫,也沒有必要使用緩存;

  • 業務數據大小如何? 如果要存儲幾百兆字節的文件,會給緩存帶來很大的壓力,這樣也沒有必要;

在考慮了這些問題之後,如果覺得有必要使用緩存,那麼就使用它!

■使用緩存會出現什麼問題?

一般來說有如下幾個問題,回答思路遵照 是什麼 → 爲什麼 → 怎麼解決

  1. 緩存雪崩問題;

  2. 緩存穿透問題;

  3. 緩存和數據庫雙寫一致性問題;

緩存雪崩問題

另外對於 "Redis 掛掉了,請求全部走數據庫" 這樣的情況,我們還可以有如下的思路:

  • 事發前:實現 Redis 的高可用(主從架構 + Sentinel 或者 Redis Cluster),儘量避免 Redis 掛掉這種情況發生。

  • 事發中:萬一 Redis 真的掛了,我們可以設置本地緩存(ehcache) + 限流(hystrix),儘量避免我們的數據庫被幹掉(起碼能保證我們的服務還是能正常工作的)

  • 事發後:Redis 持久化,重啓後自動從磁盤上加載數據,快速恢復緩存數據。

緩存穿透問題

緩存與數據庫雙寫一致問題

雙寫一致性上圖還是稍微粗糙了些,你還需要知道兩種方案 (先操作數據庫和先操作緩存) 分別都有什麼優勢和對應的問題,這裏不作贅述,可以參考一下下方的文章,寫得非常詳細。

■Redis 爲什麼早期版本選擇單線程?

官方解釋

因爲 Redis 是基於內存的操作,CPU 不是 Redis 的瓶頸,Redis 的瓶頸最有可能是 機器內存的大小 或者 網絡帶寬。既然單線程容易實現,而且 CPU 不會成爲瓶頸,那就順理成章地採用單線程的方案了。

簡單總結一下

  1. 使用單線程模型能帶來更好的 可維護性,方便開發和調試;

  2. 使用單線程模型也能 併發 的處理客戶端的請求;(I/O 多路複用機制)

  3. Redis 服務中運行的絕大多數操作的 性能瓶頸都不是 CPU

強烈推薦 各位親看一下這篇文章:

  • 爲什麼 Redis 選擇單線程模型 · Why's THE Design? - https://draveness.me/whys-the-design-redis-single-thread

■Redis 爲什麼這麼快?

簡單總結:

  1. 純內存操作:讀取不需要進行磁盤 I/O,所以比傳統數據庫要快上不少;(但不要有誤區說磁盤就一定慢,例如 Kafka 就是使用磁盤順序讀取但仍然較快)

  2. 單線程,無鎖競爭:這保證了沒有線程的上下文切換,不會因爲多線程的一些操作而降低性能;

  3. 多路 I/O 複用模型,非阻塞 I/O:採用多路 I/O 複用技術可以讓單個線程高效的處理多個網絡連接請求(儘量減少網絡 IO 的時間消耗);

  4. 高效的數據結構,加上底層做了大量優化:Redis 對於底層的數據結構和內存佔用做了大量的優化,例如不同長度的字符串使用不同的結構體表示,HyperLogLog 的密集型存儲結構等等..

二、數據結構篇

■簡述一下 Redis 常用數據結構及實現?

首先在 Redis 內部會使用一個 RedisObject 對象來表示所有的 key 和 value

其次 Redis 爲了 平衡空間和時間效率,針對 value 的具體類型在底層會採用不同的數據結構來實現,下圖展示了他們之間的映射關係:(好像亂糟糟的,但至少能看清楚..)

■Redis 的 SDS 和 C 中字符串相比有什麼優勢?

先簡單總結一下

C 語言使用了一個長度爲 N+1 的字符數組來表示長度爲 N 的字符串,並且字符數組最後一個元素總是 \0,這種簡單的字符串表示方式 不符合 Redis 對字符串在安全性、效率以及功能方面的要求

再來說 C 語言字符串的問題

這樣簡單的數據結構可能會造成以下一些問題:

  • 獲取字符串長度爲 O(N) 級別的操作 → 因爲 C 不保存數組的長度,每次都需要遍歷一遍整個數組;

  • 不能很好的杜絕 緩衝區溢出/內存泄漏 的問題 → 跟上述問題原因一樣,如果執行拼接 or 縮短字符串的操作,如果操作不當就很容易造成上述問題;

  • C 字符串 只能保存文本數據 → 因爲 C 語言中的字符串必須符合某種編碼(比如 ASCII),例如中間出現的 '\0' 可能會被判定爲提前結束的字符串而識別不了;

Redis 如何解決的 | SDS 的優勢

如果去看 Redis 的源碼 sds.h/sdshdr 文件,你會看到 SDS 完整的實現細節,這裏簡單來說一下 Redis 如何解決的:

  1. 多增加 len 表示當前字符串的長度:這樣就可以直接獲取長度了,複雜度 O(1);

  2. 自動擴展空間:當 SDS 需要對字符串進行修改時,首先藉助於 len 和 alloc 檢查空間是否滿足修改所需的要求,如果空間不夠的話,SDS 會自動擴展空間,避免了像 C 字符串操作中的覆蓋情況;

  3. 有效降低內存分配次數:C 字符串在涉及增加或者清除操作時會改變底層數組的大小造成重新分配,SDS 使用了 空間預分配 和 惰性空間釋放 機制,簡單理解就是每次在擴展時是成倍的多分配的,在縮容是也是先留着並不正式歸還給 OS;

  4. 二進制安全:C 語言字符串只能保存 ascii 碼,對於圖片、音頻等信息無法保存,SDS 是二進制安全的,寫入什麼讀取就是什麼,不做任何過濾和限制;

■字典是如何實現的?Rehash 瞭解嗎?

先總體聊一下 Redis 中的字典

字典是 Redis 服務器中出現最爲頻繁的複合型數據結構。除了 hash 結構的數據會用到字典外,整個 Redis 數據庫的所有 key 和 value 也組成了一個 全局字典,還有帶過期時間的 key 也是一個字典。(存儲在 RedisDb 數據結構中)

說明字典內部結構和 rehash

Redis 中的字典相當於 Java 中的 HashMap,內部實現也差不多類似,都是通過 "數組 + 鏈表" 的 鏈地址法 來解決部分 哈希衝突,同時這樣的結構也吸收了兩種不同數據結構的優點。

字典結構內部包含 兩個 hashtable,通常情況下只有一個 hashtable 有值,但是在字典擴容縮容時,需要分配新的 hashtable,然後進行 漸進式搬遷 (rehash),這時候兩個 hashtable 分別存儲舊的和新的 hashtable,待搬遷結束後,舊的將被刪除,新的 hashtable 取而代之。

擴縮容的條件

正常情況下,當 hash 表中 元素的個數等於第一維數組的長度時,就會開始擴容,擴容的新數組是 原數組大小的 2 倍。不過如果 Redis 正在做 bgsave(持久化命令),爲了減少內存也得過多分離,Redis 儘量不去擴容,但是如果 hash 表非常滿了,達到了第一維數組長度的 5 倍了,這個時候就會 強制擴容

當 hash 表因爲元素逐漸被刪除變得越來越稀疏時,Redis 會對 hash 表進行縮容來減少 hash 表的第一維數組空間佔用。所用的條件是 元素個數低於數組長度的 10%,縮容不會考慮 Redis 是否在做 bgsave

■跳躍表是如何實現的?原理?

這是 Redis 中比較重要的一個數據結構,建議閱讀 之前寫過的文章,裏面詳細介紹了原理和一些細節:

■HyperLogLog 有了解嗎?

建議閱讀 之前的系列文章:

■布隆過濾器有了解嗎?

建議閱讀 之前的系列文章:

■GeoHash 瞭解嗎?

建議閱讀 之前的系列文章:

■壓縮列表瞭解嗎?

這是 Redis 爲了節約內存 而使用的一種數據結構,zset 和 hash 容器對象會在元素個數較少的時候,採用壓縮列表(ziplist)進行存儲。壓縮列表是 一塊連續的內存空間,元素之間緊挨着存儲,沒有任何冗餘空隙。

因爲之前自己也沒有學習過,所以找了一篇比較好比較容易理解的文章:

■快速列表 quicklist 瞭解嗎?

Redis 早期版本存儲 list 列表數據結構使用的是壓縮列表 ziplist 和普通的雙向鏈表 linkedlist,也就是說當元素少時使用 ziplist,當元素多時用 linkedlist。但考慮到鏈表的附加空間相對較高,prev 和 next 指針就要佔去 16 個字節(64 位操作系統佔用 8 個字節),另外每個節點的內存都是單獨分配,會傢俱內存的碎片化,影響內存管理效率。

後來 Redis 新版本(3.2)對列表數據結構進行了改造,使用 quicklist 代替了 ziplist 和linkedlist

同上..建議閱讀一下以下的文章:

  • Redis列表list 底層原理 - https://zhuanlan.zhihu.com/p/102422311

■Stream 結構有了解嗎?

Redis Stream 從概念上來說,就像是一個 僅追加內容 的 消息鏈表,把所有加入的消息都一個一個串起來,每個消息都有一個唯一的 ID 和內容,這很簡單,讓它複雜的是從 Kafka 借鑑的另一種概念:消費者組(Consumer Group) (思路一致,實現不同)

上圖就展示了一個典型的 Stream 結構。每個 Stream 都有唯一的名稱,它就是 Redis 的 key,在我們首次使用 xadd 指令追加消息時自動創建。我們對圖中的一些概念做一下解釋:

  • Consumer Group:消費者組,可以簡單看成記錄流狀態的一種數據結構。消費者既可以選擇使用 XREAD 命令進行 獨立消費,也可以多個消費者同時加入一個消費者組進行 組內消費。同一個消費者組內的消費者共享所有的 Stream 信息,同一條消息只會有一個消費者消費到,這樣就可以應用在分佈式的應用場景中來保證消息的唯一性。

  • last_delivered_id:用來表示消費者組消費在 Stream 上 消費位置 的遊標信息。每個消費者組都有一個 Stream 內 唯一的名稱,消費者組不會自動創建,需要使用 XGROUP CREATE指令來顯式創建,並且需要指定從哪一個消息 ID 開始消費,用來初始化 last_delivered_id 這個變量。

  • pending_ids:每個消費者內部都有的一個狀態變量,用來表示 已經 被客戶端 獲取,但是還沒有 ack 的消息。記錄的目的是爲了 保證客戶端至少消費了消息一次,而不會在網絡傳輸的中途丟失而沒有對消息進行處理。如果客戶端沒有 ack,那麼這個變量裏面的消息 ID 就會越來越多,一旦某個消息被 ack,它就會對應開始減少。這個變量也被 Redis 官方稱爲 PEL (Pending Entries List)

Stream 消息太多怎麼辦?

很容易想到,要是消息積累太多,Stream 的鏈表豈不是很長,內容會不會爆掉就是個問題了。xdel 指令又不會刪除消息,它只是給消息做了個標誌位。

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

> XADD mystream MAXLEN 2 * value 1
1526654998691-0
> XADD mystream MAXLEN 2 * value 2
1526654999635-0
> XADD mystream MAXLEN 2 * value 3
1526655000369-0
> XLEN mystream
(integer) 2
> XRANGE mystream - +
1) 1) 1526654999635-0
   2) 1) "value"
      2) "2"
2) 1) 1526655000369-0
   2) 1) "value"
      2) "3"

如果使用 MAXLEN 選項,當 Stream 的達到指定長度後,老的消息會自動被淘汰掉,因此 Stream 的大小是恆定的。目前還沒有選項讓 Stream 只保留給定數量的條目,因爲爲了一致地運行,這樣的命令必須在很長一段時間內阻塞以淘汰消息。(例如在添加數據的高峯期間,你不得不長暫停來淘汰舊消息和添加新的消息)

另外使用 MAXLEN 選項的花銷是很大的,Stream 爲了節省內存空間,採用了一種特殊的結構表示,而這種結構的調整是需要額外的花銷的。所以我們可以使用一種帶有 ~ 的特殊命令:

XADD mystream MAXLEN ~ 1000 * ... entry fields here ...

它會基於當前的結構合理地對節點執行裁剪,來保證至少會有 1000 條數據,可能是 1010也可能是 1030

PEL 是如何避免消息丟失的?

在客戶端消費者讀取 Stream 消息時,Redis 服務器將消息回覆給客戶端的過程中,客戶端突然斷開了連接,消息就丟失了。但是 PEL 裏已經保存了發出去的消息 ID,待客戶端重新連上之後,可以再次收到 PEL 中的消息 ID 列表。不過此時 xreadgroup 的起始消息 ID 不能爲參數 > ,而必須是任意有效的消息 ID,一般將參數設爲 0-0,表示讀取所有的 PEL 消息以及自 last_delivered_id 之後的新消息。

和 Kafka 對比起來呢?

Redis 基於內存存儲,這意味着它會比基於磁盤的 Kafka 快上一些,也意味着使用 Redis 我們 不能長時間存儲大量數據。不過如果您想以 最小延遲 實時處理消息的話,您可以考慮 Redis,但是如果 消息很大並且應該重用數據 的話,則應該首先考慮使用 Kafka。

另外從某些角度來說,Redis Stream 也更適用於小型、廉價的應用程序,因爲 Kafka 相對來說更難配置一些。

推薦閱讀 之前的系列文章,裏面 也對 Pub/ Sub 做了詳細的描述

三、持久化篇

■什麼是持久化?

先簡單談一談是什麼

Redis 的數據 全部存儲 在 內存 中,如果 突然宕機,數據就會全部丟失,因此必須有一套機制來保證 Redis 的數據不會因爲故障而丟失,這種機制就是 Redis 的 持久化機制,它會將內存中的數據庫狀態 保存到磁盤 中。

解釋一下持久化發生了什麼

我們來稍微考慮一下 Redis 作爲一個 "內存數據庫" 要做的關於持久化的事情。通常來說,從客戶端發起請求開始,到服務器真實地寫入磁盤,需要發生如下幾件事情:

詳細版 的文字描述大概就是下面這樣:

  1. 客戶端向數據庫 發送寫命令 (數據在客戶端的內存中)

  2. 數據庫 接收 到客戶端的 寫請求 (數據在服務器的內存中)

  3. 數據庫 調用系統 API 將數據寫入磁盤 (數據在內核緩衝區中)

  4. 操作系統將 寫緩衝區 傳輸到 磁盤控控制器 (數據在磁盤緩存中)

  5. 操作系統的磁盤控制器將數據 寫入實際的物理媒介 中 (數據在磁盤中)

分析如何保證持久化安全

如果我們故障僅僅涉及到 軟件層面 (該進程被管理員終止或程序崩潰) 並且沒有接觸到內核,那麼在 上述步驟 3 成功返回之後,我們就認爲成功了。即使進程崩潰,操作系統仍然會幫助我們把數據正確地寫入磁盤。

如果我們考慮 停電/ 火災 等 更具災難性 的事情,那麼只有在完成了第 5 步之後,纔是安全的。

機房”火了“

所以我們可以總結得出數據安全最重要的階段是:步驟三、四、五,即:

  • 數據庫軟件調用寫操作將用戶空間的緩衝區轉移到內核緩衝區的頻率是多少?

  • 內核多久從緩衝區取數據刷新到磁盤控制器?

  • 磁盤控制器多久把數據寫入物理媒介一次?

  • 注意: 如果真的發生災難性的事件,我們可以從上圖的過程中看到,任何一步都可能被意外打斷丟失,所以只能 儘可能地保證 數據的安全,這對於所有數據庫來說都是一樣的。

我們從 第三步 開始。Linux 系統提供了清晰、易用的用於操作文件的 POSIX file API20多年過去,仍然還有很多人對於這一套 API 的設計津津樂道,我想其中一個原因就是因爲你光從 API 的命名就能夠很清晰地知道這一套 API 的用途:

int open(const char *path, int oflag, .../*,mode_t mode */);
int close (int filedes);int remove( const char *fname );
ssize_t write(int fildes, const void *buf, size_t nbyte);
ssize_t read(int fildes, void *buf, size_t nbyte);
  • 參考自:API 設計最佳實踐的思考 - https://www.cnblogs.com/yuanjiangw/p/10846560.html

所以,我們有很好的可用的 API 來完成 第三步,但是對於成功返回之前,我們對系統調用花費的時間沒有太多的控制權。

然後我們來說說 第四步。我們知道,除了早期對電腦特別瞭解那幫人 (操作系統就這幫人搞的),實際的物理硬件都不是我們能夠 直接操作 的,都是通過 操作系統調用 來達到目的的。爲了防止過慢的 I/O 操作拖慢整個系統的運行,操作系統層面做了很多的努力,譬如說 上述第四步 提到的 寫緩衝區,並不是所有的寫操作都會被立即寫入磁盤,而是要先經過一個緩衝區,默認情況下,Linux 將在 30 秒 後實際提交寫入。

但是很明顯,30 秒 並不是 Redis 能夠承受的,這意味着,如果發生故障,那麼最近 30 秒內寫入的所有數據都可能會丟失。幸好 PROSIX API 提供了另一個解決方案:fsync,該命令會 強制 內核將 緩衝區 寫入 磁盤,但這是一個非常消耗性能的操作,每次調用都會 阻塞等待直到設備報告 IO 完成,所以一般在生產環境的服務器中,Redis 通常是每隔 1s 左右執行一次 fsync 操作。

到目前爲止,我們瞭解到瞭如何控制 第三步 和 第四步,但是對於 第五步,我們 完全無法控制。也許一些內核實現將試圖告訴驅動實際提交物理介質上的數據,或者控制器可能會爲了提高速度而重新排序寫操作,不會盡快將數據真正寫到磁盤上,而是會等待幾個多毫秒。這完全是我們無法控制的。

普通人簡單說一下第一條就過了,如果你詳細地對後面兩方面 侃侃而談,那面試官就會對你另眼相看了。

■Redis 中的兩種持久化方式?

方式一:快照

Redis 快照 是最簡單的 Redis 持久性模式。當滿足特定條件時,它將生成數據集的時間點快照,例如,如果先前的快照是在 2 分鐘前創建的,並且現在已經至少有 100 次新寫入,則將創建一個新的快照。此條件可以由用戶配置 Redis 實例來控制,也可以在運行時修改而無需重新啓動服務器。快照作爲包含整個數據集的單個 .rdb 文件生成。

方式二:AOF

快照不是很持久。如果運行 Redis 的計算機停止運行,電源線出現故障或者您 kill -9 的實例意外發生,則寫入 Redis 的最新數據將丟失。儘管這對於某些應用程序可能不是什麼大問題,但有些使用案例具有充分的耐用性,在這些情況下,快照並不是可行的選擇。

AOF(Append Only File - 僅追加文件) 它的工作方式非常簡單:每次執行 修改內存 中數據集的寫操作時,都會 記錄 該操作。假設 AOF 日誌記錄了自 Redis 實例創建以來 所有的修改性指令序列,那麼就可以通過對一個空的 Redis 實例 順序執行所有的指令,也就是 「重放」,來恢復 Redis 當前實例的內存數據結構的狀態。

Redis 4.0 的混合持久化

重啓 Redis 時,我們很少使用 rdb 來恢復內存狀態,因爲會丟失大量數據。我們通常使用 AOF 日誌重放,但是重放 AOF 日誌性能相對 rdb 來說要慢很多,這樣在 Redis 實例很大的情況下,啓動需要花費很長的時間。

Redis 4.0 爲了解決這個問題,帶來了一個新的持久化選項——混合持久化。將 rdb 文件的內容和增量的 AOF 日誌文件存在一起。這裏的 AOF 日誌不再是全量的日誌,而是 自持久化開始到持久化結束 的這段時間發生的增量 AOF 日誌,通常這部分 AOF 日誌很小:

於是在 Redis 重啓的時候,可以先加載 rdb 的內容,然後再重放增量 AOF 日誌就可以完全替代之前的 AOF 全量文件重放,重啓效率因此大幅得到提升。

關於兩種持久化方式的更多細節 (原理) 可以參考:

■RDB 和 AOF 各自有什麼優缺點?

RDB | 優點

  1. 只有一個文件 dump.rdb方便持久化

  2. 容災性好,一個文件可以保存到安全的磁盤。

  3. 性能最大化fork 子進程來完成寫操作,讓主進程繼續處理命令,所以使 IO 最大化。使用單獨子進程來進行持久化,主進程不會進行任何 IO 操作,保證了 Redis 的高性能

  4. 相對於數據集大時,比 AOF 的 啓動效率 更高。

RDB | 缺點

  1. 數據安全性低。RDB 是間隔一段時間進行持久化,如果持久化之間 Redis 發生故障,會發生數據丟失。所以這種方式更適合數據要求不嚴謹的時候;

AOF | 優點

  1. 數據安全,aof 持久化可以配置 appendfsync 屬性,有 always,每進行一次命令操作就記錄到 aof 文件中一次。

  2. 通過 append 模式寫文件,即使中途服務器宕機,可以通過 redis-check-aof 工具解決數據一致性問題。

  3. AOF 機制的 rewrite 模式。AOF 文件沒被 rewrite 之前(文件過大時會對命令 進行合併重寫),可以刪除其中的某些命令(比如誤操作的 flushall)

AOF | 缺點

  1. AOF 文件比 RDB 文件大,且 恢復速度慢

  2. 數據集大 的時候,比 rdb 啓動效率低

■兩種方式如何選擇?

  • 一般來說, 如果想達到足以媲美 PostgreSQL 的 數據安全性,你應該 同時使用兩種持久化功能。在這種情況下,當 Redis 重啓的時候會優先載入 AOF 文件來恢復原始的數據,因爲在通常情況下 AOF 文件保存的數據集要比 RDB 文件保存的數據集要完整。

  • 如果你非常關心你的數據, 但仍然 可以承受數分鐘以內的數據丟失,那麼你可以 只使用 RDB 持久化

  • 有很多用戶都只使用 AOF 持久化,但並不推薦這種方式,因爲定時生成 RDB 快照(snapshot)非常便於進行數據庫備份, 並且 RDB 恢復數據集的速度也要比 AOF 恢復的速度要快,除此之外,使用 RDB 還可以避免 AOF 程序的 bug。

  • 如果你只希望你的數據在服務器運行的時候存在,你也可以不使用任何持久化方式。

■Redis 的數據恢復

Redis 的數據恢復有着如下的優先級:

  1. 如果只配置 AOF ,重啓時加載 AOF 文件恢復數據;

  2. 如果同時配置了 RDB 和 AOF ,啓動只加載 AOF 文件恢復數據;

  3. 如果只配置 RDB,啓動將加載 dump 文件恢復數據。

拷貝 AOF 文件到 Redis 的數據目錄,啓動 redis-server AOF 的數據恢復過程:Redis 虛擬一個客戶端,讀取 AOF 文件恢復 Redis 命令和參數,然後執行命令從而恢復數據,這些過程主要在 loadAppendOnlyFile() 中實現。

拷貝 RDB 文件到 Redis 的數據目錄,啓動 redis-server 即可,因爲 RDB 文件和重啓前保存的是真實數據而不是命令狀態和參數。

四、集羣篇

■主從同步瞭解嗎?

主從複製,是指將一臺 Redis 服務器的數據,複製到其他的 Redis 服務器。前者稱爲 主節點(master),後者稱爲 從節點(slave)。且數據的複製是 單向 的,只能由主節點到從節點。Redis 主從複製支持 主從同步 和 從從同步 兩種,後者是 Redis 後續版本新增的功能,以減輕主節點的同步負擔。

主從複製主要的作用

  • 數據冗餘: 主從複製實現了數據的熱備份,是持久化之外的一種數據冗餘方式。

  • 故障恢復: 當主節點出現問題時,可以由從節點提供服務,實現快速的故障恢復 (實際上是一種服務的冗餘)

  • 負載均衡: 在主從複製的基礎上,配合讀寫分離,可以由主節點提供寫服務,由從節點提供讀服務 (即寫 Redis 數據時應用連接主節點,讀 Redis 數據時應用連接從節點),分擔服務器負載。尤其是在寫少讀多的場景下,通過多個從節點分擔讀負載,可以大大提高 Redis 服務器的併發量。

  • 高可用基石: 除了上述作用以外,主從複製還是哨兵和集羣能夠實施的 基礎,因此說主從複製是 Redis 高可用的基礎。

實現原理

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1uploading.4e448015.gif轉存失敗重新上傳取消

爲了節省篇幅,我把主要的步驟都 濃縮 在了上圖中,其實也可以 簡化成三個階段:準備階段-數據同步階段-命令傳播階段

更多細節 推薦閱讀 之前的系列文章,不僅有原理講解,還有實戰環節:

■哨兵模式瞭解嗎?

上圖 展示了一個典型的哨兵架構圖,它由兩部分組成,哨兵節點和數據節點:

  • 哨兵節點: 哨兵系統由一個或多個哨兵節點組成,哨兵節點是特殊的 Redis 節點,不存儲數據;

  • 數據節點: 主節點和從節點都是數據節點;

在複製的基礎上,哨兵實現了 自動化的故障恢復 功能,下方是官方對於哨兵功能的描述:

  • 監控(Monitoring): 哨兵會不斷地檢查主節點和從節點是否運作正常。

  • 自動故障轉移(Automatic failover): 當 主節點 不能正常工作時,哨兵會開始 自動故障轉移操作,它會將失效主節點的其中一個 從節點升級爲新的主節點,並讓其他從節點改爲複製新的主節點。

  • 配置提供者(Configuration provider): 客戶端在初始化時,通過連接哨兵來獲得當前 Redis 服務的主節點地址。

  • 通知(Notification): 哨兵可以將故障轉移的結果發送給客戶端。

其中,監控和自動故障轉移功能,使得哨兵可以及時發現主節點故障並完成轉移。而配置提供者和通知功能,則需要在與客戶端的交互中才能體現。

新的主服務器是怎樣被挑選出來的?

故障轉移操作的第一步 要做的就是在已下線主服務器屬下的所有從服務器中,挑選出一個狀態良好、數據完整的從服務器,然後向這個從服務器發送 slaveof no one 命令,將這個從服務器轉換爲主服務器。但是這個從服務器是怎麼樣被挑選出來的呢?

簡單來說 Sentinel 使用以下規則來選擇新的主服務器:

  1. 在失效主服務器屬下的從服務器當中, 那些被標記爲主觀下線、已斷線、或者最後一次回覆 PING 命令的時間大於五秒鐘的從服務器都會被 淘汰

  2. 在失效主服務器屬下的從服務器當中, 那些與失效主服務器連接斷開的時長超過 down-after 選項指定的時長十倍的從服務器都會被 淘汰

  3. 在 經歷了以上兩輪淘汰之後 剩下來的從服務器中, 我們選出 複製偏移量(replication offset)最大 的那個 從服務器 作爲新的主服務器;如果複製偏移量不可用,或者從服務器的複製偏移量相同,那麼 帶有最小運行 ID 的那個從服務器成爲新的主服務器。

更多細節 推薦閱讀 之前的系列文章,不僅有原理講解,還有實戰環節:

■Redis 集羣使用過嗎?原理?

上圖 展示了 Redis Cluster 典型的架構圖,集羣中的每一個 Redis 節點都 互相兩兩相連,客戶端任意 直連 到集羣中的 任意一臺,就可以對其他 Redis 節點進行 讀寫 的操作。

基本原理

Redis 集羣中內置了 16384 個哈希槽。當客戶端連接到 Redis 集羣之後,會同時得到一份關於這個 集羣的配置信息,當客戶端具體對某一個 key 值進行操作時,會計算出它的一個 Hash 值,然後把結果對 16384  求餘數,這樣每個 key 都會對應一個編號在 0-16383 之間的哈希槽,Redis 會根據節點數量 大致均等 的將哈希槽映射到不同的節點。

再結合集羣的配置信息就能夠知道這個 key 值應該存儲在哪一個具體的 Redis 節點中,如果不屬於自己管,那麼就會使用一個特殊的 MOVED 命令來進行一個跳轉,告訴客戶端去連接這個節點以獲取數據:

GET x
-MOVED 3999 127.0.0.1:6381

MOVED 指令第一個參數 3999 是 key 對應的槽位編號,後面是目標節點地址,MOVED 命令前面有一個減號,表示這是一個錯誤的消息。客戶端在收到 MOVED 指令後,就立即糾正本地的 槽位映射表,那麼下一次再訪問 key 時就能夠到正確的地方去獲取了。

集羣的主要作用

  1. 數據分區: 數據分區 (或稱數據分片) 是集羣最核心的功能。集羣將數據分散到多個節點,一方面 突破了 Redis 單機內存大小的限制,存儲容量大大增加另一方面 每個主節點都可以對外提供讀服務和寫服務,大大提高了集羣的響應能力。Redis 單機內存大小受限問題,在介紹持久化和主從複製時都有提及,例如,如果單機內存太大,bgsave 和 bgrewriteaof 的 fork 操作可能導致主進程阻塞,主從環境下主機切換時可能導致從節點長時間無法提供服務,全量複製階段主節點的複製緩衝區可能溢出……

  2. 高可用: 集羣支持主從複製和主節點的 自動故障轉移 (與哨兵類似),當任一節點發生故障時,集羣仍然可以對外提供服務。

■集羣中數據如何分區?

Redis 採用方案三。

方案一:哈希值 % 節點數

哈希取餘分區思路非常簡單:計算 key 的 hash 值,然後對節點數量進行取餘,從而決定數據映射到哪個節點上。

不過該方案最大的問題是,當新增或刪減節點時,節點數量發生變化,系統中所有的數據都需要 重新計算映射關係,引發大規模數據遷移。

方案二:一致性哈希分區

一致性哈希算法將 整個哈希值空間 組織成一個虛擬的圓環,範圍是 [0 - 232 - 1],對於每一個數據,根據 key 計算 hash 值,確數據在環上的位置,然後從此位置沿順時針行走,找到的第一臺服務器就是其應該映射到的服務器:

與哈希取餘分區相比,一致性哈希分區將 增減節點的影響限制在相鄰節點。以上圖爲例,如果在 node1 和 node2 之間增加 node5,則只有 node2 中的一部分數據會遷移到node5;如果去掉 node2,則原 node2 中的數據只會遷移到 node4 中,只有 node4 會受影響。

一致性哈希分區的主要問題在於,當 節點數量較少 時,增加或刪減節點,對單個節點的影響可能很大,造成數據的嚴重不平衡。還是以上圖爲例,如果去掉 node2node4 中的數據由總數據的 1/4 左右變爲 1/2 左右,與其他節點相比負載過高。

方案三:帶有虛擬節點的一致性哈希分區

該方案在 一致性哈希分區的基礎上,引入了 虛擬節點 的概念。Redis 集羣使用的便是該方案,其中的虛擬節點稱爲 槽(slot)。槽是介於數據和實際節點之間的虛擬概念,每個實際節點包含一定數量的槽,每個槽包含哈希值在一定範圍內的數據。

在使用了槽的一致性哈希分區中,槽是數據管理和遷移的基本單位。槽 解耦 了 數據和實際節點 之間的關係,增加或刪除節點對系統的影響很小。仍以上圖爲例,系統中有 4 個實際節點,假設爲其分配 16 個槽(0-15);

  • 槽 0-3 位於 node1;4-7 位於 node2;以此類推....

如果此時刪除 node2,只需要將槽 4-7 重新分配即可,例如槽 4-5 分配給 node1,槽 6 分配給 node3,槽 7 分配給 node4;可以看出刪除 node2 後,數據在其他節點的分佈仍然較爲均衡。

■節點之間的通信機制瞭解嗎?

集羣的建立離不開節點之間的通信,例如我們在 快速體驗 中剛啓動六個集羣節點之後通過 redis-cli 命令幫助我們搭建起來了集羣,實際上背後每個集羣之間的兩兩連接是通過了 CLUSTER MEET <ip> <port> 命令發送 MEET 消息完成的,下面我們展開詳細說說。

兩個端口

在 哨兵系統 中,節點分爲 數據節點 和 哨兵節點:前者存儲數據,後者實現額外的控制功能。在 集羣 中,沒有數據節點與非數據節點之分:所有的節點都存儲數據,也都參與集羣狀態的維護。爲此,集羣中的每個節點,都提供了兩個 TCP 端口:

  • 普通端口: 即我們在前面指定的端口 (7000等)。普通端口主要用於爲客戶端提供服務(與單機節點類似);但在節點間數據遷移時也會使用。

  • 集羣端口: 端口號是普通端口 + 10000 (10000是固定值,無法改變),如 7000 節點的集羣端口爲 17000集羣端口只用於節點之間的通信,如搭建集羣、增減節點、故障轉移等操作時節點間的通信;不要使用客戶端連接集羣接口。爲了保證集羣可以正常工作,在配置防火牆時,要同時開啓普通端口和集羣端口。

Gossip 協議

節點間通信,按照通信協議可以分爲幾種類型:單對單、廣播、Gossip 協議等。重點是廣播和 Gossip 的對比。

  • 廣播是指向集羣內所有節點發送消息。優點 是集羣的收斂速度快(集羣收斂是指集羣內所有節點獲得的集羣信息是一致的),缺點 是每條消息都要發送給所有節點,CPU、帶寬等消耗較大。

  • Gossip 協議的特點是:在節點數量有限的網絡中,每個節點都 “隨機” 的與部分節點通信(並不是真正的隨機,而是根據特定的規則選擇通信的節點),經過一番雜亂無章的通信,每個節點的狀態很快會達到一致。Gossip 協議的 優點 有負載 (比廣播) 低、去中心化、容錯性高 (因爲通信有冗餘) 等;缺點 主要是集羣的收斂速度慢。

消息類型

集羣中的節點採用 固定頻率(每秒10次) 的 定時任務 進行通信相關的工作:判斷是否需要發送消息及消息類型、確定接收節點、發送消息等。如果集羣狀態發生了變化,如增減節點、槽狀態變更,通過節點間的通信,所有節點會很快得知整個集羣的狀態,使集羣收斂。

節點間發送的消息主要分爲 5 種:meet 消息ping 消息pong 消息fail 消息publish 消息。不同的消息類型,通信協議、發送的頻率和時機、接收節點的選擇等是不同的:

  • MEET 消息: 在節點握手階段,當節點收到客戶端的 CLUSTER MEET 命令時,會向新加入的節點發送 MEET 消息,請求新節點加入到當前集羣;新節點收到 MEET 消息後會回覆一個 PONG 消息。

  • PING 消息: 集羣裏每個節點每秒鐘會選擇部分節點發送 PING 消息,接收者收到消息後會回覆一個 PONG 消息。PING 消息的內容是自身節點和部分其他節點的狀態信息,作用是彼此交換信息,以及檢測節點是否在線。PING 消息使用 Gossip 協議發送,接收節點的選擇兼顧了收斂速度和帶寬成本,具體規則如下:(1)隨機找 5 個節點,在其中選擇最久沒有通信的 1 個節點;(2)掃描節點列表,選擇最近一次收到 PONG 消息時間大於 cluster_node_timeout / 2 的所有節點,防止這些節點長時間未更新。

  • PONG消息: PONG 消息封裝了自身狀態數據。可以分爲兩種:第一種 是在接到 MEET/PING 消息後回覆的 PONG 消息;第二種 是指節點向集羣廣播 PONG 消息,這樣其他節點可以獲知該節點的最新信息,例如故障恢復後新的主節點會廣播 PONG 消息。

  • FAIL 消息: 當一個主節點判斷另一個主節點進入 FAIL 狀態時,會向集羣廣播這一FAIL 消息;接收節點會將這一 FAIL 消息保存起來,便於後續的判斷。

  • PUBLISH 消息: 節點收到 PUBLISH 命令後,會先執行該命令,然後向集羣廣播這一消息,接收節點也會執行該 PUBLISH 命令。

■集羣數據如何存儲的有了解嗎?

節點需要專門的數據結構來存儲集羣的狀態。所謂集羣的狀態,是一個比較大的概念,包括:集羣是否處於上線狀態、集羣中有哪些節點、節點是否可達、節點的主從狀態、槽的分佈……

節點爲了存儲集羣狀態而提供的數據結構中,最關鍵的是 clusterNode 和 clusterState 結構:前者記錄了一個節點的狀態,後者記錄了集羣作爲一個整體的狀態。

clusterNode 結構

clusterNode 結構保存了 一個節點的當前狀態,包括創建時間、節點 id、ip 和端口號等。每個節點都會用一個 clusterNode 結構記錄自己的狀態,併爲集羣內所有其他節點都創建一個 clusterNode 結構來記錄節點狀態。

下面列舉了 clusterNode 的部分字段,並說明了字段的含義和作用:

typedef struct clusterNode {
    //節點創建時間
    mstime_t ctime;
    //節點id
    char name[REDIS_CLUSTER_NAMELEN];
    //節點的ip和端口號
    char ip[REDIS_IP_STR_LEN];
    int port;
    //節點標識:整型,每個bit都代表了不同狀態,如節點的主從狀態、是否在線、是否在握手等
    int flags;
    //配置紀元:故障轉移時起作用,類似於哨兵的配置紀元
    uint64_t configEpoch;
    //槽在該節點中的分佈:佔用16384/8個字節,16384個比特;每個比特對應一個槽:比特值爲1,則該比特對應的槽在節點中;比特值爲0,則該比特對應的槽不在節點中
    unsigned char slots[16384/8];
    //節點中槽的數量
    int numslots;
    …………
} clusterNode;

除了上述字段,clusterNode 還包含節點連接、主從複製、故障發現和轉移需要的信息等。

clusterState 結構

clusterState 結構保存了在當前節點視角下,集羣所處的狀態。主要字段包括:

typedef struct clusterState {
    //自身節點
    clusterNode *myself;
    //配置紀元
    uint64_t currentEpoch;
    //集羣狀態:在線還是下線
    int state;
    //集羣中至少包含一個槽的節點數量
    int size;
    //哈希表,節點名稱->clusterNode節點指針
    dict *nodes;
    //槽分佈信息:數組的每個元素都是一個指向clusterNode結構的指針;如果槽還沒有分配給任何節點,則爲NULL
    clusterNode *slots[16384];
    …………
} clusterState;

除此之外,clusterState 還包括故障轉移、槽遷移等需要的信息。

五、其他問題

■Redis 如何實現分佈式鎖?

推薦閱讀 之前的系列文章:Redis(3)——分佈式鎖深入探究

■Redis 過期鍵的刪除策略?

簡單描述

先拋開 Redis 想一下幾種可能的刪除策略:

  1. 定時刪除:在設置鍵的過期時間的同時,創建一個定時器 timer). 讓定時器在鍵的過期時間來臨時,立即執行對鍵的刪除操作。

  2. 惰性刪除:放任鍵過期不管,但是每次從鍵空間中獲取鍵時,都檢查取得的鍵是否過期,如果過期的話,就刪除該鍵;如果沒有過期,就返回該鍵。

  3. 定期刪除:每隔一段時間程序就對數據庫進行一次檢查,刪除裏面的過期鍵。至於要刪除多少過期鍵,以及要檢查多少個數據庫,則由算法決定。

在上述的三種策略中定時刪除和定期刪除屬於不同時間粒度的 主動刪除,惰性刪除屬於 被動刪除

三種策略都有各自的優缺點

  1. 定時刪除對內存使用率有優勢,但是對 CPU 不友好;

  2. 惰性刪除對內存不友好,如果某些鍵值對一直不被使用,那麼會造成一定量的內存浪費;

  3. 定期刪除是定時刪除和惰性刪除的折中。

Redis 中的實現

Reids 採用的是 惰性刪除和定時刪除 的結合,一般來說可以藉助最小堆來實現定時器,不過 Redis 的設計考慮到時間事件的有限種類和數量,使用了無序鏈表存儲時間事件,這樣如果在此基礎上實現定時刪除,就意味着 O(N) 遍歷獲取最近需要刪除的數據。

■Redis 的淘汰策略有哪些?

Redis 有六種淘汰策略

 

策略 描述
volatile-lru 從已設置過期時間的 KV 集中優先對最近最少使用(less recently used)的數據淘汰
volitile-ttl 從已設置過期時間的 KV 集中優先對剩餘時間短(time to live)的數據淘汰
volitile-random 從已設置過期時間的 KV 集中隨機選擇數據淘汰
allkeys-lru 從所有 KV 集中優先對最近最少使用(less recently used)的數據淘汰
allKeys-random 從所有 KV 集中隨機選擇數據淘汰
noeviction 不淘汰策略,若超過最大內存,返回錯誤信息

 

4.0 版本後增加以下兩種

  • volatile-lfu:從已設置過期時間的數據集(server.db[i].expires)中挑選最不經常使用的數據淘汰

  • allkeys-lfu:當內存不足以容納新寫入數據時,在鍵空間中,移除最不經常使用的 key

■Redis常見性能問題和解決方案?

  1. Master 最好不要做任何持久化工作,包括內存快照和 AOF 日誌文件,特別是不要啓用內存快照做持久化。

  2. 如果數據比較關鍵,某個 Slave 開啓 AOF 備份數據,策略爲每秒同步一次。

  3. 爲了主從複製的速度和連接的穩定性,Slave 和 Master 最好在同一個局域網內。

  4. 儘量避免在壓力較大的主庫上增加從庫。

  5. Master 調用 BGREWRITEAOF 重寫 AOF 文件,AOF 在重寫的時候會佔大量的 CPU 和內存資源,導致服務 load 過高,出現短暫服務暫停現象。

  6. 爲了 Master 的穩定性,主從複製不要用圖狀結構,用單向鏈表結構更穩定,即主從關係爲:Master<–Slave1<–Slave2<–Slave3…,這樣的結構也方便解決單點故障問題,實現 Slave 對 Master 的替換,也即,如果 Master 掛了,可以立馬啓用 Slave1 做 Master,其他不變。

■假如Redis裏面有1億個key,其中有10w個key是以某個固定的已知的前綴開頭的,如何將它們全部找出來?

使用 keys 指令可以掃出指定模式的 key 列表。但是要注意 keys 指令會導致線程阻塞一段時間,線上服務會停頓,直到指令執行完畢,服務才能恢復。這個時候可以使用 scan 指令,scan 指令可以無阻塞的提取出指定模式的 key 列表,但是會有一定的重複概率,在客戶端做一次去重就可以了,但是整體所花費的時間會比直接用 keys 指令長。

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