媽媽不再擔憂我面試被Redis問得臉都綠了

長文前排提醒,收藏向前排提醒,素質三連 (轉發 + 在看 + 留言) 前排提醒!html

前言

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

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

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

  • Ps: 我把 重要的知識點 都作成了 圖片,但願各位 "用餐愉快"(不錯記得付餐費.. 點個贊留個言..)

1、基礎篇

什麼是 Redis ?

先解釋 Redis 基本概念

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

簡單提一下 Redis 數據結構

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

必定要說出一些高級數據結構 (固然你本身也要了解.. 下面會說到的別擔憂),這樣面試官的眼睛纔會亮。web

Redis 小總結

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

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

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:93: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 爲何這麼快?

簡單總結:

  1. 純內存操做:讀取不須要進行磁盤 I/O,因此比傳統數據庫要快上很多; (但不要有誤區說磁盤就必定慢,例如 Kafka 就是使用磁盤順序讀取但仍然較快)
  2. 單線程,無鎖競爭:這保證了沒有線程的上下文切換,不會由於多線程的一些操做而下降性能;
  3. 多路 I/O 複用模型,非阻塞 I/O:採用多路 I/O 複用技術可讓單個線程高效的處理多個網絡鏈接請求(儘可能減小網絡 IO 的時間消耗);
  4. 高效的數據結構,加上底層作了大量優化:Redis 對於底層的數據結構和內存佔用作了大量的優化,例如不一樣長度的字符串使用不一樣的結構體表示,HyperLogLog 的密集型存儲結構等等..

2、數據結構篇

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

首先在 Redis 內部會使用一個 RedisObject 對象來表示全部的 keyvalue

其次 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 須要對字符串進行修改時,首先借助於 lenalloc 檢查空間是否知足修改所需的要求,若是空間不夠的話,SDS 會自動擴展空間,避免了像 C 字符串操做中的覆蓋狀況;
  3. 有效下降內存分配次數:C 字符串在涉及增長或者清除操做時會改變底層數組的大小形成從新分配,SDS 使用了 空間預分配惰性空間釋放 機制,簡單理解就是每次在擴展時是成倍的多分配的,在縮容是也是先留着並不正式歸還給 OS;
  4. 二進制安全:C 語言字符串只能保存 ascii 碼,對於圖片、音頻等信息沒法保存,SDS 是二進制安全的,寫入什麼讀取就是什麼,不作任何過濾和限制;

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

先整體聊一下 Redis 中的字典

字典是 Redis 服務器中出現最爲頻繁的複合型數據結構。除了 hash 結構的數據會用到字典外,整個 Redis 數據庫的全部 keyvalue 也組成了一個 全局字典,還有帶過時時間的 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 爲了節約內存 而使用的一種數據結構,zsethash 容器對象會在元素個數較少的時候,採用壓縮列表(ziplist)進行存儲。壓縮列表是 一塊連續的內存空間,元素之間緊挨着存儲,沒有任何冗餘空隙。

由於以前本身也沒有學習過,因此找了一篇比較比如較容易理解的文章:

快速列表 quicklist 瞭解嗎?

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

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

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

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 作了詳細的描述

3、持久化篇

什麼是持久化?

先簡單談一談是什麼

Redis 的數據 所有存儲內存 中,若是 忽然宕機,數據就會所有丟失,所以必須有一套機制來保證 Redis 的數據不會由於故障而丟失,這種機制就是 Redis 的 持久化機制,它會將內存中的數據庫狀態 保存到磁盤 中。

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

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

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

  1. 客戶端向數據庫 發送寫命令 (數據在客戶端的內存中)
  2. 數據庫 接收 到客戶端的 寫請求 (數據在服務器的內存中)
  3. 數據庫 調用系統 API 將數據寫入磁盤 (數據在內核緩衝區中)
  4. 操做系統將 寫緩衝區 傳輸到 磁盤控控制器 (數據在磁盤緩存中)
  5. 操做系統的磁盤控制器將數據 寫入實際的物理媒介(數據在磁盤中)

分析如何保證持久化安全

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

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

機房」火了「
機房」火了「

因此咱們能夠總結得出數據安全最重要的階段是:步驟3、4、五,即:

  • 數據庫軟件調用寫操做將用戶空間的緩衝區轉移到內核緩衝區的頻率是多少?
  • 內核多久從緩衝區取數據刷新到磁盤控制器?
  • 磁盤控制器多久把數據寫入物理媒介一次?
  • 注意: 若是真的發生災難性的事件,咱們能夠從上圖的過程當中看到,任何一步均可能被意外打斷丟失,因此只能 儘量地保證 數據的安全,這對於全部數據庫來講都是同樣的。

