面渣逆襲:Redis連環五十二問,圖文詳解,這下面試穩了!

三分惡發表於2022-05-05

大家好,我是老三,面渣逆襲系列繼續,這節我們來搞定Redis——不會有人假期玩去了吧?不會吧?

基礎

1.說說什麼是Redis?

Redis圖示

Redis是一種基於鍵值對(key-value)的NoSQL資料庫。

比一般鍵值對資料庫強大的地方,Redis中的value支援string(字串)、hash(雜湊)、 list(列表)、set(集合)、zset(有序集合)、Bitmaps(點陣圖)、 HyperLogLog、GEO(地理資訊定位)等多種資料結構,因此 Redis可以滿足很多的應用場景。

而且因為Redis會將所有資料都存放在記憶體中,所以它的讀寫效能非常出色。

不僅如此,Redis還可以將記憶體的資料利用快照和日誌的形式儲存到硬碟上,這樣在發生類似斷電或者機器故障的時候,記憶體中的資料不會“丟失”。

除了上述功能以外,Redis還提供了鍵過期、釋出訂閱、事務、流水線、Lua指令碼等附加功能。

總之,Redis是一款強大的效能利器。

2.Redis可以用來幹什麼?

Redis

  1. 快取

    這是Redis應用最廣泛地方,基本所有的Web應用都會使用Redis作為快取,來降低資料來源壓力,提高響應速度。
    Redis快取

  2. 計數器
    Redis天然支援計數功能,而且計數效能非常好,可以用來記錄瀏覽量、點贊量等等。

  3. 排行榜
    Redis提供了列表和有序集合資料結構,合理地使用這些資料結構可以很方便地構建各種排行榜系統。

  4. 社交網路
    贊/踩、粉絲、共同好友/喜好、推送、下拉重新整理。

  5. 訊息佇列
    Redis提供了釋出訂閱功能和阻塞佇列的功能,可以滿足一般訊息佇列功能。

  6. 分散式鎖
    分散式環境下,利用Redis實現分散式鎖,也是Redis常見的應用。

Redis的應用一般會結合專案去問,以一個電商專案的使用者服務為例:

  • Token儲存:使用者登入成功之後,使用Redis儲存Token
  • 登入失敗次數計數:使用Redis計數,登入失敗超過一定次數,鎖定賬號
  • 地址快取:對省市區資料的快取
  • 分散式鎖:分散式環境下登入、註冊等操作加分散式鎖
  • ……

3.Redis 有哪些資料結構?

Redis基本資料結構
Redis有五種基本資料結構。

string

字串最基礎的資料結構。字串型別的值實際可以是字串(簡單的字串、複雜的字串(例如JSON、XML))、數字 (整數、浮點數),甚至是二進位制(圖片、音訊、視訊),但是值最大不能超過512MB。

字串主要有以下幾個典型使用場景:

  • 快取功能
  • 計數
  • 共享Session
  • 限速

hash

雜湊型別是指鍵值本身又是一個鍵值對結構。

雜湊主要有以下典型應用場景:

  • 快取使用者資訊
  • 快取物件

list

列表(list)型別是用來儲存多個有序的字串。列表是一種比較靈活的資料結構,它可以充當棧和佇列的角色

列表主要有以下幾種使用場景:

  • 訊息佇列
  • 文章列表

set

集合(set)型別也是用來儲存多個的字串元素,但和列表型別不一 樣的是,集合中不允許有重複元素,並且集合中的元素是無序的。

集合主要有如下使用場景:

  • 標籤(tag)
  • 共同關注

sorted set

有序集合中的元素可以排序。但是它和列表使用索引下標作為排序依據不同的是,它給每個元素設定一個權重(score)作為排序的依據。

有序集合主要應用場景:

  • 使用者點贊統計
  • 使用者排序

4.Redis為什麼快呢?

Redis的速度⾮常的快,單機的Redis就可以⽀撐每秒十幾萬的併發,相對於MySQL來說,效能是MySQL的⼏⼗倍。速度快的原因主要有⼏點:

  1. 完全基於記憶體操作
  2. 使⽤單執行緒,避免了執行緒切換和競態產生的消耗
  3. 基於⾮阻塞的IO多路復⽤機制
  4. C語⾔實現,優化過的資料結構,基於⼏種基礎的資料結構,redis做了⼤量的優化,效能極⾼
    Redis使用IO多路複用和自身事件模型

5.能說一下I/O多路複用嗎?

引用知乎上一個高讚的回答來解釋什麼是I/O多路複用。假設你是一個老師,讓30個學生解答一道題目,然後檢查學生做的是否正確,你有下面幾個選擇:

  • 第一種選擇:按順序逐個檢查,先檢查A,然後是B,之後是C、D。。。這中間如果有一個學生卡住,全班都會被耽誤。這種模式就好比,你用迴圈挨個處理socket,根本不具有併發能力。

  • 第二種選擇:你建立30個分身,每個分身檢查一個學生的答案是否正確。 這種類似於為每一個使用者建立一個程式或者- 執行緒處理連線。

  • 第三種選擇,你站在講臺上等,誰解答完誰舉手。這時C、D舉手,表示他們解答問題完畢,你下去依次檢查C、D的答案,然後繼續回到講臺上等。此時E、A又舉手,然後去處理E和A。

第一種就是阻塞IO模型,第三種就是I/O複用模型。

多路複用模型

Linux系統有三種方式實現IO多路複用:select、poll和epoll。

例如epoll方式是將使用者socket對應的fd註冊進epoll,然後epoll幫你監聽哪些socket上有訊息到達,這樣就避免了大量的無用操作。此時的socket應該採用非阻塞模式。

這樣,整個過程只在進行select、poll、epoll這些呼叫的時候才會阻塞,收發客戶訊息是不會阻塞的,整個程式或者執行緒就被充分利用起來,這就是事件驅動,所謂的reactor模式。

6. Redis為什麼早期選擇單執行緒?

官方解釋:https://redis.io/topics/faq

官方單執行緒解釋
官方FAQ表示,因為Redis是基於記憶體的操作,CPU成為Redis的瓶頸的情況很少見,Redis的瓶頸最有可能是記憶體的大小或者網路限制。

如果想要最大程度利用CPU,可以在一臺機器上啟動多個Redis例項。

PS:網上有這樣的回答,吐槽官方的解釋有些敷衍,其實就是歷史原因,開發者嫌多執行緒麻煩,後來這個CPU的利用問題就被拋給了使用者。

同時FAQ裡還提到了, Redis 4.0 之後開始變成多執行緒,除了主執行緒外,它也有後臺執行緒在處理一些較為緩慢的操作,例如清理髒資料、無用連線的釋放、大 Key 的刪除等等。

7.Redis6.0使用多執行緒是怎麼回事?

Redis不是說用單執行緒的嗎?怎麼6.0成了多執行緒的?

Redis6.0的多執行緒是用多執行緒來處理資料的讀寫和協議解析,但是Redis執行命令還是單執行緒的。

Redis6.0多執行緒
這樣做的⽬的是因為Redis的效能瓶頸在於⽹絡IO⽽⾮CPU,使⽤多執行緒能提升IO讀寫的效率,從⽽整體提⾼Redis的效能。

持久化

8.Redis持久化⽅式有哪些?有什麼區別?

Redis持久化⽅案分為RDB和AOF兩種。
Redis持久化兩種方式

RDB

RDB持久化是把當前程式資料生成快照儲存到硬碟的過程,觸發RDB持久化過程分為手動觸發和自動觸發。

RDB⽂件是⼀個壓縮的⼆進位制⽂件,通過它可以還原某個時刻資料庫的狀態。由於RDB⽂件是儲存在硬碟上的,所以即使Redis崩潰或者退出,只要RDB⽂件存在,就可以⽤它來恢復還原資料庫的狀態。

手動觸發分別對應save和bgsave命令:
save和bgsave

  • save命令:阻塞當前Redis伺服器,直到RDB過程完成為止,對於記憶體比較大的例項會造成長時間阻塞,線上環境不建議使用。

  • bgsave命令:Redis程式執行fork操作建立子程式,RDB持久化過程由子程式負責,完成後自動結束。阻塞只發生在fork階段,一般時間很短。

以下場景會自動觸發RDB持久化:

  • 使用save相關配置,如“save m n”。表示m秒內資料集存在n次修改時,自動觸發bgsave。
  • 如果從節點執行全量複製操作,主節點自動執行bgsave生成RDB檔案併傳送給從節點
  • 執行debug reload命令重新載入Redis時,也會自動觸發save操作
  • 預設情況下執行shutdown命令時,如果沒有開啟AOF持久化功能則自動執行bgsave。

