大廠必問的Redis面試題

程式設計師大彬發表於2022-01-15

本文目錄:

  • Redis是什麼?
  • Redis優缺點?
  • Redis為什麼這麼快?
  • Redis為何選擇單執行緒
  • Redis6.0為何引入多執行緒?
  • Redis應用場景有哪些?
  • Memcached和Redis的區別?
  • Redis 資料型別有哪些?
  • keys命令存在的問題?
  • SortedSet和List異同點?
  • Redis事務
  • 持久化機制

    • RDB方式
    • AOF方式
  • RDB和AOF如何選擇?
  • Redis常見的部署方式有哪些?
  • 主從複製
  • 哨兵Sentinel
  • Redis cluster

    • 雜湊分割槽演算法有哪些?
  • 過期鍵的刪除策略?
  • 記憶體淘汰策略有哪些?
  • 如何保證快取與資料庫雙寫時的資料一致性?
  • 快取穿透
  • 快取雪崩
  • 快取擊穿
  • Redis 怎麼實現訊息佇列?
  • pipeline的作用?
  • LUA指令碼
  • 什麼是RedLock?

Redis是什麼?

Redis(Remote Dictionary Server)是一個使用 C 語言編寫的,高效能非關係型的鍵值對資料庫。與傳統資料庫不同的是,Redis 的資料是存在記憶體中的,所以讀寫速度非常快,被廣泛應用於快取方向。Redis可以將資料寫入磁碟中,保證了資料的安全不丟失,而且Redis的操作是原子性的。

Redis優缺點?

優點

  1. 基於記憶體操作,記憶體讀寫速度快。
  2. Redis是單執行緒的,避免執行緒切換開銷及多執行緒的競爭問題。單執行緒是指網路請求使用一個執行緒來處理,即一個執行緒處理所有網路請求,Redis 執行時不止有一個執行緒,比如資料持久化的過程會另起執行緒。
  3. 支援多種資料型別,包括String、Hash、List、Set、ZSet等。
  4. 支援持久化。Redis支援RDB和AOF兩種持久化機制,持久化功能可以有效地避免資料丟失問題。
  5. 支援事務。Redis的所有操作都是原子性的,同時Redis還支援對幾個操作合併後的原子性執行。
  6. 支援主從複製。主節點會自動將資料同步到從節點,可以進行讀寫分離。

缺點

  1. 對結構化查詢的支援比較差。
  2. 資料庫容量受到實體記憶體的限制,不適合用作海量資料的高效能讀寫,因此Redis適合的場景主要侷限在較小資料量的操作。
  3. Redis 較難支援線上擴容,在叢集容量達到上限時線上擴容會變得很複雜。

Redis為什麼這麼快?

  • 基於記憶體:Redis是使用記憶體儲存,沒有磁碟IO上的開銷。資料存在記憶體中,讀寫速度快。
  • 單執行緒實現( Redis 6.0以前):Redis使用單個執行緒處理請求,避免了多個執行緒之間執行緒切換和鎖資源爭用的開銷。
  • IO多路複用模型:Redis 採用 IO 多路複用技術。Redis 使用單執行緒來輪詢描述符,將資料庫的操作都轉換成了事件,不在網路I/O上浪費過多的時間。
  • 高效的資料結構:Redis 每種資料型別底層都做了優化,目的就是為了追求更快的速度。

Redis為何選擇單執行緒

  • 避免過多的上下文切換開銷。程式始終執行在程式中單個執行緒內,沒有多執行緒切換的場景。
  • 避免同步機制的開銷:如果 Redis選擇多執行緒模型,需要考慮資料同步的問題,則必然會引入某些同步機制,會導致在運算元據過程中帶來更多的開銷,增加程式複雜度的同時還會降低效能。
  • 實現簡單,方便維護:如果 Redis使用多執行緒模式,那麼所有的底層資料結構的設計都必須考慮執行緒安全問題,那麼 Redis 的實現將會變得更加複雜。