咱們從 第三步 開始。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 來完成 第三步,可是對於成功返回以前,咱們對系統調用花費的時間沒有太多的控制權。

而後咱們來講說 第四步。咱們知道,除了早期對電腦特別瞭解那幫人 (操做系統就這幫人搞的),實際的物理硬件都不是咱們可以 直接操做 的,都是經過 操做系統調用 來達到目的的。爲了防止過慢的 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 文件和重啓前保存的是真實數據而不是命令狀態和參數。

4、集羣篇

主從同步瞭解嗎?

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

主從複製主要的做用

  • 數據冗餘: 主從複製實現了數據的熱備份,是持久化以外的一種數據冗餘方式。
  • 故障恢復: 當主節點出現問題時,能夠由從節點提供服務,實現快速的故障恢復 (其實是一種服務的冗餘)
  • 負載均衡: 在主從複製的基礎上,配合讀寫分離,能夠由主節點提供寫服務,由從節點提供讀服務 (即寫 Redis 數據時應用鏈接主節點,讀 Redis 數據時應用鏈接從節點),分擔服務器負載。尤爲是在寫少讀多的場景下,經過多個從節點分擔讀負載,能夠大大提升 Redis 服務器的併發量。
  • 高可用基石: 除了上述做用之外,主從複製仍是哨兵和集羣可以實施的 基礎,所以說主從複製是 Redis 高可用的基礎。

實現原理

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

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

哨兵模式瞭解嗎?

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

  • 哨兵節點: 哨兵系統由一個或多個哨兵節點組成,哨兵節點是特殊的 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 指令第一個參數 3999key 對應的槽位編號,後面是目標節點地址,MOVED 命令前面有一個減號,表示這是一個錯誤的消息。客戶端在收到 MOVED 指令後,就當即糾正本地的 槽位映射表,那麼下一次再訪問 key 時就可以到正確的地方去獲取了。

集羣的主要做用

  1. 數據分區: 數據分區 (或稱數據分片) 是集羣最核心的功能。集羣將數據分散到多個節點, 一方面 突破了 Redis 單機內存大小的限制, 存儲容量大大增長另外一方面 每一個主節點均可以對外提供讀服務和寫服務, 大大提升了集羣的響應能力。Redis 單機內存大小受限問題,在介紹持久化和主從複製時都有說起,例如,若是單機內存太大, bgsavebgrewriteaoffork 操做可能致使主進程阻塞,主從環境下主機切換時可能致使從節點長時間沒法提供服務,全量複製階段主節點的複製緩衝區可能溢出……
  2. 高可用: 集羣支持主從複製和主節點的 自動故障轉移 (與哨兵相似),當任一節點發生故障時,集羣仍然能夠對外提供服務。

集羣中數據如何分區?

Redis 採用方案三。

方案一:哈希值 % 節點數

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

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

方案二:一致性哈希分區

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

與哈希取餘分區相比,一致性哈希分區將 增減節點的影響限制在相鄰節點。以上圖爲例,若是在 node1node2 之間增長 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 命令。

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

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

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

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 還包括故障轉移、槽遷移等須要的信息。

5、其餘問題

Redis 如何實現分佈式鎖?

推薦閱讀 以前的系列文章: Redis(3)——分佈式鎖深刻探究 - www.wmyskxz.com/2020/03/01/…

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 指令長。

More..

參考資料

  1. 3w字深度好文|Redis面試全攻略,讀完這個就能夠和麪試官大戰幾個回合了 - mp.weixin.qq.com/s/f9N13fnyT…
  2. 大廠面試!我和麪試官之間關於Redis的一場對弈! - [https://mp.weixin.qq.com/s/DHTPSfmWTZpdTmlytzLz1g](https://mp.weixin.qq.com/s/DHTPSfmWTZpdTmlytzLz1g
  3. Redis面試題(2020最新版) - blog.csdn.net/ThinkWon/ar…
  • 本文已收錄至個人 Github 程序員成長系列 【More Than Java】,學習,不止 Code,歡迎 star:github.com/wmyskxz/Mor…
  • 我的公衆號 :wmyskxz, 我的獨立域名博客:wmyskxz.com,堅持原創輸出,下方掃碼關注,2020,與您共同成長!

很是感謝各位人才能 看到這裏,若是以爲本篇文章寫得不錯,以爲 「我沒有三顆心臟」有點東西 的話,求點贊,求關注,求分享,求留言!

創做不易,各位的支持和承認,就是我創做的最大動力,咱們下篇文章見!

相關文章
相關標籤/搜索