AOF

AOF(append only file)持久化:以獨立日誌的方式記錄每次寫命令, 重啟時再重新執行AOF檔案中的命令達到恢復資料的目的。AOF的主要作用是解決了資料持久化的實時性,目前已經是Redis持久化的主流方式。

AOF的工作流程操作:命令寫入 (append)、檔案同步(sync)、檔案重寫(rewrite)、重啟載入 (load)
AOF工作流程流程如下:

1)所有的寫入命令會追加到aof_buf(緩衝區)中。

2)AOF緩衝區根據對應的策略向硬碟做同步操作。

3)隨著AOF檔案越來越大,需要定期對AOF檔案進行重寫,達到壓縮 的目的。

4)當Redis伺服器重啟時,可以載入AOF檔案進行資料恢復。

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

RDB | 優點

  1. 只有一個緊湊的二進位制檔案 dump.rdb,非常適合備份、全量複製的場景。
  2. 容災性好,可以把RDB檔案拷貝道遠端機器或者檔案系統張,用於容災恢復。
  3. 恢復速度快,RDB恢復資料的速度遠遠快於AOF的方式

RDB | 缺點

  1. 實時性低,RDB 是間隔一段時間進行持久化,沒法做到實時持久化/秒級持久化。如果在這一間隔事件發生故障,資料會丟失。
  2. 存在相容問題,Redis演進過程存在多個格式的RDB版本,存在老版本Redis無法相容新版本RDB的問題。

AOF | 優點

  1. 實時性好,aof 持久化可以配置 appendfsync 屬性,有 always,每進行一次命令操作就記錄到 aof 檔案中一次。
  2. 通過 append 模式寫檔案,即使中途伺服器當機,可以通過 redis-check-aof 工具解決資料一致性問題。

AOF | 缺點

  1. AOF 檔案比 RDB 檔案大,且 恢復速度慢
  2. 資料集大 的時候,比 RDB 啟動效率低

10.RDB和AOF如何選擇?

  • 一般來說, 如果想達到足以媲美資料庫的 資料安全性,應該 同時使用兩種持久化功能。在這種情況下,當 Redis 重啟的時候會優先載入 AOF 檔案來恢復原始的資料,因為在通常情況下 AOF 檔案儲存的資料集要比 RDB 檔案儲存的資料集要完整。
  • 如果 可以接受數分鐘以內的資料丟失,那麼可以 只使用 RDB 持久化
  • 有很多使用者都只使用 AOF 持久化,但並不推薦這種方式,因為定時生成 RDB 快照(snapshot)非常便於進行資料備份, 並且 RDB 恢復資料集的速度也要比 AOF 恢復的速度要快,除此之外,使用 RDB 還可以避免 AOF 程式的 bug。
  • 如果只需要資料在伺服器執行的時候存在,也可以不使用任何持久化方式。

11.Redis的資料恢復?

當Redis發生了故障,可以從RDB或者AOF中恢復資料。

恢復的過程也很簡單,把RDB或者AOF檔案拷貝到Redis的資料目錄下,如果使用AOF恢復,配置檔案開啟AOF,然後啟動redis-server即可。
Redis啟動載入資料

Redis 啟動時載入資料的流程:

  1. AOF持久化開啟且存在AOF檔案時,優先載入AOF檔案。
  2. AOF關閉或者AOF檔案不存在時,載入RDB檔案。
  3. 載入AOF/RDB檔案成功後,Redis啟動成功。
  4. AOF/RDB檔案存在錯誤時,Redis啟動失敗並列印錯誤資訊。

12.Redis 4.0 的混合持久化了解嗎?

重啟 Redis 時,我們很少使用 RDB 來恢復記憶體狀態,因為會丟失大量資料。我們通常使用 AOF 日誌重放,但是重放 AOF 日誌效能相對 RDB 來說要慢很多,這樣在 Redis 例項很大的情況下,啟動需要花費很長的時間。

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

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

高可用

Redis保證高可用主要有三種方式:主從、哨兵、叢集。

13.主從複製瞭解嗎?

Redis主從複製簡圖

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

主從複製主要的作用?

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

14.Redis主從有幾種常見的拓撲結構?

Redis的複製拓撲結構可以支援單層或多層複制關係,根據拓撲復雜性可以分為以下三種:一主一從、一主多從、樹狀主從結構。

1.一主一從結構

一主一從結構是最簡單的複製拓撲結構,用於主節點出現當機時從節點提供故障轉移支援。
一主一從結構
2.一主多從結構

一主多從結構(又稱為星形拓撲結構)使得應用端可以利用多個從節點實現讀寫分離(見圖6-5)。對於讀佔比較大的場景,可以把讀命令傳送到從節點來分擔主節點壓力。
一主多從結構
3.樹狀主從結構

樹狀主從結構(又稱為樹狀拓撲結構)使得從節點不但可以複製主節點資料,同時可以作為其他從節點的主節點繼續向下層複製。通過引入複製中間層,可以有效降低主節點負載和需要傳送給從節點的資料量。
樹狀主從結構

15.Redis的主從複製原理了解嗎?

Redis主從複製的工作流程大概可以分為如下幾步:
Redis主從複製工作流程

  1. 儲存主節點(master)資訊
    這一步只是儲存主節點資訊,儲存主節點的ip和port。
  2. 主從建立連線
    從節點(slave)發現新的主節點後,會嘗試和主節點建立網路連線。
  3. 傳送ping命令
    連線建立成功後從節點傳送ping請求進行首次通訊,主要是檢測主從之間網路套接字是否可用、主節點當前是否可接受處理命令。
  4. 許可權驗證
    如果主節點要求密碼驗證,從節點必須正確的密碼才能通過驗證。
  5. 同步資料集
    主從複製連線正常通訊後,主節點會把持有的資料全部傳送給從節點。
  6. 命令持續複製
    接下來主節點會持續地把寫命令傳送給從節點,保證主從資料一致性。

16.說說主從資料同步的方式?

Redis在2.8及以上版本使用psync命令完成主從資料同步,同步過程分為:全量複製和部分複製。

主從資料同步方式

全量複製
一般用於初次複製場景,Redis早期支援的複製功能只有全量複製,它會把主節點全部資料一次性傳送給從節點,當資料量較大時,會對主從節點和網路造成很大的開銷。

全量複製的完整執行流程如下:
全量複製

  1. 傳送psync命令進行資料同步,由於是第一次進行復制,從節點沒有複製偏移量和主節點的執行ID,所以傳送psync-1。
  2. 主節點根據psync-1解析出當前為全量複製,回覆+FULLRESYNC響應。
  3. 從節點接收主節點的響應資料儲存執行ID和偏移量offset
  4. 主節點執行bgsave儲存RDB檔案到本地
  5. 主節點傳送RDB檔案給從節點,從節點把接收的RDB檔案儲存在本地並直接作為從節點的資料檔案
  6. 對於從節點開始接收RDB快照到接收完成期間,主節點仍然響應讀寫命令,因此主節點會把這期間寫命令資料儲存在複製客戶端緩衝區內,當從節點載入完RDB檔案後,主節點再把緩衝區內的資料傳送給從節點,保證主從之間資料一致性。
  7. 從節點接收完主節點傳送來的全部資料後會清空自身舊資料
  8. 從節點清空資料後開始載入RDB檔案
  9. 從節點成功載入完RDB後,如果當前節點開啟了AOF持久化功能, 它會立刻做bgrewriteaof操作,為了保證全量複製後AOF持久化檔案立刻可用。

部分複製
部分複製主要是Redis針對全量複製的過高開銷做出的一種優化措施, 使用psync{runId}{offset}命令實現。當從節點(slave)正在複製主節點 (master)時,如果出現網路閃斷或者命令丟失等異常情況時,從節點會向 主節點要求補發丟失的命令資料,如果主節點的複製積壓緩衝區記憶體在這部分資料則直接傳送給從節點,這樣就可以保持主從節點複製的一致性。
部分複製

  1. 當主從節點之間網路出現中斷時,如果超過repl-timeout時間,主節點會認為從節點故障並中斷複製連線
  2. 主從連線中斷期間主節點依然響應命令,但因複製連線中斷命令無法傳送給從節點,不過主節點內部存在的複製積壓緩衝區,依然可以儲存最近一段時間的寫命令資料,預設最大快取1MB。
  3. 當主從節點網路恢復後,從節點會再次連上主節點
  4. 當主從連線恢復後,由於從節點之前儲存了自身已複製的偏移量和主節點的執行ID。因此會把它們當作psync引數傳送給主節點,要求進行部分複製操作。
  5. 主節點接到psync命令後首先核對引數runId是否與自身一致,如果一 致,說明之前複製的是當前主節點;之後根據引數offset在自身複製積壓緩衝區查詢,如果偏移量之後的資料存在緩衝區中,則對從節點傳送+CONTINUE響應,表示可以進行部分複製。
  6. 主節點根據偏移量把複製積壓緩衝區裡的資料傳送給從節點,保證主從複製進入正常狀態。