Redis6.0為何引入多執行緒?

Redis支援多執行緒主要有兩個原因:

  • 可以充分利用伺服器 CPU 資源,單執行緒模型的主執行緒只能利用一個cpu;
  • 多執行緒任務可以分攤 Redis 同步 IO 讀寫的負荷。

Redis應用場景有哪些?

  1. 快取熱點資料,緩解資料庫的壓力。
  2. 利用 Redis 原子性的自增操作,可以實現計數器的功能,比如統計使用者點贊數、使用者訪問數等。
  3. 簡單的訊息佇列,可以使用Redis自身的釋出/訂閱模式或者List來實現簡單的訊息佇列,實現非同步操作。
  4. 限速器,可用於限制某個使用者訪問某個介面的頻率,比如秒殺場景用於防止使用者快速點選帶來不必要的壓力。
  5. 好友關係,利用集合的一些命令,比如交集、並集、差集等,實現共同好友、共同愛好之類的功能。

Memcached和Redis的區別?

  1. Redis 只使用單核,而 Memcached 可以使用多核。
  2. MemCached 資料結構單一,僅用來快取資料,而 Redis 支援多種資料型別
  3. MemCached 不支援資料持久化,重啟後資料會消失。Redis 支援資料持久化
  4. Redis 提供主從同步機制和 cluster 叢集部署能力,能夠提供高可用服務。Memcached 沒有提供原生的叢集模式,需要依靠客戶端實現往叢集中分片寫入資料。
  5. Redis 的速度比 Memcached 快很多。
  6. Redis 使用單執行緒的多路 IO 複用模型,Memcached使用多執行緒的非阻塞 IO 模型。

Redis 資料型別有哪些?

基本資料型別

1、String:最常用的一種資料型別,String型別的值可以是字串、數字或者二進位制,但值最大不能超過512MB。

2、Hash:Hash 是一個鍵值對集合。

3、Set:無序去重的集合。Set 提供了交集、並集等方法,對於實現共同好友、共同關注等功能特別方便。

4、List:有序可重複的集合,底層是依賴雙向連結串列實現的。

5、SortedSet:有序Set。內部維護了一個score的引數來實現。適用於排行榜和帶權重的訊息佇列等場景。

特殊的資料型別

1、Bitmap:點陣圖,可以認為是一個以位為單位陣列,陣列中的每個單元只能存0或者1,陣列的下標在 Bitmap 中叫做偏移量。Bitmap的長度與集合中元素個數無關,而是與基數的上限有關。

2、Hyperloglog。HyperLogLog 是用來做基數統計的演算法,其優點是,在輸入元素的數量或者體積非常非常大時,計算基數所需的空間總是固定的、並且是很小的。典型的使用場景是統計獨立訪客。

3、Geospatial :主要用於儲存地理位置資訊,並對儲存的資訊進行操作,適用場景如定位、附近的人等。

keys命令存在的問題?

redis的單執行緒的。keys指令會導致執行緒阻塞一段時間,直到執行完畢,服務才能恢復。scan採用漸進式遍歷的方式來解決keys命令可能帶來的阻塞問題,每次scan命令的時間複雜度是O(1),但是要真正實現keys的功能,需要執行多次scan。

scan的缺點:在scan的過程中如果有鍵的變化(增加、刪除、修改),遍歷過程可能會有以下問題:新增的鍵可能沒有遍歷到,遍歷出了重複的鍵等情況,也就是說scan並不能保證完整的遍歷出來所有的鍵。

SortedSet和List異同點?

相同點

  1. 都是有序的;
  2. 都可以獲得某個範圍內的元素。

不同點:

  1. 列表基於連結串列實現,獲取兩端元素速度快,訪問中間元素速度慢;
  2. 有序集合基於雜湊表和跳躍表實現,訪問中間元素時間複雜度是OlogN;
  3. 列表不能簡單的調整某個元素的位置,有序列表可以(更改元素的分數);
  4. 有序集合更耗記憶體。

