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

大家好,我是老三,面渣逆襲系列繼續,這節我們來搞定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設計與源碼分析》


⭐面渣逆襲系列:

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