17.主從複製存在哪些問題呢?

主從複製雖好,但也存在一些問題:

  • 一旦主節點出現故障,需要手動將一個從節點晉升為主節點,同時需要修改應用方的主節點地址,還需要命令其他從節點去複製新的主節點,整個過程都需要人工干預。
  • 主節點的寫能力受到單機的限制。
  • 主節點的儲存能力受到單機的限制。

第一個問題是Redis的高可用問題,第二、三個問題屬於Redis的分散式問題。

18.Redis Sentinel(哨兵)瞭解嗎?

主從複製存在一個問題,沒法完成自動故障轉移。所以我們需要一個方案來完成自動故障轉移,它就是Redis Sentinel(哨兵)。

Redis Sentinel

Redis Sentinel ,它由兩部分組成,哨兵節點和資料節點:

  • 哨兵節點: 哨兵系統由一個或多個哨兵節點組成,哨兵節點是特殊的 Redis 節點,不儲存資料,對資料節點進行監控。
  • 資料節點: 主節點和從節點都是資料節點;

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

  • 監控(Monitoring): 哨兵會不斷地檢查主節點和從節點是否運作正常。
  • 自動故障轉移(Automatic failover):主節點 不能正常工作時,哨兵會開始 自動故障轉移操作,它會將失效主節點的其中一個 從節點升級為新的主節點,並讓其他從節點改為複製新的主節點。
  • 配置提供者(Configuration provider): 客戶端在初始化時,通過連線哨兵來獲得當前 Redis 服務的主節點地址。
  • 通知(Notification): 哨兵可以將故障轉移的結果傳送給客戶端。

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

19.Redis Sentinel(哨兵)實現原理知道嗎?

哨兵模式是通過哨兵節點完成對資料節點的監控、下線、故障轉移。
Redis Sentinel工作流程

  • 定時監控
    三個定時任務Redis Sentinel通過三個定時監控任務完成對各個節點發現和監控:
    1. 每隔10秒,每個Sentinel節點會向主節點和從節點傳送info命令獲取最新的拓撲結構
    2. 每隔2秒,每個Sentinel節點會向Redis資料節點的__sentinel__:hello 頻道上傳送該Sentinel節點對於主節點的判斷以及當前Sentinel節點的資訊
    3. 每隔1秒,每個Sentinel節點會向主節點、從節點、其餘Sentinel節點傳送一條ping命令做一次心跳檢測,來確認這些節點當前是否可達
  • 主觀下線和客觀下線
    主觀下線就是哨兵節點認為某個節點有問題,客觀下線就是超過一定數量的哨兵節點認為主節點有問題。
    主觀下線和客觀下線
  1. 主觀下線
    每個Sentinel節點會每隔1秒對主節點、從節點、其他Sentinel節點傳送ping命令做心跳檢測,當這些節點超過 down-after-milliseconds沒有進行有效回覆,Sentinel節點就會對該節點做失敗判定,這個行為叫做主觀下線。

  2. 客觀下線
    當Sentinel主觀下線的節點是主節點時,該Sentinel節點會通過sentinel is- master-down-by-addr命令向其他Sentinel節點詢問對主節點的判斷,當超過 <quorum>個數,Sentinel節點認為主節點確實有問題,這時該Sentinel節點會做出客觀下線的決定

  • 領導者Sentinel節點選舉
    Sentinel節點之間會做一個領導者選舉的工作,選出一個Sentinel節點作為領導者進行故障轉移的工作。Redis使用了Raft演算法實現領導者選舉。

  • 故障轉移

    領導者選舉出的Sentinel節點負責故障轉移,過程如下:
    故障轉移

    1. 在從節點列表中選出一個節點作為新的主節點,這一步是相對複雜一些的一步
    2. Sentinel領導者節點會對第一步選出來的從節點執行slaveof no one命令讓其成為主節點
    3. Sentinel領導者節點會向剩餘的從節點傳送命令,讓它們成為新主節點的從節點
    4. Sentinel節點集合會將原來的主節點更新為從節點,並保持著對其關注,當其恢復後命令它去複製新的主節點

20.領導者Sentinel節點選舉了解嗎?

Redis使用了Raft演算法實 現領導者選舉,大致流程如下:
領導者Sentinel節點選舉

  1. 每個線上的Sentinel節點都有資格成為領導者,當它確認主節點主觀 下線時候,會向其他Sentinel節點傳送sentinel is-master-down-by-addr命令, 要求將自己設定為領導者。
  2. 收到命令的Sentinel節點,如果沒有同意過其他Sentinel節點的sentinel is-master-down-by-addr命令,將同意該請求,否則拒絕。
  3. 如果該Sentinel節點發現自己的票數已經大於等於max(quorum, num(sentinels)/2+1),那麼它將成為領導者。
  4. 如果此過程沒有選舉出領導者,將進入下一次選舉。

21.新的主節點是怎樣被挑選出來的?

選出新的主節點,大概分為這麼幾步:
新的主節點

  1. 過濾:“不健康”(主觀下線、斷線)、5秒內沒有回覆過Sentinel節 點ping響應、與主節點失聯超過down-after-milliseconds*10秒。
  2. 選擇slave-priority(從節點優先順序)最高的從節點列表,如果存在則返回,不存在則繼續。
  3. 選擇複製偏移量最大的從節點(複製的最完整),如果存在則返 回,不存在則繼續。
  4. 選擇runid最小的從節點。

22.Redis 叢集瞭解嗎?

前面說到了主從存在高可用和分散式的問題,哨兵解決了高可用的問題,而叢集就是終極方案,一舉解決高可用和分散式問題。
Redis 叢集示意圖

  1. 資料分割槽: 資料分割槽 (或稱資料分片) 是叢集最核心的功能。叢集將資料分散到多個節點,一方面 突破了 Redis 單機記憶體大小的限制,儲存容量大大增加另一方面 每個主節點都可以對外提供讀服務和寫服務,大大提高了叢集的響應能力

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

23.叢集中資料如何分割槽?

分散式的儲存中,要把資料集按照分割槽規則對映到多個節點,常見的資料分割槽規則三種:
分散式資料分割槽

方案一:節點取餘分割槽

節點取餘分割槽,非常好理解,使用特定的資料,比如Redis的鍵,或者使用者ID之類,對響應的hash值取餘:hash(key)%N,來確定資料對映到哪一個節點上。

不過該方案最大的問題是,當節點數量變化時,如擴容或收縮節點,資料節點對映關 系需要重新計算,會導致資料的重新遷移。

節點取餘分割槽

方案二:一致性雜湊分割槽

將整個 Hash 值空間組織成一個虛擬的圓環,然後將快取節點的 IP 地址或者主機名做 Hash 取值後,放置在這個圓環上。當我們需要確定某一個 Key 需 要存取到哪個節點上的時候,先對這個 Key 做同樣的 Hash 取值,確定在環上的位置,然後按照順時針方向在環上“行走”,遇到的第一個快取節點就是要訪問的節點。

比如說下面 這張圖裡面,Key 1 和 Key 2 會落入到 Node 1 中,Key 3、Key 4 會落入到 Node 2 中,Key 5 落入到 Node 3 中,Key 6 落入到 Node 4 中。
一致性雜湊分割槽

這種方式相比節點取餘最大的好處在於加入和刪除節點隻影響雜湊環中 相鄰的節點,對其他節點無影響。

但它還是存在問題:

  • 快取節點在圓環上分佈不平均,會造成部分快取節點的壓力較大
  • 當某個節點故障時,這個節點所要承擔的所有訪問都會被順移到另一個節點上,會對後面這個節點造成力。

方案三:虛擬槽分割槽

這個方案 一致性雜湊分割槽的基礎上,引入了 虛擬節點 的概念。Redis 叢集使用的便是該方案,其中的虛擬節點稱為 槽(slot)。槽是介於資料和實際節點之間的虛擬概念,每個實際節點包含一定數量的槽,每個槽包含雜湊值在一定範圍內的資料。
虛擬槽分配

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

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