Redis事務

事務的原理是將一個事務範圍內的若干命令傳送給Redis,然後再讓Redis依次執行這些命令。

事務的生命週期:

  1. 使用MULTI開啟一個事務
  2. 在開啟事務的時候,每次操作的命令將會被插入到一個佇列中,同時這個命令並不會被真的執行
  3. EXEC命令進行提交事務

一個事務範圍內某個命令出錯不會影響其他命令的執行,不保證原子性:

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set a 1
QUEUED
127.0.0.1:6379> set b 1 2
QUEUED
127.0.0.1:6379> set c 3
QUEUED
127.0.0.1:6379> exec
1) OK
2) (error) ERR syntax error
3) OK

WATCH命令

WATCH命令可以監控一個或多個鍵,一旦其中有一個鍵被修改,之後的事務就不會執行(類似於樂觀鎖)。執行EXEC命令之後,就會自動取消監控。

127.0.0.1:6379> watch name
OK
127.0.0.1:6379> set name 1
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name 2
QUEUED
127.0.0.1:6379> set gender 1
QUEUED
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379> get gender
(nil)

比如上面的程式碼中:

  1. watch name開啟了對name這個key的監控
  2. 修改name的值
  3. 開啟事務a
  4. 在事務a中設定了namegender的值
  5. 使用EXEC命令進提交事務
  6. 使用命令get gender發現不存在,即事務a沒有執行

使用UNWATCH可以取消WATCH命令對key的監控,所有監控鎖將會被取消。

持久化機制

持久化就是把記憶體的資料寫到磁碟中,防止服務當機導致記憶體資料丟失。

Redis支援兩種方式的持久化,一種是RDB的方式,一種是AOF的方式。前者會根據指定的規則定時將記憶體中的資料儲存在硬碟上,而後者在每次執行完命令後將命令記錄下來。一般將兩者結合使用。

RDB方式

RDB是 Redis 預設的持久化方案。RDB持久化時會將記憶體中的資料寫入到磁碟中,在指定目錄下生成一個dump.rdb檔案。Redis 重啟會載入dump.rdb檔案恢復資料。

bgsave是主流的觸發 RDB 持久化的方式,執行過程如下:

  • 執行BGSAVE命令
  • Redis 父程式判斷當前是否存在正在執行的子程式,如果存在,BGSAVE命令直接返回。
  • 父程式執行fork操作建立子程式,fork操作過程中父程式會阻塞。
  • 父程式fork完成後,父程式繼續接收並處理客戶端的請求,而子程式開始將記憶體中的資料寫進硬碟的臨時檔案
  • 當子程式寫完所有資料後會用該臨時檔案替換舊的 RDB 檔案

Redis啟動時會讀取RDB快照檔案,將資料從硬碟載入記憶體。通過 RDB 方式的持久化,一旦Redis異常退出,就會丟失最近一次持久化以後更改的資料。

觸發 RDB 持久化的方式:

  1. 手動觸發:使用者執行SAVEBGSAVE命令。SAVE命令執行快照的過程會阻塞所有客戶端的請求,應避免在生產環境使用此命令。BGSAVE命令可以在後臺非同步進行快照操作,快照的同時伺服器還可以繼續響應客戶端的請求,因此需要手動執行快照時推薦使用BGSAVE命令。
  2. 被動觸發

    • 根據配置規則進行自動快照,如SAVE 100 10,100秒內至少有10個鍵被修改則進行快照。
    • 如果從節點執行全量複製操作,主節點會自動執行BGSAVE生成 RDB 檔案併傳送給從節點。
    • 預設情況下執行shutdown命令時,如果沒有開啟 AOF 持久化功能則自動執行·BGSAVE·。

優點

  1. Redis 載入 RDB 恢復資料遠遠快於 AOF 的方式
  2. 使用單獨子程式來進行持久化,主程式不會進行任何 IO 操作,保證了 Redis 的高效能