如果此時刪除 node2,只需要將槽 4-7 重新分配即可,例如槽 4-5 分配給 node1,槽 6 分配給 node3,槽 7 分配給 node4,資料在其他節點的分佈仍然較為均衡。

24.能說說Redis叢集的原理嗎?

Redis叢集通過資料分割槽來實現資料的分散式儲存,通過自動故障轉移實現高可用。

叢集建立

資料分割槽是在叢集建立的時候完成的。
叢集建立

設定節點
Redis叢集一般由多個節點組成,節點數量至少為6個才能保證組成完整高可用的叢集。每個節點需要開啟配置cluster-enabled yes,讓Redis執行在叢集模式下。
節點和握手
節點握手
節點握手是指一批執行在叢集模式下的節點通過Gossip協議彼此通訊, 達到感知對方的過程。節點握手是叢集彼此通訊的第一步,由客戶端發起命 令:cluster meet{ip}{port}。完成節點握手之後,一個個的Redis節點就組成了一個多節點的叢集。

分配槽(slot)
Redis叢集把所有的資料對映到16384個槽中。每個節點對應若干個槽,只有當節點分配了槽,才能響應和這些槽關聯的鍵命令。通過 cluster addslots命令為節點分配槽。

分配槽

故障轉移

Redis叢集的故障轉移和哨兵的故障轉移類似,但是Redis叢集中所有的節點都要承擔狀態維護的任務。

故障發現
Redis叢集內節點通過ping/pong訊息實現節點通訊,叢集中每個節點都會定期向其他節點傳送ping訊息,接收節點回復pong 訊息作為響應。如果在cluster-node-timeout時間內通訊一直失敗,則傳送節 點會認為接收節點存在故障,把接收節點標記為主觀下線(pfail)狀態。
主觀下線
當某個節點判斷另一個節點主觀下線後,相應的節點狀態會跟隨訊息在叢集內傳播。通過Gossip訊息傳播,叢集內節點不斷收集到故障節點的下線報告。當 半數以上持有槽的主節點都標記某個節點是主觀下線時。觸發客觀下線流程。
主觀下線和客觀下線

故障恢復

故障節點變為客觀下線後,如果下線節點是持有槽的主節點則需要在它 的從節點中選出一個替換它,從而保證叢集的高可用。

故障恢復流程

  1. 資格檢查
    每個從節點都要檢查最後與主節點斷線時間,判斷是否有資格替換故障 的主節點。
  2. 準備選舉時間
    當從節點符合故障轉移資格後,更新觸發故障選舉的時間,只有到達該 時間後才能執行後續流程。
  3. 發起選舉
    當從節點定時任務檢測到達故障選舉時間(failover_auth_time)到達後,發起選舉流程。
  4. 選舉投票
    持有槽的主節點處理故障選舉訊息。投票過程其實是一個領導者選舉的過程,如叢集內有N個持有槽的主節 點代表有N張選票。由於在每個配置紀元內持有槽的主節點只能投票給一個 從節點,因此只能有一個從節點獲得N/2+1的選票,保證能夠找出唯一的從節點。
    選舉投票
  5. 替換主節點
    當從節點收集到足夠的選票之後,觸發替換主節點操作。

部署Redis叢集至少需要幾個物理節點?

在投票選舉的環節,故障主節點也算在投票數內,假設叢集內節點規模是3主3從,其中有2 個主節點部署在一臺機器上,當這臺機器當機時,由於從節點無法收集到 3/2+1個主節點選票將導致故障轉移失敗。這個問題也適用於故障發現環節。因此部署叢集時所有主節點最少需要部署在3臺物理機上才能避免單點問題。

25.說說叢集的伸縮?

Redis叢集提供了靈活的節點擴容和收縮方案,可以在不影響叢集對外服務的情況下,為叢集新增節點進行擴容也可以下線部分節點進行縮容。
叢集的伸縮其實,叢集擴容和縮容的關鍵點,就在於槽和節點的對應關係,擴容和縮容就是將一部分資料遷移給新節點。

例如下面一個叢集,每個節點對應若干個槽,每個槽對應一定的資料,如果希望加入1個節點希望實現叢集擴容時,需要通過相關命令把一部分槽和內容遷移給新節點。
擴容例項縮容也是類似,先把槽和資料遷移到其它節點,再把對應的節點下線。

快取設計

26.什麼是快取擊穿、快取穿透、快取雪崩?

PS:這是多年黃曆的老八股了,一定要理解清楚。

快取擊穿

一個併發訪問量比較大的key在某個時間過期,導致所有的請求直接打在DB上。

快取擊穿
解決⽅案:

  1. 加鎖更新,⽐如請求查詢A,發現快取中沒有,對A這個key加鎖,同時去資料庫查詢資料,寫⼊快取,再返回給⽤戶,這樣後⾯的請求就可以從快取中拿到資料了。
    加鎖更新

  2. 將過期時間組合寫在value中,通過非同步的⽅式不斷的重新整理過期時間,防⽌此類現象。

快取穿透

快取穿透指的查詢快取和資料庫中都不存在的資料,這樣每次請求直接打到資料庫,就好像快取不存在一樣。

快取穿透
快取穿透將導致不存在的資料每次請求都要到儲存層去查詢,失去了快取保護後端儲存的意義。

快取穿透可能會使後端儲存負載加大,如果發現大量儲存層空命中,可能就是出現了快取穿透問題。

快取穿透可能有兩種原因:

  1. 自身業務程式碼問題
  2. 惡意攻擊,爬蟲造成空命中

它主要有兩種解決辦法:

  • 快取空值/預設值

一種方式是在資料庫不命中之後,把一個空物件或者預設值儲存到快取,之後再訪問這個資料,就會從快取中獲取,這樣就保護了資料庫。

快取空值/預設值

快取空值有兩大問題:

  1. 空值做了快取,意味著快取層中存了更多的鍵,需要更多的記憶體空間(如果是攻擊,問題更嚴重),比較有效的方法是針對這類資料設定一個較短的過期時間,讓其自動剔除。

  2. 快取層和儲存層的資料會有一段時間視窗的不一致,可能會對業務有一定影響。
    例如過期時間設定為5分鐘,如果此時儲存層新增了這個資料,那此段時間就會出現快取層和儲存層資料的不一致。
    這時候可以利用訊息佇列或者其它非同步方式清理快取中的空物件。

  • 布隆過濾器
    除了快取空物件,我們還可以在儲存和快取之前,加一個布隆過濾器,做一層過濾。

布隆過濾器裡會儲存資料是否存在,如果判斷資料不不能再,就不會訪問儲存。
布隆過濾器
兩種解決方案的對比:
快取空物件核布隆過濾器方案對比

快取雪崩

某⼀時刻發⽣⼤規模的快取失效的情況,例如快取服務當機、大量key在同一時間過期,這樣的後果就是⼤量的請求進來直接打到DB上,可能導致整個系統的崩潰,稱為雪崩。

快取雪崩
快取雪崩是三大快取問題裡最嚴重的一種,我們來看看怎麼預防和處理。

  • 提高快取可用性
  1. 叢集部署:通過叢集來提升快取的可用性,可以利用Redis本身的Redis Cluster或者第三方叢集方案如Codis等。
  2. 多級快取:設定多級快取,第一級快取失效的基礎上,訪問二級快取,每一級快取的失效時間都不同。
  • 過期時間
  1. 均勻過期:為了避免大量的快取在同一時間過期,可以把不同的 key 過期時間隨機生成,避免過期時間太過集中。
  2. 熱點資料永不過期。
  • 熔斷降級
  1. 服務熔斷:當快取伺服器當機或超時響應時,為了防止整個系統出現雪崩,暫時停止業務服務訪問快取系統。
  2. 服務降級:當出現大量快取失效,而且處在高併發高負荷的情況下,在業務系統內部暫時捨棄對一些非核心的介面和資料的請求,而直接返回一個提前準備好的 fallback(退路)錯誤處理資訊。

27.能說說布隆過濾器嗎?

布隆過濾器,它是一個連續的資料結構,每個儲存位儲存都是一個bit,即0或者1, 來標識資料是否存在。

儲存資料的時時候,使用K個不同的雜湊函式將這個變數對映為bit列表的的K個點,把它們置為1。

布隆過濾器我們判斷快取key是否存在,同樣,K個雜湊函式,對映到bit列表上的K個點,判斷是不是1:

  • 如果全不是1,那麼key不存在;
  • 如果都是1,也只是表示key可能存在。