缺點

  1. RDB方式資料無法做到實時持久化。因為BGSAVE每次執行都要執行fork操作建立子程式,屬於重量級操作,頻繁執行成本比較高。
  2. RDB 檔案使用特定二進位制格式儲存,Redis 版本升級過程中有多個格式的 RDB 版本,存在老版本 Redis 無法相容新版 RDB 格式的問題

AOF方式

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

預設情況下Redis沒有開啟AOF方式的持久化,可以通過appendonly引數啟用:appendonly yes。開啟AOF方式持久化後每執行一條寫命令,Redis就會將該命令寫進aof_buf緩衝區,AOF緩衝區根據對應的策略向硬碟做同步操作。

預設情況下系統每30秒會執行一次同步操作。為了防止緩衝區資料丟失,可以在Redis寫入AOF檔案後主動要求系統將緩衝區資料同步到硬碟上。可以通過appendfsync引數設定同步的時機。

appendfsync always //每次寫入aof檔案都會執行同步,最安全最慢,不建議配置
appendfsync everysec  //既保證效能也保證安全,建議配置
appendfsync no //由作業系統決定何時進行同步操作

接下來看一下 AOF 持久化執行流程:

  1. 所有的寫入命令會追加到 AOP 緩衝區中。
  2. AOF 緩衝區根據對應的策略向硬碟同步。
  3. 隨著 AOF 檔案越來越大,需要定期對 AOF 檔案進行重寫,達到壓縮檔案體積的目的。AOF檔案重寫是把Redis程式內的資料轉化為寫命令同步到新AOF檔案的過程。
  4. 當 Redis 伺服器重啟時,可以載入 AOF 檔案進行資料恢復。

優點

  1. AOF可以更好的保護資料不丟失,可以配置 AOF 每秒執行一次fsync操作,如果Redis程式掛掉,最多丟失1秒的資料。
  2. AOF以append-only的模式寫入,所以沒有磁碟定址的開銷,寫入效能非常高。

缺點

  1. 對於同一份檔案AOF檔案比RDB資料快照要大。
  2. 資料恢復比較慢。

RDB和AOF如何選擇?

通常來說,應該同時使用兩種持久化方案,以保證資料安全。

  • 如果資料不敏感,且可以從其他地方重新生成,可以關閉持久化。
  • 如果資料比較重要,且能夠承受幾分鐘的資料丟失,比如快取等,只需要使用RDB即可。
  • 如果是用做記憶體資料,要使用Redis的持久化,建議是RDB和AOF都開啟。
  • 如果只用AOF,優先使用everysec的配置選擇,因為它在可靠性和效能之間取了一個平衡。

當RDB與AOF兩種方式都開啟時,Redis會優先使用AOF恢復資料,因為AOF儲存的檔案比RDB檔案更完整。

Redis常見的部署方式有哪些?

Redis的幾種常見使用方式包括:

  • 單機版
  • Redis主從
  • Redis Sentinel(哨兵)
  • Redis Cluster

使用場景

單機版:很少使用。存在的問題:1、記憶體容量有限 2、處理能力有限 3、無法高可用。

主從模式:master 節點掛掉後,需要手動指定新的 master,可用性不高,基本不用。

哨兵模式:master 節點掛掉後,哨兵程式會主動選舉新的 master,可用性高,但是每個節點儲存的資料是一樣的,浪費記憶體空間。資料量不是很多,叢集規模不是很大,需要自動容錯容災的時候使用。

Redis cluster:主要是針對海量資料+高併發+高可用的場景,如果是海量資料,如果你的資料量很大,那麼建議就用Redis cluster,所有主節點的容量總和就是Redis cluster可快取的資料容量。

主從複製

Redis的複製功能是支援多個資料庫之間的資料同步。主資料庫可以進行讀寫操作,當主資料庫的資料發生變化時會自動將資料同步到從資料庫。從資料庫一般是隻讀的,它會接收主資料庫同步過來的資料。一個主資料庫可以有多個從資料庫,而一個從資料庫只能有一個主資料庫。