布隆過濾器也有一些缺點:

  1. 它在判斷元素是否在集合中時是有一定錯誤機率,因為雜湊演算法有一定的碰撞的概率。
  2. 不支援刪除元素。

28.如何保證快取和資料庫資料的⼀致性?

根據CAP理論,在保證可用性和分割槽容錯性的前提下,無法保證一致性,所以快取和資料庫的絕對一致是不可能實現的,只能儘可能儲存快取和資料庫的最終一致性。

選擇合適的快取更新策略

1. 刪除快取而不是更新快取

當一個執行緒對快取的key進行寫操作的時候,如果其它執行緒進來讀資料庫的時候,讀到的就是髒資料,產生了資料不一致問題。

相比較而言,刪除快取的速度比更新快取的速度快很多,所用時間相對也少很多,讀髒資料的概率也小很多。
刪除快取和更新快取

  1. 先更資料,後刪快取
    先更資料庫還是先刪快取?這是一個問題。

更新資料,耗時可能在刪除快取的百倍以上。在快取中不存在對應的key,資料庫又沒有完成更新的時候,如果有執行緒進來讀取資料,並寫入到快取,那麼在更新成功之後,這個key就是一個髒資料。

毫無疑問,先刪快取,再更資料庫,快取中key不存在的時間的時間更長,有更大的概率會產生髒資料。

先更資料庫還是先刪快取目前最流行的快取讀寫策略cache-aside-pattern就是採用先更資料庫,再刪快取的方式。

快取不一致處理

如果不是併發特別高,對快取依賴性很強,其實一定程式的不一致是可以接受的。

但是如果對一致性要求比較高,那就得想辦法保證快取和資料庫中資料一致。

快取和資料庫資料不一致常見的兩種原因:

  • 快取key刪除失敗
  • 併發導致寫入了髒資料

快取一致性

訊息佇列保證key被刪除
可以引入訊息佇列,把要刪除的key或者刪除失敗的key丟盡訊息佇列,利用訊息佇列的重試機制,重試刪除對應的key。

訊息佇列保證key被刪除這種方案看起來不錯,缺點是對業務程式碼有一定的侵入性。

資料庫訂閱+訊息佇列保證key被刪除
可以用一個服務(比如阿里的 canal)去監聽資料庫的binlog,獲取需要操作的資料。

然後用一個公共的服務獲取訂閱程式傳來的資訊,進行快取刪除操作。
資料庫訂閱+訊息佇列保證key被刪除
這種方式降低了對業務的侵入,但其實整個系統的複雜度是提升的,適合基建完善的大廠。

延時雙刪防止髒資料
還有一種情況,是在快取不存在的時候,寫入了髒資料,這種情況在先刪快取,再更資料庫的快取更新策略下發生的比較多,解決方案是延時雙刪。

簡單說,就是在第一次刪除快取之後,過了一段時間之後,再次刪除快取。

延時雙刪

這種方式的延時時間設定需要仔細考量和測試。

設定快取過期時間兜底

這是一個樸素但是有用的辦法,給快取設定一個合理的過期時間,即使發生了快取資料不一致的問題,它也不會永遠不一致下去,快取過期的時候,自然又會恢復一致。

29.如何保證本地快取和分散式快取的一致?

PS:這道題面試很少問,但實際工作中很常見。

在日常的開發中,我們常常採用兩級快取:本地快取+分散式快取。

所謂本地快取,就是對應伺服器的記憶體快取,比如Caffeine,分散式快取基本就是採用Redis。

那麼問題來了,本地快取和分散式快取怎麼保持資料一致?
延時雙刪
Redis快取,資料庫發生更新,直接刪除快取的key即可,因為對於應用系統而言,它是一種中心化的快取。

但是本地快取,它是非中心化的,散落在分散式服務的各個節點上,沒法通過客戶端的請求刪除本地快取的key,所以得想辦法通知叢集所有節點,刪除對應的本地快取key。
本地快取/分散式快取保持一致

可以採用訊息佇列的方式:

  1. 採用Redis本身的Pub/Sub機制,分散式叢集的所有節點訂閱刪除本地快取頻道,刪除Redis快取的節點,同事釋出刪除本地快取訊息,訂閱者們訂閱到訊息後,刪除對應的本地key。
    但是Redis的釋出訂閱不是可靠的,不能保證一定刪除成功。
  2. 引入專業的訊息佇列,比如RocketMQ,保證訊息的可靠性,但是增加了系統的複雜度。
  3. 設定適當的過期時間兜底,本地快取可以設定相對短一些的過期時間。

30.怎麼處理熱key?

什麼是熱Key?
所謂的熱key,就是訪問頻率比較的key。

比如,熱門新聞事件或商品,這類key通常有大流量的訪問,對儲存這類資訊的 Redis來說,是不小的壓力。

假如Redis叢集部署,熱key可能會造成整體流量的不均衡,個別節點出現OPS過大的情況,極端情況下熱點key甚至會超過 Redis本身能夠承受的OPS。

怎麼處理熱key?

熱key處理
對熱key的處理,最關鍵的是對熱點key的監控,可以從這些端來監控熱點key:

  1. 客戶端
    客戶端其實是距離key“最近”的地方,因為Redis命令就是從客戶端發出的,例如在客戶端設定全域性字典(key和呼叫次數),每次呼叫Redis命令時,使用這個字典進行記錄。

  2. 代理端
    像Twemproxy、Codis這些基於代理的Redis分散式架構,所有客戶端的請求都是通過代理端完成的,可以在代理端進行收集統計。

  3. Redis服務端
    使用monitor命令統計熱點key是很多開發和運維人員首先想到,monitor命令可以監控到Redis執行的所有命令。

只要監控到了熱key,對熱key的處理就簡單了:

  1. 把熱key打散到不同的伺服器,降低壓⼒

  2. 加⼊⼆級快取,提前載入熱key資料到記憶體中,如果redis當機,⾛記憶體查詢

31.快取預熱怎麼做呢?

所謂快取預熱,就是提前把資料庫裡的資料刷到快取裡,通常有這些方法:

1、直接寫個快取重新整理頁面或者介面,上線時手動操作

2、資料量不大,可以在專案啟動的時候自動進行載入

3、定時任務重新整理快取.

32.熱點key重建?問題?解決?

開發的時候一般使用“快取+過期時間”的策略,既可以加速資料讀寫,又保證資料的定期更新,這種模式基本能夠滿足絕大部分需求。

但是有兩個問題如果同時出現,可能就會出現比較大的問題:

  • 當前key是一個熱點key(例如一個熱門的娛樂新聞),併發量非常大。

  • 重建快取不能在短時間完成,可能是一個複雜計算,例如複雜的 SQL、多次IO、多個依賴等。 在快取失效的瞬間,有大量執行緒來重建快取,造成後端負載加大,甚至可能會讓應用崩潰。

怎麼處理呢?

要解決這個問題也不是很複雜,解決問題的要點在於:

  • 減少重建快取的次數。
  • 資料儘可能一致。
  • 較少的潛在危險。

所以一般採用如下方式:

  1. 互斥鎖(mutex key)
    這種方法只允許一個執行緒重建快取,其他執行緒等待重建快取的執行緒執行完,重新從快取獲取資料即可。
  2. 永遠不過期
    “永遠不過期”包含兩層意思:
  • 從快取層面來看,確實沒有設定過期時間,所以不會出現熱點key過期後產生的問題,也就是“物理”不過期。
  • 從功能層面來看,為每個value設定一個邏輯過期時間,當發現超過邏輯過期時間後,會使用單獨的執行緒去構建快取。

33.無底洞問題嗎?如何解決?

什麼是無底洞問題?

2010年,Facebook的Memcache節點已經達到了3000個,承載著TB級別的快取資料。但開發和運維人員發現了一個問題,為了滿足業務要求新增了大量新Memcache節點,但是發現效能不但沒有好轉反而下降了,當時將這 種現象稱為快取的“無底洞”現象。

那麼為什麼會產生這種現象呢?

通常來說新增節點使得Memcache叢集 效能應該更強了,但事實並非如此。鍵值資料庫由於通常採用雜湊函式將 key對映到各個節點上,造成key的分佈與業務無關,但是由於資料量和訪問量的持續增長,造成需要新增大量節點做水平擴容,導致鍵值分佈到更多的 節點上,所以無論是Memcache還是Redis的分散式,批量操作通常需要從不同節點上獲取,相比於單機批量操作只涉及一次網路操作,分散式批量操作會涉及多次網路時間。

無底洞問題如何優化呢?