redis-server //啟動Redis例項作為主資料庫 
redis-server --port 6380 --slaveof  127.0.0.1 6379  //啟動另一個例項作為從資料庫 
slaveof 127.0.0.1 6379
SLAVEOF NO ONE //停止接收其他資料庫的同步並轉化為主資料庫。

主從複製的原理?

  1. 當啟動一個從節點時,它會傳送一個 PSYNC 命令給主節點;
  2. 如果是從節點初次連線到主節點,那麼會觸發一次全量複製。此時主節點會啟動一個後臺執行緒,開始生成一份 RDB 快照檔案;
  3. 同時還會將從客戶端 client 新收到的所有寫命令快取在記憶體中。RDB 檔案生成完畢後, 主節點會將RDB檔案傳送給從節點,從節點會先將RDB檔案寫入本地磁碟,然後再從本地磁碟載入到記憶體中
  4. 接著主節點會將記憶體中快取的寫命令傳送到從節點,從節點同步這些資料;
  5. 如果從節點跟主節點之間網路出現故障,連線斷開了,會自動重連,連線之後主節點僅會將部分缺失的資料同步給從節點。

哨兵Sentinel

主從複製存在不能自動故障轉移、達不到高可用的問題。哨兵模式解決了這些問題。通過哨兵機制可以自動切換主從節點。

客戶端連線Redis的時候,先連線哨兵,哨兵會告訴客戶端Redis主節點的地址,然後客戶端連線上Redis並進行後續的操作。當主節點當機的時候,哨兵監測到主節點當機,會重新推選出某個表現良好的從節點成為新的主節點,然後通過釋出訂閱模式通知其他的從伺服器,讓它們切換主機。

工作原理

  • 每個Sentinel以每秒鐘一次的頻率向它所知道的MasterSlave以及其他 Sentinel 例項傳送一個 PING命令。
  • 如果一個例項距離最後一次有效回覆 PING 命令的時間超過指定值, 則這個例項會被 Sentine 標記為主觀下線。
  • 如果一個Master被標記為主觀下線,則正在監視這個Master的所有 Sentinel 要以每秒一次的頻率確認Master是否真正進入主觀下線狀態。
  • 當有足夠數量的 Sentinel(大於等於配置檔案指定值)在指定的時間範圍內確認Master的確進入了主觀下線狀態, 則Master會被標記為客觀下線 。若沒有足夠數量的 Sentinel 同意 Master 已經下線, Master 的客觀下線狀態就會被解除。 若 Master重新向 SentinelPING 命令返回有效回覆, Master 的主觀下線狀態就會被移除。
  • 哨兵節點會選舉出哨兵 leader,負責故障轉移的工作。
  • 哨兵 leader 會推選出某個表現良好的從節點成為新的主節點,然後通知其他從節點更新主節點資訊。

Redis cluster

哨兵模式解決了主從複製不能自動故障轉移、達不到高可用的問題,但還是存在主節點的寫能力、容量受限於單機配置的問題。而cluster模式實現了Redis的分散式儲存,每個節點儲存不同的內容,解決主節點的寫能力、容量受限於單機配置的問題。

Redis cluster叢集節點最小配置6個節點以上(3主3從),其中主節點提供讀寫操作,從節點作為備用節點,不提供請求,只作為故障轉移使用。

Redis cluster採用虛擬槽分割槽,所有的鍵根據雜湊函式對映到0~16383個整數槽內,每個節點負責維護一部分槽以及槽所對映的鍵值資料。

雜湊槽是如何對映到 Redis 例項上的?

  1. 對鍵值對的key使用 crc16 演算法計算一個結果
  2. 將結果對 16384 取餘,得到的值表示 key 對應的雜湊槽
  3. 根據該槽資訊定位到對應的例項