先分析一下無底洞問題:

  • 客戶端一次批量操作會涉及多次網路操作,也就意味著批量操作會隨著節點的增多,耗時會不斷增大。

  • 網路連線數變多,對節點的效能也有一定影響。

常見的優化思路如下:

  • 命令本身的優化,例如優化操作語句等。

  • 減少網路通訊次數。

  • 降低接入成本,例如客戶端使用長連/連線池、NIO等。

Redis運維

34.Redis報記憶體不足怎麼處理?

Redis 記憶體不足有這麼幾種處理方式:

  • 修改配置檔案 redis.conf 的 maxmemory 引數,增加 Redis 可用記憶體
  • 也可以通過命令set maxmemory動態設定記憶體上限
  • 修改記憶體淘汰策略,及時釋放記憶體空間
  • 使用 Redis 叢集模式,進行橫向擴容。

35.Redis的過期資料回收策略有哪些?

Redis主要有2種過期資料回收策略:
在這裡插入圖片描述

惰性刪除

惰性刪除指的是當我們查詢key的時候才對key進⾏檢測,如果已經達到過期時間,則刪除。顯然,他有⼀個缺點就是如果這些過期的key沒有被訪問,那麼他就⼀直⽆法被刪除,⽽且⼀直佔⽤記憶體。

定期刪除

定期刪除指的是Redis每隔⼀段時間對資料庫做⼀次檢查,刪除⾥⾯的過期key。由於不可能對所有key去做輪詢來刪除,所以Redis會每次隨機取⼀些key去做檢查和刪除。

36.Redis有哪些記憶體溢位控制/記憶體淘汰策略?

Redis所用記憶體達到maxmemory上限時會觸發相應的溢位控制策略,Redis支援六種策略:
Redis六種記憶體溢位控制策略

  1. noeviction:預設策略,不會刪除任何資料,拒絕所有寫入操作並返 回客戶端錯誤資訊,此 時Redis只響應讀操作。
  2. volatile-lru:根據LRU演算法刪除設定了超時屬性(expire)的鍵,直 到騰出足夠空間為止。如果沒有可刪除的鍵物件,回退到noeviction策略。
  3. allkeys-lru:根據LRU演算法刪除鍵,不管資料有沒有設定超時屬性, 直到騰出足夠空間為止。
  4. allkeys-random:隨機刪除所有鍵,直到騰出足夠空間為止。
  5. volatile-random:隨機刪除過期鍵,直到騰出足夠空間為止。
  6. volatile-ttl:根據鍵值物件的ttl屬性,刪除最近將要過期資料。如果 沒有,回退到noeviction策略。

37.Redis阻塞?怎麼解決?

Redis發生阻塞,可以從以下幾個方面排查:
Redis阻塞排查

  • API或資料結構使用不合理

    通常Redis執行命令速度非常快,但是不合理地使用命令,可能會導致執行速度很慢,導致阻塞,對於高併發的場景,應該儘量避免在大物件上執行演算法複雜 度超過O(n)的命令。

    對慢查詢的處理分為兩步:

    1. 發現慢查詢: slowlog get{n}命令可以獲取最近 的n條慢查詢命令;
    2. 發現慢查詢後,可以從兩個方向去優化慢查詢:
      1)修改為低演算法複雜度的命令,如hgetall改為hmget等,禁用keys、sort等命 令
      2)調整大物件:縮減大物件資料或把大物件拆分為多個小物件,防止一次命令操作過多的資料。
  • CPU飽和的問題

    單執行緒的Redis處理命令時只能使用一個CPU。而CPU飽和是指Redis單核CPU使用率跑到接近100%。

    針對這種情況,處理步驟一般如下:

    1. 判斷當前Redis併發量是否已經達到極限,可以使用統計命令redis-cli-h{ip}-p{port}--stat獲取當前 Redis使用情況
    2. 如果Redis的請求幾萬+,那麼大概就是Redis的OPS已經到了極限,應該做叢集化水品擴充套件來分攤OPS壓力
    3. 如果只有幾百幾千,那麼就得排查命令和記憶體的使用
  • 持久化相關的阻塞

    對於開啟了持久化功能的Redis節點,需要排查是否是持久化導致的阻塞。

    1. fork阻塞
      fork操作發生在RDB和AOF重寫時,Redis主執行緒呼叫fork操作產生共享 記憶體的子程式,由子程式完成持久化檔案重寫工作。如果fork操作本身耗時過長,必然會導致主執行緒的阻塞。
    2. AOF刷盤阻塞
      當我們開啟AOF持久化功能時,檔案刷盤的方式一般採用每秒一次,後臺執行緒每秒對AOF檔案做fsync操作。當硬碟壓力過大時,fsync操作需要等 待,直到寫入完成。如果主執行緒發現距離上一次的fsync成功超過2秒,為了 資料安全性它會阻塞直到後臺執行緒執行fsync操作完成。
    3. HugePage寫操作阻塞
      對於開啟Transparent HugePages的 作業系統,每次寫命令引起的複製記憶體頁單位由4K變為2MB,放大了512 倍,會拖慢寫操作的執行時間,導致大量寫操作慢查詢。

38.大key問題了解嗎?

Redis使用過程中,有時候會出現大key的情況, 比如:

  • 單個簡單的key儲存的value很大,size超過10KB
  • hash, set,zset,list 中儲存過多的元素(以萬為單位)

大key會造成什麼問題呢?

  • 客戶端耗時增加,甚至超時
  • 對大key進行IO操作時,會嚴重佔用頻寬和CPU
  • 造成Redis叢集中資料傾斜
  • 主動刪除、被動刪等,可能會導致阻塞

如何找到大key?

  • bigkeys命令:使用bigkeys命令以遍歷的方式分析Redis例項中的所有Key,並返回整體統計資訊與每個資料型別中Top1的大Key
  • redis-rdb-tools:redis-rdb-tools是由Python寫的用來分析Redis的rdb快照檔案用的工具,它可以把rdb快照檔案生成json檔案或者生成報表用來分析Redis的使用詳情。

如何處理大key?

大key處理

  • 刪除大key

    • 當Redis版本大於4.0時,可使用UNLINK命令安全地刪除大Key,該命令能夠以非阻塞的方式,逐步地清理傳入的Key。
    • 當Redis版本小於4.0時,避免使用阻塞式命令KEYS,而是建議通過SCAN命令執行增量迭代掃描key,然後判斷進行刪除。
  • 壓縮和拆分key

    • 當vaule是string時,比較難拆分,則使用序列化、壓縮演算法將key的大小控制在合理範圍內,但是序列化和反序列化都會帶來更多時間上的消耗。
    • 當value是string,壓縮之後仍然是大key,則需要進行拆分,一個大key分為不同的部分,記錄每個部分的key,使用multiget等操作實現事務讀取。
    • 當value是list/set等集合型別時,根據預估的資料規模來進行分片,不同的元素計算後分到不同的片。

39.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應用

40.使用Redis 如何實現非同步佇列?

我們知道redis支援很多種結構的資料,那麼如何使用redis作為非同步佇列使用呢?
一般有以下幾種方式:

  • 使用list作為佇列,lpush生產訊息,rpop消費訊息

這種方式,消費者死迴圈rpop從佇列中消費訊息。但是這樣,即使佇列裡沒有訊息,也會進行rpop,會導致Redis CPU的消耗。
list作為佇列
可以通過讓消費者休眠的方式的方式來處理,但是這樣又會又訊息的延遲問題。

-使用list作為佇列,lpush生產訊息,brpop消費訊息

brpop是rpop的阻塞版本,list為空的時候,它會一直阻塞,直到list中有值或者超時。
list作為佇列,brpop

這種方式只能實現一對一的訊息佇列。

  • 使用Redis的pub/sub來進行訊息的釋出/訂閱

釋出/訂閱模式可以1:N的訊息釋出/訂閱。釋出者將訊息釋出到指定的頻道頻道(channel),訂閱相應頻道的客戶端都能收到訊息。

pub/sub
但是這種方式不是可靠的,它不保證訂閱者一定能收到訊息,也不進行訊息的儲存。

所以,一般的非同步佇列的實現還是交給專業的訊息佇列。

41.Redis 如何實現延時佇列?

  • 使用zset,利用排序實現

可以使用 zset這個結構,用設定好的時間戳作為score進行排序,使用 zadd score1 value1 ....命令就可以一直往記憶體中生產訊息。再利用 zrangebysocre 查詢符合條件的所有待處理的任務,通過迴圈執行佇列任務即可。
zset實現延時佇列

42.Redis 支援事務嗎?

Redis提供了簡單的事務,但它對事務ACID的支援並不完備。

multi命令代表事務開始,exec命令代表事務結束,它們之間的命令是原子順序執行的:

127.0.0.1:6379> multi 
OK
127.0.0.1:6379> sadd user:a:follow user:b 
QUEUED 
127.0.0.1:6379> sadd user:b:fans user:a 
QUEUED
127.0.0.1:6379> sismember user:a:follow user:b 
(integer) 0
127.0.0.1:6379> exec 1) (integer) 1
2) (integer) 1

Redis事務的原理,是所有的指令在 exec 之前不執行,而是快取在
伺服器的一個事務佇列中,伺服器一旦收到 exec 指令,才開執行整個事務佇列,執行完畢後一次性返回所有指令的執行結果。
Redis事務

因為Redis執行命令是單執行緒的,所以這組命令順序執行,而且不會被其它執行緒打斷。

Redis事務的注意點有哪些?

需要注意的點有:

  • Redis 事務是不支援回滾的,不像 MySQL 的事務一樣,要麼都執行要麼都不執行;

  • Redis 服務端在執行事務的過程中,不會被其他客戶端傳送來的命令請求打斷。直到事務命令全部執行完畢才會執行其他客戶端的命令。

Redis 事務為什麼不支援回滾?

Redis 的事務不支援回滾。

如果執行的命令有語法錯誤,Redis 會執行失敗,這些問題可以從程式層面捕獲並解決。但是如果出現其他問題,則依然會繼續執行餘下的命令。

這樣做的原因是因為回滾需要增加很多工作,而不支援回滾則可以保持簡單、快速的特性

43.Redis和Lua指令碼的使用瞭解嗎?

Redis的事務功能比較簡單,平時的開發中,可以利用Lua指令碼來增強Redis的命令。

Lua指令碼能給開發人員帶來這些好處:

  • Lua指令碼在Redis中是原子執行的,執行過程中間不會插入其他命令。
  • Lua指令碼可以幫助開發和運維人員創造出自己定製的命令,並可以將這 些命令常駐在Redis記憶體中,實現複用的效果。
  • Lua指令碼可以將多條命令一次性打包,有效地減少網路開銷。

比如這一段很(爛)經(大)典(街)的秒殺系統利用lua扣減Redis庫存的指令碼:

   -- 庫存未預熱
   if (redis.call('exists', KEYS[2]) == 1) then
        return -9;
    end;
    -- 秒殺商品庫存存在
    if (redis.call('exists', KEYS[1]) == 1) then
        local stock = tonumber(redis.call('get', KEYS[1]));
        local num = tonumber(ARGV[1]);
        -- 剩餘庫存少於請求數量
        if (stock < num) then
            return -3
        end;
        -- 扣減庫存
        if (stock >= num) then
            redis.call('incrby', KEYS[1], 0 - num);
            -- 扣減成功
            return 1
        end;
        return -2;
    end;
    -- 秒殺商品庫存不存在
    return -1;

44.Redis的管道瞭解嗎?

Redis 提供三種將客戶端多條命令打包傳送給服務端執行的方式:

Pipelining(管道) 、 Transactions(事務) 和 Lua Scripts(Lua 指令碼) 。

Pipelining(管道)

Redis 管道是三者之中最簡單的,當客戶端需要執行多條 redis 命令時,可以通過管道一次性將要執行的多條命令傳送給服務端,其作用是為了降低 RTT(Round Trip Time) 對效能的影響,比如我們使用 nc 命令將兩條指令傳送給 redis 服務端。