優點:

  • 無中心架構,支援動態擴容;
  • 資料按照slot儲存分佈在多個節點,節點間資料共享,可動態調整資料分佈
  • 高可用性。部分節點不可用時,叢集仍可用。叢集模式能夠實現自動故障轉移(failover),節點之間通過gossip協議交換狀態資訊,用投票機制完成SlaveMaster的角色轉換。

缺點:

  • 不支援批量操作(pipeline)。
  • 資料通過非同步複製,不保證資料的強一致性
  • 事務操作支援有限,只支援多key在同一節點上的事務操作,當多個key分佈於不同的節點上時無法使用事務功能。
  • key作為資料分割槽的最小粒度,不能將一個很大的鍵值物件如hashlist等對映到不同的節點。
  • 不支援多資料庫空間,單機下的Redis可以支援到16個資料庫,叢集模式下只能使用1個資料庫空間。

雜湊分割槽演算法有哪些?

節點取餘分割槽。使用特定的資料,如Redis的鍵或使用者ID,對節點數量N取餘:hash(key)%N計算出雜湊值,用來決定資料對映到哪一個節點上。
優點是簡單性。擴容時通常採用翻倍擴容,避免資料對映全部被打亂導致全量遷移的情況。

一致性雜湊分割槽。為系統中每個節點分配一個token,範圍一般在0~232,這些token構成一個雜湊環。資料讀寫執行節點查詢操作時,先根據key計算hash值,然後順時針找到第一個大於等於該雜湊值的token節點。
這種方式相比節點取餘最大的好處在於加入和刪除節點隻影響雜湊環中相鄰的節點,對其他節點無影響。

虛擬槽分割槽,所有的鍵根據雜湊函式對映到0~16383整數槽內,計算公式:slot=CRC16(key)&16383。每一個節點負責維護一部分槽以及槽所對映的鍵值資料。Redis Cluser採用虛擬槽分割槽演算法。

過期鍵的刪除策略?

1、被動刪除。在訪問key時,如果發現key已經過期,那麼會將key刪除。

2、主動刪除。定時清理key,每次清理會依次遍歷所有DB,從db隨機取出20個key,如果過期就刪除,如果其中有5個key過期,那麼就繼續對這個db進行清理,否則開始清理下一個db。

3、記憶體不夠時清理。Redis有最大記憶體的限制,通過maxmemory引數可以設定最大記憶體,當使用的記憶體超過了設定的最大記憶體,就要進行記憶體釋放, 在進行記憶體釋放的時候,會按照配置的淘汰策略清理記憶體。

記憶體淘汰策略有哪些?

當Redis的記憶體超過最大允許的記憶體之後,Redis 會觸發記憶體淘汰策略,刪除一些不常用的資料,以保證Redis伺服器正常執行。

Redisv4.0前提供 6 種資料淘汰策略

  • volatile-lru:LRU(Least Recently Used),最近使用。利用LRU演算法移除設定了過期時間的key
  • allkeys-lru:當記憶體不足以容納新寫入資料時,從資料集中移除最近最少使用的key
  • volatile-ttl:從已設定過期時間的資料集中挑選將要過期的資料淘汰
  • volatile-random:從已設定過期時間的資料集中任意選擇資料淘汰
  • allkeys-random:從資料集中任意選擇資料淘汰
  • no-eviction:禁止刪除資料,當記憶體不足以容納新寫入資料時,新寫入操作會報錯

Redisv4.0後增加以下兩種

  • volatile-lfu:LFU,Least Frequently Used,最少使用,從已設定過期時間的資料集中挑選最不經常使用的資料淘汰。
  • allkeys-lfu:當記憶體不足以容納新寫入資料時,從資料集中移除最不經常使用的key。

記憶體淘汰策略可以通過配置檔案來修改,相應的配置項是maxmemory-policy,預設配置是noeviction

如何保證快取與資料庫雙寫時的資料一致性?

1、先刪除快取再更新資料庫

進行更新操作時,先刪除快取,然後更新資料庫,後續的請求再次讀取時,會從資料庫讀取後再將新資料更新到快取。