Redis 服務端接收到管道傳送過來的多條命令後,會一直執命令,並將命令的執行結果進行快取,直到最後一條命令執行完成,再所有命令的執行結果一次性返回給客戶端 。
Pipelining示意圖`

Pipelining的優勢

在效能方面, Pipelining 有下面兩個優勢:

  • 節省了RTT:將多條命令打包一次性傳送給服務端,減少了客戶端與服務端之間的網路呼叫次數
  • 減少了上下文切換:當客戶端/服務端需要從網路中讀寫資料時,都會產生一次系統呼叫,系統呼叫是非常耗時的操作,其中設計到程式由使用者態切換到核心態,再從核心態切換回使用者態的過程。當我們執行 10 條 redis 命令的時候,就會發生 10 次使用者態到核心態的上下文切換,但如果我們使用 Pipeining 將多條命令打包成一條一次性傳送給服務端,就只會產生一次上下文切換。

45.Redis實現分散式鎖瞭解嗎?

Redis是分散式鎖本質上要實現的目標就是在 Redis 裡面佔一個“茅坑”,當別的程式也要來佔時,發現已經有人蹲在那裡了,就只好放棄或者稍後再試。

  • V1:setnx命令

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

> setnx lock:fighter true
OK
... do something critical ...
> del lock:fighter
(integer) 1

但是有個問題,如果邏輯執行到中間出現異常了,可能會導致 del 指令沒有被呼叫,這樣就會陷入死鎖,鎖永遠得不到釋放。

  • V2:鎖超時釋放

所以在拿到鎖之後,再給鎖加上一個過期時間,比如 5s,這樣即使中間出現異常也可以保證 5 秒之後鎖會自動釋放。
鎖超時釋放

> setnx lock:fighter true
OK
> expire lock:fighter 5
... do something critical ...
> del lock:fighter
(integer) 1

但是以上邏輯還有問題。如果在 setnx 和 expire 之間伺服器程式突然掛掉了,可能是因為機器掉電或者是被人為殺掉的,就會導致 expire 得不到執行,也會造成死鎖。

這種問題的根源就在於 setnx 和 expire 是兩條指令而不是原子指令。如果這兩條指令可以一起執行就不會出現問題。

  • V3:set指令

這個問題在Redis 2.8 版本中得到了解決,這個版本加入了 set 指令的擴充套件引數,使得 setnx 和expire 指令可以一起執行。
set原子指令

set lock:fighter3 true ex 5 nx OK ... do something critical ... > del lock:codehole

上面這個指令就是 setnx 和 expire 組合在一起的原子指令,這個就算是比較完善的分散式鎖了。

當然實際的開發,沒人會去自己寫分散式鎖的命令,因為有專業的輪子——Redisson

底層結構

這一部分就比較深了,如果不是簡歷上寫了精通Redis,應該不會怎麼問。

46.說說Redis底層資料結構?

Redis有動態字串(sds)連結串列(list)字典(ht)跳躍表(skiplist)整數集合(intset)壓縮列表(ziplist) 等底層資料結構。

Redis並沒有使用這些資料結構來直接實現鍵值對資料庫,而是基於這些資料結構建立了一個物件系統,來表示所有的key-value。

redisObject對應的對映
我們常用的資料型別和編碼對應的對映關係:

型別-編碼-結構
簡單看一下底層資料結構,如果對資料結構掌握不錯的話,理解這些結構應該不是特別難:

  1. 字串:redis沒有直接使⽤C語⾔傳統的字串表示,⽽是⾃⼰實現的叫做簡單動態字串SDS的抽象型別。

    C語⾔的字串不記錄⾃身的⻓度資訊,⽽SDS則儲存了⻓度資訊,這樣將獲取字串⻓度的時間由O(N)降低到了O(1),同時可以避免緩衝區溢位和減少修改字串⻓度時所需的記憶體重分配次數。

SDS

  1. 連結串列linkedlist:redis連結串列是⼀個雙向⽆環連結串列結構,很多釋出訂閱、慢查詢、監視器功能都是使⽤到了連結串列來實現,每個連結串列的節點由⼀個listNode結構來表示,每個節點都有指向前置節點和後置節點的指標,同時表頭節點的前置和後置節點都指向NULL。

連結串列linkedlist

  1. 字典dict:⽤於儲存鍵值對的抽象資料結構。Redis使⽤hash表作為底層實現,一個雜湊表裡可以有多個雜湊表節點,而每個雜湊表節點就儲存了字典裡中的一個鍵值對。
    每個字典帶有兩個hash表,供平時使⽤和rehash時使⽤,hash表使⽤鏈地址法來解決鍵衝突,被分配到同⼀個索引位置的多個鍵值對會形成⼀個單向連結串列,在對hash表進⾏擴容或者縮容的時候,為了服務的可⽤性,rehash的過程不是⼀次性完成的,⽽是漸進式的。
    字典

  2. 跳躍表skiplist:跳躍表是有序集合的底層實現之⼀,Redis中在實現有序集合鍵和叢集節點的內部結構中都是⽤到了跳躍表。Redis跳躍表由zskiplist和zskiplistNode組成,zskiplist⽤於儲存跳躍表資訊(表頭、表尾節點、⻓度等),zskiplistNode⽤於表示表跳躍節點,每個跳躍表節點的層⾼都是1-32的隨機數,在同⼀個跳躍表中,多個節點可以包含相同的分值,但是每個節點的成員物件必須是唯⼀的,節點按照分值⼤⼩排序,如果分值相同,則按照成員物件的⼤⼩排序。
    跳躍表

  3. 整數集合intset:⽤於儲存整數值的集合抽象資料結構,不會出現重複元素,底層實現為陣列。
    整數集合intset

  4. 壓縮列表ziplist:壓縮列表是為節約記憶體⽽開發的順序性資料結構,它可以包含任意多個節點,每個節點可以儲存⼀個位元組陣列或者整數值。

壓縮列表組成

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

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

C語言的字串

C語言的字串可能有什麼問題?

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

  • 獲取字串長度複雜度高 :因為 C 不儲存陣列的長度,每次都需要遍歷一遍整個陣列,時間複雜度為O(n);
  • 不能杜絕 緩衝區溢位/記憶體洩漏 的問題 : C字串不記錄自身長度帶來的另外一個問題是容易造成快取區溢位(buffer overflow),例如在字串拼接的時候,新的
  • C 字串 只能儲存文字資料 → 因為 C 語言中的字串必須符合某種編碼(比如 ASCII),例如中間出現的 '\0' 可能會被判定為提前結束的字串而識別不了;

Redis如何解決?優勢?

Redis sds

簡單來說一下 Redis 如何解決的:

  1. 多增加 len 表示當前字串的長度:這樣就可以直接獲取長度了,複雜度 O(1);
  2. 自動擴充套件空間:當 SDS 需要對字串進行修改時,首先借助於 lenalloc 檢查空間是否滿足修改所需的要求,如果空間不夠的話,SDS 會自動擴充套件空間,避免了像 C 字串操作中的溢位情況;
  3. 有效降低記憶體分配次數:C 字串在涉及增加或者清除操作時會改變底層陣列的大小造成重新分配,SDS 使用了 空間預分配惰性空間釋放 機制,簡單理解就是每次在擴充套件時是成倍的多分配的,在縮容是也是先留著並不正式歸還給 OS;
  4. 二進位制安全:C 語言字串只能儲存 ascii 碼,對於圖片、音訊等資訊無法儲存,SDS 是二進位制安全的,寫入什麼讀取就是什麼,不做任何過濾和限制;

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

字典是 Redis 伺服器中出現最為頻繁的複合型資料結構。除了 hash 結構的資料會用到字典外,整個 Redis 資料庫的所有 keyvalue 也組成了一個 全域性字典,還有帶過期時間的 key 也是一個字典。(儲存在 RedisDb 資料結構中)

字典結構是什麼樣的呢?

Redis 中的字典相當於 Java 中的 HashMap,內部實現也差不多類似,採用雜湊與運算計算下標位置;通過 "陣列 + 連結串列" 鏈地址法 來解決雜湊衝突,同時這樣的結構也吸收了兩種不同資料結構的優點。
Redis字典結構

字典是怎麼擴容的?

字典結構內部包含 兩個 hashtable,通常情況下只有一個雜湊表 ht[0] 有值,在擴容的時候,把ht[0]裡的值rehash到ht[1],然後進行 漸進式rehash ——所謂漸進式rehash,指的是這個rehash的動作並不是一次性、集中式地完成的,而是分多次、漸進式地完成的。

待搬遷結束後,h[1]就取代h[0]儲存字典的元素。

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

PS:跳躍表是比較常問的一種結構。

跳躍表(skiplist)是一種有序資料結構,它通過在每個節點中維持多個指向其它節點的指標,從而達到快速訪問節點的目的。
跳躍表

為什麼使用跳躍表?

首先,因為 zset 要支援隨機的插入和刪除,所以它 不宜使用陣列來實現,關於排序問題,我們也很容易就想到 紅黑樹/ 平衡樹 這樣的樹形結構,為什麼 Redis 不使用這樣一些結構呢?

  1. 效能考慮: 在高併發的情況下,樹形結構需要執行一些類似於 rebalance 這樣的可能涉及整棵樹的操作,相對來說跳躍表的變化只涉及區域性;
  2. 實現考慮: 在複雜度與紅黑樹相同的情況下,跳躍表實現起來更簡單,看起來也更加直觀;

基於以上的一些考慮,Redis 基於 William Pugh 的論文做出一些改進後採用了 跳躍表 這樣的結構。

本質是解決查詢問題。

跳躍表是怎麼實現的?

跳躍表的節點裡有這些元素:


  • 跳躍表節點的level陣列可以包含多個元素,每個元素都包含一個指向其它節點的指標,程式可以通過這些層來加快訪問其它節點的速度,一般來說,層的數量月多,訪問其它節點的速度就越快。

    每次建立一個新的跳躍表節點的時候,程式都根據冪次定律,隨機生成一個介於1和32之間的值作為level陣列的大小,這個大小就是層的“高度”

  • 前進指標
    每個層都有一個指向表尾的前進指標(level[i].forward屬性),用於從表頭向表尾方向訪問節點。

    我們看一下跳躍表從表頭到表尾,遍歷所有節點的路徑:
    通過前進指標遍歷

  • 跨度
    層的跨度用於記錄兩個節點之間的距離。跨度是用來計算排位(rank)的:在查詢某個節點的過程中,將沿途訪問過的所有層的跨度累計起來,得到的結果就是目標節點在跳躍表中的排位。

    例如查詢,分值為3.0、成員物件為o3的節點時,沿途經歷的層:查詢的過程只經過了一個層,並且層的跨度為3,所以目標節點在跳躍表中的排位為3。
    計算節點的排位

  • 分值和成員
    節點的分值(score屬性)是一個double型別的浮點數,跳躍表中所有的節點都按分值從小到大來排序。

    節點的成員物件(obj屬性)是一個指標,它指向一個字串物件,而字串物件則儲存這一個SDS值。

50.壓縮列表瞭解嗎?

壓縮列表是 Redis 為了節約記憶體 而使用的一種資料結構,是由一系列特殊編碼的連續記憶體快組成的順序型資料結構。

一個壓縮列表可以包含任意多個節點(entry),每個節點可以儲存一個位元組陣列或者一個整數值。

壓縮列表組成部分壓縮列表由這麼幾部分組成:

  • zlbyttes:記錄整個壓縮列表佔用的記憶體位元組數
  • zltail:記錄壓縮列表表尾節點距離壓縮列表的起始地址有多少位元組
  • zllen:記錄壓縮列表包含的節點數量
  • entryX:列表節點
  • zlend:用於標記壓縮列表的末端

壓縮列表示例

51.快速列表 quicklist 瞭解嗎?

Redis 早期版本儲存 list 列表資料結構使用的是壓縮列表 ziplist 和普通的雙向連結串列 linkedlist,也就是說當元素少時使用 ziplist,當元素多時用 linkedlist。

但考慮到連結串列的附加空間相對較高,prevnext 指標就要佔去 16 個位元組(64 位作業系統佔用 8 個位元組),另外每個節點的記憶體都是單獨分配,會傢俱記憶體的碎片化,影響記憶體管理效率。

後來 Redis 新版本(3.2)對列表資料結構進行了改造,使用 quicklist 代替了 ziplistlinkedlist,quicklist是綜合考慮了時間效率與空間效率引入的新型資料結構。

quicklist由list和ziplist結合而成,它是一個由ziplist充當節點的雙向連結串列。
quicklist

其他問題

52.假如Redis裡面有1億個key,其中有10w個key是以某個固定的已知的字首開頭的,如何將它們全部找出來?

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



參考:

[1].《Redis開發與實戰》
[2].《Redis設計與實現》
[3].《Redis深度歷險》
[4]. 艾小仙《我要進大廠》
[5].田維常《後端面試小筆記》
[6]. 美團二面:Redis與MySQL雙寫一致性如何保證?
[7]. 媽媽再也不擔心我面試被Redis問得臉都綠了
[8]. 面試官:快取一致性問題怎麼解決?
[9]. 高併發場景下,到底先更新快取還是先更新資料庫?
[10] .【Redis破障之路】三:Redis單執行緒架構
[11]. Redis官網
[12]. 解決了Redis大key問題,同事們都誇他牛皮
[13].Redis 分散式鎖原理看這篇就夠了, 循循漸進
[14]. 《Redis5設計與原始碼分析》


⭐面渣逆襲系列:

相關文章