存在的問題:刪除快取資料之後,更新資料庫完成之前,這個時間段內如果有新的讀請求過來,就會從資料庫讀取舊資料重新寫到快取中,再次造成不一致,並且後續讀的都是舊資料。

2、先更新資料庫再刪除快取

進行更新操作時,先更新MySQL,成功之後,刪除快取,後續讀取請求時再將新資料回寫快取。

存在的問題:更新MySQL和刪除快取這段時間內,請求讀取的還是快取的舊資料,不過等資料庫更新完成,就會恢復一致,影響相對比較小。

3、非同步更新快取

資料庫的更新操作完成後不直接操作快取,而是把這個操作命令封裝成訊息扔到訊息佇列中,然後由Redis自己去消費更新資料,訊息佇列可以保證資料操作順序一致性,確保快取系統的資料正常。

快取穿透

快取穿透是指查詢一個不存在的資料,由於快取是不命中時被動寫的,如果從DB查不到資料則不寫入快取,這將導致這個不存在的資料每次請求都要到DB去查詢,失去了快取的意義。在流量大時,可能DB就掛掉了。

  1. 快取空值,不會查資料庫。
  2. 採用布隆過濾器,將所有可能存在的資料雜湊到一個足夠大的bitmap中,查詢不存在的資料會被這個bitmap攔截掉,從而避免了對DB的查詢壓力。

布隆過濾器的原理:當一個元素被加入集合時,通過K個雜湊函式將這個元素對映成一個位陣列中的K個點,把它們置為1。查詢時,將元素通過雜湊函式對映之後會得到k個點,如果這些點有任何一個0,則被檢元素一定不在,直接返回;如果都是1,則查詢元素很可能存在,就會去查詢Redis和資料庫。

快取雪崩

快取雪崩是指在我們設定快取時採用了相同的過期時間,導致快取在某一時刻同時失效,請求全部轉發到DB,DB瞬時壓力過重掛掉。

解決方法:在原有的失效時間基礎上增加一個隨機值,使得過期時間分散一些。

快取擊穿

快取擊穿:大量的請求同時查詢一個 key 時,此時這個 key 正好失效了,就會導致大量的請求都落到資料庫。快取擊穿是查詢快取中失效的 key,而快取穿透是查詢不存在的 key。

解決方法:加分散式鎖,第一個請求的執行緒可以拿到鎖,拿到鎖的執行緒查詢到了資料之後設定快取,其他的執行緒獲取鎖失敗會等待50ms然後重新到快取取資料,這樣便可以避免大量的請求落到資料庫。

public String get(String key) {
    String value = redis.get(key);
    if (value == null) { //快取值過期
        String unique_key = systemId + ":" + key;
        //設定30s的超時
        if (redis.set(unique_key, 1, 'NX', 'PX', 30000) == 1) {  //設定成功
            value = db.get(key);
            redis.set(key, value, expire_secs);
            redis.del(unique_key);
        } else {  //其他執行緒已經到資料庫取值並回寫到快取了,可以重試獲取快取值
            sleep(50);
            get(key);  //重試
        }
    } else {
        return value;
    }
}

Redis 怎麼實現訊息佇列?

使用一個列表,讓生產者將任務使用LPUSH命令放進列表,消費者不斷用RPOP從列表取出任務。

BRPOP和RPOP命令相似,唯一的區別就是當列表沒有元素時BRPOP命令會一直阻塞連線,直到有新元素加入。
BRPOP queue 0  //0表示不限制等待時間

優先順序佇列

如果多個鍵都有元素,則按照從左到右的順序取元素。

BLPOP queue:1 queue:2 queue:3 0

釋出/訂閱模式

PSUBSCRIBE channel?* 按照規則訂閱。
PUNSUBSCRIBE channel?* 退訂通過PSUBSCRIBE命令按照某種規則訂閱的頻道。其中訂閱規則要進行嚴格的字串匹配,PUNSUBSCRIBE *無法退訂channel?*規則。

PUBLISH channel1 hi
SUBSCRIBE channel1
UNSUBSCRIBE channel1 //退訂通過SUBSCRIBE命令訂閱的頻道。

缺點:在消費者下線的情況下,生產的訊息會丟失。

延時佇列

使用sortedset,拿時間戳作為score,訊息內容作為key,呼叫zadd來生產訊息,消費者用zrangebyscore指令獲取N秒之前的資料輪詢進行處理。

pipeline的作用?

redis客戶端執行一條命令分4個過程: 傳送命令、命令排隊、命令執行、返回結果。使用pipeline可以批量請求,批量返回結果,執行速度比逐條執行要快。

使用pipeline組裝的命令個數不能太多,不然資料量過大,增加客戶端的等待時間,還可能造成網路阻塞,可以將大量命令的拆分多個小的pipeline命令完成。

原生批命令(mset和mget)與pipeline對比:

  1. 原生批命令是原子性,pipeline非原子性。pipeline命令中途異常退出,之前執行成功的命令不會回滾
  2. 原生批命令只有一個命令,但pipeline支援多命令

LUA指令碼

Redis 通過 LUA 指令碼建立具有原子性的命令: 當lua指令碼命令正在執行的時候,不會有其他指令碼或 Redis 命令被執行,實現組合命令的原子操作。

在Redis中執行Lua指令碼有兩種方法:evalevalshaeval命令使用內建的 Lua 直譯器,對 Lua 指令碼進行求值。

//第一個引數是lua指令碼,第二個引數是鍵名引數個數,剩下的是鍵名引數和附加引數
> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"

lua指令碼作用

1、Lua指令碼在Redis中是原子執行的,執行過程中間不會插入其他命令。

2、Lua指令碼可以將多條命令一次性打包,有效地減少網路開銷。

應用場景

舉例:限制介面訪問頻率。

在Redis維護一個介面訪問次數的鍵值對,key是介面名稱,value是訪問次數。每次訪問介面時,會執行以下操作:

  • 通過aop攔截介面的請求,對介面請求進行計數,每次進來一個請求,相應的介面訪問次數count加1,存入redis。
  • 如果是第一次請求,則會設定count=1,並設定過期時間。因為這裡set()expire()組合操作不是原子操作,所以引入lua指令碼,實現原子操作,避免併發訪問問題。
  • 如果給定時間範圍內超過最大訪問次數,則會丟擲異常。
private String buildLuaScript() {
    return "local c" +
        "\nc = redis.call('get',KEYS[1])" +
        "\nif c and tonumber(c) > tonumber(ARGV[1]) then" +
        "\nreturn c;" +
        "\nend" +
        "\nc = redis.call('incr',KEYS[1])" +
        "\nif tonumber(c) == 1 then" +
        "\nredis.call('expire',KEYS[1],ARGV[2])" +
        "\nend" +
        "\nreturn c;";
}

String luaScript = buildLuaScript();
RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
Number count = redisTemplate.execute(redisScript, keys, limit.count(), limit.period());

PS:這種介面限流的實現方式比較簡單,問題也比較多,一般不會使用,介面限流用的比較多的是令牌桶演算法和漏桶演算法。

什麼是RedLock?

Redis 官方站提出了一種權威的基於 Redis 實現分散式鎖的方式名叫 Redlock,此種方式比原先的單節點的方法更安全。它可以保證以下特性:

  1. 安全特性:互斥訪問,即永遠只有一個 client 能拿到鎖
  2. 避免死鎖:最終 client 都可能拿到鎖,不會出現死鎖的情況,即使原本鎖住某資源的 client 掛掉了
  3. 容錯性:只要大部分 Redis 節點存活就可以正常提供服務

文章對你有用的話,點個贊,支援一下~

我是大彬,非科班轉碼,校招拿了多家網際網路中大廠offer,專注分享Java技術乾貨,歡迎關注~

相關文章