1.5萬字總結 Redis 常見面試題&知識點

以下內容來源於於我開源的 JavaGuide (Java學習&&面試指南,Github 130k star,370人共同參與愛完善), 萬字總結,質量有保障!

這篇文章最早寫於2019年,經過不斷完善,內容也更全面了,裏面的很多內容也被很多人蔘考借鑑。

Redis 基礎

什麼是 Redis?

Redis 是一個基於 C 語言開發的開源數據庫(BSD 許可),與傳統數據庫不同的是 Redis 的數據是存在內存中的(內存數據庫),讀寫速度非常快,被廣泛應用於緩存方向。並且,Redis 存儲的是 KV 鍵值對數據。

爲了滿足不同的業務場景,Redis 內置了多種數據類型實現(比如 String、Hash、Sorted Set、Bitmap)。並且,Redis 還支持事務 、持久化、Lua 腳本、多種開箱即用的集羣方案(Redis Sentinel、Redis Cluster)。

Redis 沒有外部依賴,Linux 和 OS X 是 Redis 開發和測試最多的兩個操作系統,官方推薦生產環境使用 Linux 部署 Redis。

個人學習的話,你可以自己本機安裝 Redis 或者通過 Redis 官網提供的在線 Redis 環境來實際體驗 Redis。

try-redis

全世界有非常多的網站使用到了 Redis ,techstacks.io 專門維護了一個使用 Redis 的熱門站點列表 ,感興趣的話可以看看。

Redis 爲什麼這麼快?

Redis 內部做了非常多的性能優化,比較重要的主要有下面 3 點:

  • Redis 基於內存,內存的訪問速度是磁盤的上千倍;
  • Redis 基於 Reactor 模式設計開發了一套高效的事件處理模型,主要是單線程事件循環和 IO 多路複用(Redis 線程模式後面會詳細介紹到);
  • Redis 內置了多種優化過後的數據結構實現,性能非常高。

下面這張圖片總結的挺不錯的,分享一下,出自 Why is Redis so fast?

在這裏插入圖片描述

分佈式緩存常見的技術選型方案有哪些?

分佈式緩存的話,比較老牌同時也是使用的比較多的還是 MemcachedRedis。不過,現在基本沒有看過還有項目使用 Memcached 來做緩存,都是直接用 Redis

Memcached 是分佈式緩存最開始興起的那會,比較常用的。後來,隨着 Redis 的發展,大家慢慢都轉而使用更加強大的 Redis 了。

另外,騰訊也開源了一款類似於 Redis 的分佈式高性能 KV 存儲數據庫,基於知名的開源項目 RocksDB 作爲存儲引擎 ,100% 兼容 Redis 協議和 Redis4.0 所有數據模型,名爲 Tendis

關於 Redis 和 Tendis 的對比,騰訊官方曾經發過一篇文章:Redis vs Tendis:冷熱混合存儲版架構揭祕 ,可以簡單參考一下。

從這個項目的 Github 提交記錄可以看出,Tendis 開源版幾乎已經沒有被維護更新了,加上其關注度並不高,使用的公司也比較少。因此,不建議你使用 Tendis 來實現分佈式緩存。

說一下 Redis 和 Memcached 的區別和共同點

現在公司一般都是用 Redis 來實現緩存,而且 Redis 自身也越來越強大了!不過,瞭解 Redis 和 Memcached 的區別和共同點,有助於我們在做相應的技術選型的時候,能夠做到有理有據!

共同點

  1. 都是基於內存的數據庫,一般都用來當做緩存使用。
  2. 都有過期策略。
  3. 兩者的性能都非常高。

區別

  1. Redis 支持更豐富的數據類型(支持更復雜的應用場景)。Redis 不僅僅支持簡單的 k/v 類型的數據,同時還提供 list,set,zset,hash 等數據結構的存儲。Memcached 只支持最簡單的 k/v 數據類型。
  2. Redis 支持數據的持久化,可以將內存中的數據保持在磁盤中,重啓的時候可以再次加載進行使用,而 Memcached 把數據全部存在內存之中。
  3. Redis 有災難恢復機制。 因爲可以把緩存中的數據持久化到磁盤上。
  4. Redis 在服務器內存使用完之後,可以將不用的數據放到磁盤上。但是,Memcached 在服務器內存使用完之後,就會直接報異常。
  5. Memcached 沒有原生的集羣模式,需要依靠客戶端來實現往集羣中分片寫入數據;但是 Redis 目前是原生支持 cluster 模式的。
  6. Memcached 是多線程,非阻塞 IO 複用的網絡模型;Redis 使用單線程的多路 IO 複用模型。 (Redis 6.0 引入了多線程 IO )
  7. Redis 支持發佈訂閱模型、Lua 腳本、事務等功能,而 Memcached 不支持。並且,Redis 支持更多的編程語言。
  8. Memcached 過期數據的刪除策略只用了惰性刪除,而 Redis 同時使用了惰性刪除與定期刪除。

相信看了上面的對比之後,我們已經沒有什麼理由可以選擇使用 Memcached 來作爲自己項目的分佈式緩存了。

爲什麼要用 Redis/爲什麼要用緩存?

下面我們主要從“高性能”和“高併發”這兩點來回答這個問題。

高性能

假如用戶第一次訪問數據庫中的某些數據的話,這個過程是比較慢,畢竟是從硬盤中讀取的。但是,如果說,用戶訪問的數據屬於高頻數據並且不會經常改變的話,那麼我們就可以很放心地將該用戶訪問的數據存在緩存中。

這樣有什麼好處呢? 那就是保證用戶下一次再訪問這些數據的時候就可以直接從緩存中獲取了。操作緩存就是直接操作內存,所以速度相當快。

高併發

一般像 MySQL 這類的數據庫的 QPS 大概都在 1w 左右(4 核 8g) ,但是使用 Redis 緩存之後很容易達到 10w+,甚至最高能達到 30w+(就單機 Redis 的情況,Redis 集羣的話會更高)。

QPS(Query Per Second):服務器每秒可以執行的查詢次數;

由此可見,直接操作緩存能夠承受的數據庫請求數量是遠遠大於直接訪問數據庫的,所以我們可以考慮把數據庫中的部分數據轉移到緩存中去,這樣用戶的一部分請求會直接到緩存這裏而不用經過數據庫。進而,我們也就提高了系統整體的併發。

Redis 除了做緩存,還能做什麼?

  • 分佈式鎖 : 通過 Redis 來做分佈式鎖是一種比較常見的方式。通常情況下,我們都是基於 Redisson 來實現分佈式鎖。關於 Redis 實現分佈式鎖的詳細介紹,可以看我寫的這篇文章:分佈式鎖詳解
  • 限流 :一般是通過 Redis + Lua 腳本的方式來實現限流。相關閱讀:《我司用了 6 年的 Redis 分佈式限流器,可以說是非常厲害了!》
  • 消息隊列 :Redis 自帶的 list 數據結構可以作爲一個簡單的隊列使用。Redis 5.0 中增加的 Stream 類型的數據結構更加適合用來做消息隊列。它比較類似於 Kafka,有主題和消費組的概念,支持消息持久化以及 ACK 機制。
  • 複雜業務場景 :通過 Redis 以及 Redis 擴展(比如 Redisson)提供的數據結構,我們可以很方便地完成很多複雜的業務場景比如通過 bitmap 統計活躍用戶、通過 sorted set 維護排行榜。
  • ......

Redis 可以做消息隊列麼?

Redis 5.0 新增加的一個數據結構 Stream 可以用來做消息隊列,Stream 支持:

  • 發佈 / 訂閱模式
  • 按照消費者組進行消費
  • 消息持久化( RDB 和 AOF)

不過,和專業的消息隊列相比,還是有很多欠缺的地方比如消息丟失和堆積問題不好解決。因此,我們通常建議是不使用 Redis 來做消息隊列的,你完全可以選擇市面上比較成熟的一些消息隊列比如 RocketMQ、Kafka。

相關文章推薦:Redis 消息隊列的三種方案(List、Streams、Pub/Sub)

如何基於 Redis 實現分佈式鎖?

關於 Redis 實現分佈式鎖的詳細介紹,可以看我寫的這篇文章:分佈式鎖詳解

Redis 數據結構

Redis 常用的數據結構有哪些?

  • 5 種基礎數據結構 :String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。
  • 3 種特殊數據結構 :HyperLogLogs(基數統計)、Bitmap (位存儲)、Geospatial (地理位置)。

關於 5 種基礎數據結構的詳細介紹請看這篇文章:Redis 5 種基本數據結構詳解

關於 3 種特殊數據結構的詳細介紹請看這篇文章:Redis 3 種特殊數據結構詳解

String 的應用場景有哪些?

  • 常規數據(比如 session、token、、序列化後的對象)的緩存;
  • 計數比如用戶單位時間的請求數(簡單限流可以用到)、頁面單位時間的訪問數;
  • 分佈式鎖(利用 SETNX key value 命令可以實現一個最簡易的分佈式鎖);
  • ......

關於 String 的詳細介紹請看這篇文章:Redis 5 種基本數據結構詳解

String 還是 Hash 存儲對象數據更好呢?

  • String 存儲的是序列化後的對象數據,存放的是整個對象。Hash 是對對象的每個字段單獨存儲,可以獲取部分字段的信息,也可以修改或者添加部分字段,節省網絡流量。如果對象中某些字段需要經常變動或者經常需要單獨查詢對象中的個別字段信息,Hash 就非常適合。
  • String 存儲相對來說更加節省內存,緩存相同數量的對象數據,String 消耗的內存約是 Hash 的一半。並且,存儲具有多層嵌套的對象時也方便很多。如果系統對性能和資源消耗非常敏感的話,String 就非常適合。

在絕大部分情況,我們建議使用 String 來存儲對象數據即可!

String 的底層實現是什麼?

Redis 是基於 C 語言編寫的,但 Redis 的 String 類型的底層實現並不是 C 語言中的字符串(即以空字符 \0 結尾的字符數組),而是自己編寫了 SDS(Simple Dynamic String,簡單動態字符串) 來作爲底層實現。

SDS 最早是 Redis 作者爲日常 C 語言開發而設計的 C 字符串,後來被應用到了 Redis 上,並經過了大量的修改完善以適合高性能操作。

Redis7.0 的 SDS 的部分源碼如下(https://github.com/redis/redis/blob/7.0/src/sds.h):

/* Note: sdshdr5 is never used, we just access the flags byte directly.
 * However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};

通過源碼可以看出,SDS 共有五種實現方式 SDS_TYPE_5(並未用到)、SDS_TYPE_8、SDS_TYPE_16、SDS_TYPE_32、SDS_TYPE_64,其中只有後四種實際用到。Redis 會根據初始化的長度決定使用哪種類型,從而減少內存的使用。

類型 字節
sdshdr5 < 1 <8
sdshdr8 1 8
sdshdr16 2 16
sdshdr32 4 32
sdshdr64 8 64

對於後四種實現都包含了下面這 4 個屬性:

  • len :字符串的長度也就是已經使用的字節數
  • alloc:總共可用的字符空間大小,alloc-len 就是 SDS 剩餘的空間大小
  • buf[] :實際存儲字符串的數組
  • flags :低三位保存類型標誌

SDS 相比於 C 語言中的字符串有如下提升:

  1. 可以避免緩衝區溢出 :C 語言中的字符串被修改(比如拼接)時,一旦沒有分配足夠長度的內存空間,就會造成緩衝區溢出。SDS 被修改時,會先根據 len 屬性檢查空間大小是否滿足要求,如果不滿足,則先擴展至所需大小再進行修改操作。
  2. 獲取字符串長度的複雜度較低 : C 語言中的字符串的長度通常是經過遍歷計數來實現的,時間複雜度爲 O(n)。SDS 的長度獲取直接讀取 len 屬性即可,時間複雜度爲 O(1)。
  3. 減少內存分配次數 : 爲了避免修改(增加/減少)字符串時,每次都需要重新分配內存(C 語言的字符串是這樣的),SDS 實現了空間預分配和惰性空間釋放兩種優化策略。當 SDS 需要增加字符串時,Redis 會爲 SDS 分配好內存,並且根據特定的算法分配多餘的內存,這樣可以減少連續執行字符串增長操作所需的內存重分配次數。當 SDS 需要減少字符串時,這部分內存不會立即被回收,會被記錄下來,等待後續使用(支持手動釋放,有對應的 API)。
  4. 二進制安全 :C 語言中的字符串以空字符 \0 作爲字符串結束的標識,這存在一些問題,像一些二進制文件(比如圖片、視頻、音頻)就可能包括空字符,C 字符串無法正確保存。SDS 使用 len 屬性判斷字符串是否結束,不存在這個問題。

多提一嘴,很多文章裏 SDS 的定義是下面這樣的:

struct sdshdr {
    unsigned int len;
    unsigned int free;
    char buf[];
};

這個也沒錯,Redis 3.2 之前就是這樣定義的。後來,由於這種方式的定義存在問題,lenfree 的定義用了 4 個字節,造成了浪費。Redis 3.2 之後,Redis 改進了 SDS 的定義,將其劃分爲了現在的 5 種類型。

購物車信息用 String 還是 Hash 存儲更好呢?

由於購物車中的商品頻繁修改和變動,購物車信息建議使用 Hash 存儲:

  • 用戶 id 爲 key
  • 商品 id 爲 field,商品數量爲 value

那用戶購物車信息的維護具體應該怎麼操作呢?

  • 用戶添加商品就是往 Hash 裏面增加新的 field 與 value;
  • 查詢購物車信息就是遍歷對應的 Hash;
  • 更改商品數量直接修改對應的 value 值(直接 set 或者做運算皆可);
  • 刪除商品就是刪除 Hash 中對應的 field;
  • 清空購物車直接刪除對應的 key 即可。

這裏只是以業務比較簡單的購物車場景舉例,實際電商場景下,field 只保存一個商品 id 是沒辦法滿足需求的。

使用 Redis 實現一個排行榜怎麼做?

Redis 中有一個叫做 sorted set 的數據結構經常被用在各種排行榜的場景,比如直播間送禮物的排行榜、朋友圈的微信步數排行榜、王者榮耀中的段位排行榜、話題熱度排行榜等等。

相關的一些 Redis 命令: ZRANGE (從小到大排序) 、 ZREVRANGE (從大到小排序)、ZREVRANK (指定元素排名)。

《Java 面試指北》 的「技術面試題篇」就有一篇文章詳細介紹如何使用 Sorted Set 來設計製作一個排行榜。

使用 Set 實現抽獎系統需要用到什麼命令?

  • SPOP key count : 隨機移除並獲取指定集合中一個或多個元素,適合不允許重複中獎的場景。
  • SRANDMEMBER key count : 隨機獲取指定集合中指定數量的元素,適合允許重複中獎的場景。

使用 Bitmap 統計活躍用戶怎麼做?

使用日期(精確到天)作爲 key,然後用戶 ID 爲 offset,如果當日活躍過就設置爲 1。

初始化數據:

> SETBIT 20210308 1 1
(integer) 0
> SETBIT 20210308 2 1
(integer) 0
> SETBIT 20210309 1 1
(integer) 0

統計 20210308~20210309 總活躍用戶數:

> BITOP and desk1 20210308 20210309
(integer) 1
> BITCOUNT desk1
(integer) 1

統計 20210308~20210309 在線活躍用戶數:

> BITOP or desk2 20210308 20210309
(integer) 1
> BITCOUNT desk2
(integer) 2

使用 HyperLogLog 統計頁面 UV 怎麼做?

1、將訪問指定頁面的每個用戶 ID 添加到 HyperLogLog 中。

PFADD PAGE_1:UV USER1 USER2 ...... USERn

2、統計指定頁面的 UV。

PFCOUNT PAGE_1:UV

Redis 線程模型

對於讀寫命令來說,Redis 一直是單線程模型。不過,在 Redis 4.0 版本之後引入了多線程來執行一些大鍵值對的異步刪除操作, Redis 6.0 版本之後引入了多線程來處理網絡請求(提高網絡 IO 讀寫性能)。

Redis 單線程模型瞭解嗎?

Redis 基於 Reactor 模式設計開發了一套高效的事件處理模型 (Netty 的線程模型也基於 Reactor 模式,Reactor 模式不愧是高性能 IO 的基石),這套事件處理模型對應的是 Redis 中的文件事件處理器(file event handler)。由於文件事件處理器(file event handler)是單線程方式運行的,所以我們一般都說 Redis 是單線程模型。

《Redis 設計與實現》有一段話是如是介紹文件事件處理器的,我覺得寫得挺不錯。

Redis 基於 Reactor 模式開發了自己的網絡事件處理器:這個處理器被稱爲文件事件處理器(file event handler)。

  • 文件事件處理器使用 I/O 多路複用(multiplexing)程序來同時監聽多個套接字,並根據套接字目前執行的任務來爲套接字關聯不同的事件處理器。
  • 當被監聽的套接字準備好執行連接應答(accept)、讀取(read)、寫入(write)、關 閉(close)等操作時,與操作相對應的文件事件就會產生,這時文件事件處理器就會調用套接字之前關聯好的事件處理器來處理這些事件。

雖然文件事件處理器以單線程方式運行,但通過使用 I/O 多路複用程序來監聽多個套接字,文件事件處理器既實現了高性能的網絡通信模型,又可以很好地與 Redis 服務器中其他同樣以單線程方式運行的模塊進行對接,這保持了 Redis 內部單線程設計的簡單性。

既然是單線程,那怎麼監聽大量的客戶端連接呢?

Redis 通過 IO 多路複用程序 來監聽來自客戶端的大量連接(或者說是監聽多個 socket),它會將感興趣的事件及類型(讀、寫)註冊到內核中並監聽每個事件是否發生。

這樣的好處非常明顯: I/O 多路複用技術的使用讓 Redis 不需要額外創建多餘的線程來監聽客戶端的大量連接,降低了資源的消耗(和 NIO 中的 Selector 組件很像)。

文件事件處理器(file event handler)主要是包含 4 個部分:

  • 多個 socket(客戶端連接)
  • IO 多路複用程序(支持多個客戶端連接的關鍵)
  • 文件事件分派器(將 socket 關聯到相應的事件處理器)
  • 事件處理器(連接應答處理器、命令請求處理器、命令回覆處理器)

文件事件處理器

相關閱讀:Redis 事件機制詳解

Redis6.0 之前爲什麼不使用多線程?

雖然說 Redis 是單線程模型,但是,實際上,Redis 在 4.0 之後的版本中就已經加入了對多線程的支持。

不過,Redis 4.0 增加的多線程主要是針對一些大鍵值對的刪除操作的命令,使用這些命令就會使用主線程之外的其他線程來“異步處理”。

爲此,Redis 4.0 之後新增了UNLINK(可以看作是 DEL 的異步版本)、FLUSHALL ASYNC(清空所有數據庫的所有 key,不僅僅是當前 SELECT 的數據庫)、FLUSHDB ASYNC(清空當前 SELECT 數據庫中的所有 key)等異步命令。

redis4.0 more thread

大體上來說,Redis 6.0 之前主要還是單線程處理。

那 Redis6.0 之前爲什麼不使用多線程? 我覺得主要原因有 3 點:

  • 單線程編程容易並且更容易維護;
  • Redis 的性能瓶頸不在 CPU ,主要在內存和網絡;
  • 多線程就會存在死鎖、線程上下文切換等問題,甚至會影響性能。

相關閱讀:爲什麼 Redis 選擇單線程模型

Redis6.0 之後爲何引入了多線程?

Redis6.0 引入多線程主要是爲了提高網絡 IO 讀寫性能,因爲這個算是 Redis 中的一個性能瓶頸(Redis 的瓶頸主要受限於內存和網絡)。

雖然,Redis6.0 引入了多線程,但是 Redis 的多線程只是在網絡數據的讀寫這類耗時操作上使用了,執行命令仍然是單線程順序執行。因此,你也不需要擔心線程安全問題。

Redis6.0 的多線程默認是禁用的,只使用主線程。如需開啓需要設置IO線程數 > 1,需要修改 redis 配置文件 redis.conf

io-threads 4 #設置1的話只會開啓主線程,官網建議4核的機器建議設置爲2或3個線程,8核的建議設置爲6個線程

另外:

  • io-threads的個數一旦設置,不能通過config動態設置
  • 當設置ssl後,io-threads將不工作

開啓多線程後,默認只會使用多線程進行IO寫入writes,即發送數據給客戶端,如果需要開啓多線程IO讀取reads,同樣需要修改 redis 配置文件 redis.conf :

io-threads-do-reads yes

但是官網描述開啓多線程讀並不能有太大提升,因此一般情況下並不建議開啓

相關閱讀:

Redis 內存管理

Redis 給緩存數據設置過期時間有啥用?

一般情況下,我們設置保存的緩存數據的時候都會設置一個過期時間。爲什麼呢?

因爲內存是有限的,如果緩存中的所有數據都是一直保存的話,分分鐘直接 Out of memory。

Redis 自帶了給緩存數據設置過期時間的功能,比如:

127.0.0.1:6379> expire key 60 # 數據在 60s 後過期
(integer) 1
127.0.0.1:6379> setex key 60 value # 數據在 60s 後過期 (setex:[set] + [ex]pire)
OK
127.0.0.1:6379> ttl key # 查看數據還有多久過期
(integer) 56

注意:Redis 中除了字符串類型有自己獨有設置過期時間的命令 setex 外,其他方法都需要依靠 expire 命令來設置過期時間 。另外, persist 命令可以移除一個鍵的過期時間。

過期時間除了有助於緩解內存的消耗,還有什麼其他用麼?

很多時候,我們的業務場景就是需要某個數據只在某一時間段內存在,比如我們的短信驗證碼可能只在 1 分鐘內有效,用戶登錄的 token 可能只在 1 天內有效。

如果使用傳統的數據庫來處理的話,一般都是自己判斷過期,這樣更麻煩並且性能要差很多。

Redis 是如何判斷數據是否過期的呢?

Redis 通過一個叫做過期字典(可以看作是 hash 表)來保存數據過期的時間。過期字典的鍵指向 Redis 數據庫中的某個 key(鍵),過期字典的值是一個 long long 類型的整數,這個整數保存了 key 所指向的數據庫鍵的過期時間(毫秒精度的 UNIX 時間戳)。

redis過期字典

過期字典是存儲在 redisDb 這個結構裏的:

typedef struct redisDb {
    ...

    dict *dict;     //數據庫鍵空間,保存着數據庫中所有鍵值對
    dict *expires   // 過期字典,保存着鍵的過期時間
    ...
} redisDb;

過期的數據的刪除策略瞭解麼?

如果假設你設置了一批 key 只能存活 1 分鐘,那麼 1 分鐘後,Redis 是怎麼對這批 key 進行刪除的呢?

常用的過期數據的刪除策略就兩個(重要!自己造緩存輪子的時候需要格外考慮的東西):

  1. 惰性刪除 :只會在取出 key 的時候纔對數據進行過期檢查。這樣對 CPU 最友好,但是可能會造成太多過期 key 沒有被刪除。
  2. 定期刪除 : 每隔一段時間抽取一批 key 執行刪除過期 key 操作。並且,Redis 底層會通過限制刪除操作執行的時長和頻率來減少刪除操作對 CPU 時間的影響。

定期刪除對內存更加友好,惰性刪除對 CPU 更加友好。兩者各有千秋,所以 Redis 採用的是 定期刪除+惰性/懶漢式刪除

但是,僅僅通過給 key 設置過期時間還是有問題的。因爲還是可能存在定期刪除和惰性刪除漏掉了很多過期 key 的情況。這樣就導致大量過期 key 堆積在內存裏,然後就 Out of memory 了。

怎麼解決這個問題呢?答案就是:Redis 內存淘汰機制。

Redis 內存淘汰機制瞭解麼?

相關問題:MySQL 裏有 2000w 數據,Redis 中只存 20w 的數據,如何保證 Redis 中的數據都是熱點數據?

Redis 提供 6 種數據淘汰策略:

  1. volatile-lru(least recently used):從已設置過期時間的數據集(server.db[i].expires)中挑選最近最少使用的數據淘汰
  2. volatile-ttl:從已設置過期時間的數據集(server.db[i].expires)中挑選將要過期的數據淘汰
  3. volatile-random:從已設置過期時間的數據集(server.db[i].expires)中任意選擇數據淘汰
  4. allkeys-lru(least recently used):當內存不足以容納新寫入數據時,在鍵空間中,移除最近最少使用的 key(這個是最常用的)
  5. allkeys-random:從數據集(server.db[i].dict)中任意選擇數據淘汰
  6. no-eviction:禁止驅逐數據,也就是說當內存不足以容納新寫入數據時,新寫入操作會報錯。這個應該沒人使用吧!

4.0 版本後增加以下兩種:

  1. volatile-lfu(least frequently used):從已設置過期時間的數據集(server.db[i].expires)中挑選最不經常使用的數據淘汰
  2. allkeys-lfu(least frequently used):當內存不足以容納新寫入數據時,在鍵空間中,移除最不經常使用的 key

Redis 持久化機制

怎麼保證 Redis 掛掉之後再重啓數據可以進行恢復?

很多時候我們需要持久化數據也就是將內存中的數據寫入到硬盤裏面,大部分原因是爲了之後重用數據(比如重啓機器、機器故障之後恢復數據),或者是爲了防止系統故障而將數據備份到一個遠程位置。

Redis 不同於 Memcached 的很重要一點就是,Redis 支持持久化,而且支持兩種不同的持久化操作。Redis 的一種持久化方式叫快照(snapshotting,RDB),另一種方式是隻追加文件(append-only file, AOF)。這兩種方法各有千秋,下面我會詳細這兩種持久化方法是什麼,怎麼用,如何選擇適合自己的持久化方法。

什麼是 RDB 持久化?

Redis 可以通過創建快照來獲得存儲在內存裏面的數據在某個時間點上的副本。Redis 創建快照之後,可以對快照進行備份,可以將快照複製到其他服務器從而創建具有相同數據的服務器副本(Redis 主從結構,主要用來提高 Redis 性能),還可以將快照留在原地以便重啓服務器的時候使用。

快照持久化是 Redis 默認採用的持久化方式,在 redis.conf 配置文件中默認有此下配置:

save 900 1           #在900秒(15分鐘)之後,如果至少有1個key發生變化,Redis就會自動觸發bgsave命令創建快照。

save 300 10          #在300秒(5分鐘)之後,如果至少有10個key發生變化,Redis就會自動觸發bgsave命令創建快照。

save 60 10000        #在60秒(1分鐘)之後,如果至少有10000個key發生變化,Redis就會自動觸發bgsave命令創建快照。

RDB 創建快照時會阻塞主線程嗎?

Redis 提供了兩個命令來生成 RDB 快照文件:

  • save : 主線程執行,會阻塞主線程;
  • bgsave : 子線程執行,不會阻塞主線程,默認選項。

什麼是 AOF 持久化?

與快照持久化相比,AOF 持久化的實時性更好,因此已成爲主流的持久化方案。默認情況下 Redis 沒有開啓 AOF(append only file)方式的持久化,可以通過 appendonly 參數開啓:

appendonly yes

開啓 AOF 持久化後每執行一條會更改 Redis 中的數據的命令,Redis 就會將該命令寫入到內存緩存 server.aof_buf 中,然後再根據 appendfsync 配置來決定何時將其同步到硬盤中的 AOF 文件。

AOF 文件的保存位置和 RDB 文件的位置相同,都是通過 dir 參數設置的,默認的文件名是 appendonly.aof

在 Redis 的配置文件中存在三種不同的 AOF 持久化方式,它們分別是:

appendfsync always    #每次有數據修改發生時都會寫入AOF文件,這樣會嚴重降低Redis的速度
appendfsync everysec  #每秒鐘同步一次,顯式地將多個寫命令同步到硬盤
appendfsync no        #讓操作系統決定何時進行同步

爲了兼顧數據和寫入性能,用戶可以考慮 appendfsync everysec 選項 ,讓 Redis 每秒同步一次 AOF 文件,Redis 性能幾乎沒受到任何影響。而且這樣即使出現系統崩潰,用戶最多隻會丟失一秒之內產生的數據。當硬盤忙於執行寫入操作的時候,Redis 還會優雅的放慢自己的速度以便適應硬盤的最大寫入速度。

相關 issue

AOF 日誌是如何實現的?

關係型數據庫(如 MySQL)通常都是執行命令之前記錄日誌(方便故障恢復),而 Redis AOF 持久化機制是在執行完命令之後再記錄日誌。

在這裏插入圖片描述

爲什麼是在執行完命令之後記錄日誌呢?

  • 避免額外的檢查開銷,AOF 記錄日誌不會對命令進行語法檢查;
  • 在命令執行完之後再記錄,不會阻塞當前的命令執行。

這樣也帶來了風險(我在前面介紹 AOF 持久化的時候也提到過):

  • 如果剛執行完命令 Redis 就宕機會導致對應的修改丟失;
  • 可能會阻塞後續其他命令的執行(AOF 記錄日誌是在 Redis 主線程中進行的)。

AOF 重寫了解嗎?

當 AOF 變得太大時,Redis 能夠在後臺自動重寫 AOF 產生一個新的 AOF 文件,這個新的 AOF 文件和原有的 AOF 文件所保存的數據庫狀態一樣,但體積更小。

AOF 重寫是一個有歧義的名字,該功能是通過讀取數據庫中的鍵值對來實現的,程序無須對現有 AOF 文件進行任何讀入、分析或者寫入操作。

在執行 BGREWRITEAOF 命令時,Redis 服務器會維護一個 AOF 重寫緩衝區,該緩衝區會在子進程創建新 AOF 文件期間,記錄服務器執行的所有寫命令。當子進程完成創建新 AOF 文件的工作之後,服務器會將重寫緩衝區中的所有內容追加到新 AOF 文件的末尾,使得新的 AOF 文件保存的數據庫狀態與現有的數據庫狀態一致。最後,服務器用新的 AOF 文件替換舊的 AOF 文件,以此來完成 AOF 文件重寫操作。

Redis 7.0 版本之前,如果在重寫期間有寫入命令,AOF 可能會使用大量內存,重寫期間到達的所有寫入命令都會寫入磁盤兩次。

如何選擇 RDB 和 AOF?

關於 RDB 和 AOF 的優缺點,官網上面也給了比較詳細的說明Redis persistence,這裏結合自己的理解簡單總結一下。

RDB 比 AOF 優秀的地方

  • RDB 文件存儲的內容是經過壓縮的二進制數據, 保存着某個時間點的數據集,文件很小,適合做數據的備份,災難恢復。AOF 文件存儲的是每一次寫命令,類似於 MySQL 的 binlog 日誌,通常會必 RDB 文件大很多。當 AOF 變得太大時,Redis 能夠在後臺自動重寫 AOF。新的 AOF 文件和原有的 AOF 文件所保存的數據庫狀態一樣,但體積更小。不過, Redis 7.0 版本之前,如果在重寫期間有寫入命令,AOF 可能會使用大量內存,重寫期間到達的所有寫入命令都會寫入磁盤兩次。
  • 使用 RDB 文件恢復數據,直接解析還原數據即可,不需要一條一條地執行命令,速度非常快。而 AOF 則需要依次執行每個寫命令,速度非常慢。也就是說,與 AOF 相比,恢復大數據集的時候,RDB 速度更快。

AOF 比 RDB 優秀的地方

  • RDB 的數據安全性不如 AOF,沒有辦法實時或者秒級持久化數據。生成 RDB 文件的過程是比繁重的, 雖然 BGSAVE 子進程寫入 RDB 文件的工作不會阻塞主線程,但會對機器的 CPU 資源和內存資源產生影響,嚴重的情況下甚至會直接把 Redis 服務幹宕機。AOF 支持秒級數據丟失(取決 fsync 策略,如果是 everysec,最多丟失 1 秒的數據),僅僅是追加命令到 AOF 文件,操作輕量。
  • RDB 文件是以特定的二進制格式保存的,並且在 Redis 版本演進中有多個版本的 RDB,所以存在老版本的 Redis 服務不兼容新版本的 RDB 格式的問題。
  • AOF 以一種易於理解和解析的格式包含所有操作的日誌。你可以輕鬆地導出 AOF 文件進行分析,你也可以直接操作 AOF 文件來解決一些問題。比如,如果執行FLUSHALL命令意外地刷新了所有內容後,只要 AOF 文件沒有被重寫,刪除最新命令並重啓即可恢復之前的狀態。

Redis 4.0 對於持久化機制做了什麼優化?

由於 RDB 和 AOF 各有優勢,於是,Redis 4.0 開始支持 RDB 和 AOF 的混合持久化(默認關閉,可以通過配置項 aof-use-rdb-preamble 開啓)。

如果把混合持久化打開,AOF 重寫的時候就直接把 RDB 的內容寫到 AOF 文件開頭。這樣做的好處是可以結合 RDB 和 AOF 的優點, 快速加載同時避免丟失過多的數據。當然缺點也是有的, AOF 裏面的 RDB 部分是壓縮格式不再是 AOF 格式,可讀性較差。

官方文檔地址:https://redis.io/topics/persistence

Redis 事務

如何使用 Redis 事務?

Redis 可以通過 MULTIEXECDISCARDWATCH 等命令來實現事務(transaction)功能。

> MULTI
OK
> SET PROJECT "JavaGuide"
QUEUED
> GET PROJECT
QUEUED
> EXEC
1) OK
2) "JavaGuide"

MULTI 命令後可以輸入多個命令,Redis 不會立即執行這些命令,而是將它們放到隊列,當調用了 EXEC 命令後,再執行所有的命令。

這個過程是這樣的:

  1. 開始事務(MULTI);
  2. 命令入隊(批量操作 Redis 的命令,先進先出(FIFO)的順序執行);
  3. 執行事務(EXEC)。

你也可以通過 DISCARD 命令取消一個事務,它會清空事務隊列中保存的所有命令。

> MULTI
OK
> SET PROJECT "JavaGuide"
QUEUED
> GET PROJECT
QUEUED
> DISCARD
OK

你可以通過WATCH 命令監聽指定的 Key,當調用 EXEC 命令執行事務時,如果一個被 WATCH 命令監視的 Key 被 其他客戶端/Session 修改的話,整個事務都不會被執行。

# 客戶端 1
> SET PROJECT "RustGuide"
OK
> WATCH PROJECT
OK
> MULTI
OK
> SET PROJECT "JavaGuide"
QUEUED

# 客戶端 2
# 在客戶端 1 執行 EXEC 命令提交事務之前修改 PROJECT 的值
> SET PROJECT "GoGuide"

# 客戶端 1
# 修改失敗,因爲 PROJECT 的值被客戶端2修改了
> EXEC
(nil)
> GET PROJECT
"GoGuide"

不過,如果 WATCH事務 在同一個 Session 裏,並且被 WATCH 監視的 Key 被修改的操作發生在事務內部,這個事務是可以被執行成功的(相關 issue :WATCH 命令碰到 MULTI 命令時的不同效果)。

事務內部修改 WATCH 監視的 Key:

> SET PROJECT "JavaGuide"
OK
> WATCH PROJECT
OK
> MULTI
OK
> SET PROJECT "JavaGuide1"
QUEUED
> SET PROJECT "JavaGuide2"
QUEUED
> SET PROJECT "JavaGuide3"
QUEUED
> EXEC
1) OK
2) OK
3) OK
127.0.0.1:6379> GET PROJECT
"JavaGuide3"

事務外部修改 WATCH 監視的 Key:

> SET PROJECT "JavaGuide"
OK
> WATCH PROJECT
OK
> SET PROJECT "JavaGuide2"
OK
> MULTI
OK
> GET USER
QUEUED
> EXEC
(nil)

Redis 官網相關介紹 https://redis.io/topics/transactions 如下:

Redis 事務

Redis 支持原子性嗎?

Redis 的事務和我們平時理解的關係型數據庫的事務不同。我們知道事務具有四大特性: 1. 原子性2. 隔離性3. 持久性4. 一致性

  1. 原子性(Atomicity): 事務是最小的執行單位,不允許分割。事務的原子性確保動作要麼全部完成,要麼完全不起作用;
  2. 隔離性(Isolation): 併發訪問數據庫時,一個用戶的事務不被其他事務所幹擾,各併發事務之間數據庫是獨立的;
  3. 持久性(Durability): 一個事務被提交之後。它對數據庫中數據的改變是持久的,即使數據庫發生故障也不應該對其有任何影響。
  4. 一致性(Consistency): 執行事務前後,數據保持一致,多個事務對同一個數據讀取的結果是相同的;

Redis 事務在運行錯誤的情況下,除了執行過程中出現錯誤的命令外,其他命令都能正常執行。並且,Redis 是不支持回滾(roll back)操作的。因此,Redis 事務其實是不滿足原子性的(而且不滿足持久性)。

Redis 官網也解釋了自己爲啥不支持回滾。簡單來說就是 Redis 開發者們覺得沒必要支持回滾,這樣更簡單便捷並且性能更好。Redis 開發者覺得即使命令執行錯誤也應該在開發過程中就被發現而不是生產過程中。

Redis 爲什麼不支持回滾

你可以將 Redis 中的事務就理解爲 :Redis 事務提供了一種將多個命令請求打包的功能。然後,再按順序執行打包的所有命令,並且不會被中途打斷。

除了不滿足原子性之外,事務中的每條命令都會與 Redis 服務器進行網絡交互,這是比較浪費資源的行爲。明明一次批量執行多個命令就可以了,這種操作實在是看不懂。

因此,Redis 事務是不建議在日常開發中使用的。

相關 issue :

如何解決 Redis 事務的缺陷?

Redis 從 2.6 版本開始支持執行 Lua 腳本,它的功能和事務非常類似。我們可以利用 Lua 腳本來批量執行多條 Redis 命令,這些 Redis 命令會被提交到 Redis 服務器一次性執行完成,大幅減小了網絡開銷。

一段 Lua 腳本可以視作一條命令執行,一段 Lua 腳本執行過程中不會有其他腳本或 Redis 命令同時執行,保證了操作不會被其他指令插入或打擾。

如果 Lua 腳本運行時出錯並中途結束,出錯之後的命令是不會被執行的。並且,出錯之前執行的命令是無法被撤銷的。因此,嚴格來說,通過 Lua 腳本來批量執行 Redis 命令也是不滿足原子性的。

另外,Redis 7.0 新增了 Redis functions 特性,你可以將 Redis functions 看作是比 Lua 更強大的腳本。

Redis 性能優化

Redis bigkey

什麼是 bigkey?

簡單來說,如果一個 key 對應的 value 所佔用的內存比較大,那這個 key 就可以看作是 bigkey。具體多大才算大呢?有一個不是特別精確的參考標準:string 類型的 value 超過 10 kb,複合類型的 value 包含的元素超過 5000 個(對於複合類型的 value 來說,不一定包含的元素越多,佔用的內存就越多)。

bigkey 有什麼危害?

除了會消耗更多的內存空間,bigkey 對性能也會有比較大的影響。

因此,我們應該儘量避免寫入 bigkey!

如何發現 bigkey?

1、使用 Redis 自帶的 --bigkeys 參數來查找。

# redis-cli -p 6379 --bigkeys

# Scanning the entire keyspace to find biggest keys as well as
# average sizes per key type.  You can use -i 0.1 to sleep 0.1 sec
# per 100 SCAN commands (not usually needed).

[00.00%] Biggest string found so far '"ballcat:oauth:refresh_auth:f6cdb384-9a9d-4f2f-af01-dc3f28057c20"' with 4437 bytes
[00.00%] Biggest list   found so far '"my-list"' with 17 items

-------- summary -------

Sampled 5 keys in the keyspace!
Total key length in bytes is 264 (avg len 52.80)

Biggest   list found '"my-list"' has 17 items
Biggest string found '"ballcat:oauth:refresh_auth:f6cdb384-9a9d-4f2f-af01-dc3f28057c20"' has 4437 bytes

1 lists with 17 items (20.00% of keys, avg size 17.00)
0 hashs with 0 fields (00.00% of keys, avg size 0.00)
4 strings with 4831 bytes (80.00% of keys, avg size 1207.75)
0 streams with 0 entries (00.00% of keys, avg size 0.00)
0 sets with 0 members (00.00% of keys, avg size 0.00)
0 zsets with 0 members (00.00% of keys, avg size 0.00

從這個命令的運行結果,我們可以看出:這個命令會掃描(Scan) Redis 中的所有 key ,會對 Redis 的性能有一點影響。並且,這種方式只能找出每種數據結構 top 1 bigkey(佔用內存最大的 string 數據類型,包含元素最多的複合數據類型)。

2、分析 RDB 文件

通過分析 RDB 文件來找出 big key。這種方案的前提是你的 Redis 採用的是 RDB 持久化。

網上有現成的代碼/工具可以直接拿來使用:

  • redis-rdb-tools :Python 語言寫的用來分析 Redis 的 RDB 快照文件用的工具
  • rdb_bigkeys : Go 語言寫的用來分析 Redis 的 RDB 快照文件用的工具,性能更好。

大量 key 集中過期問題

我在上面提到過:對於過期 key,Redis 採用的是 定期刪除+惰性/懶漢式刪除 策略。

定期刪除執行過程中,如果突然遇到大量過期 key 的話,客戶端請求必須等待定期清理過期 key 任務線程執行完成,因爲這個這個定期任務線程是在 Redis 主線程中執行的。這就導致客戶端請求沒辦法被及時處理,響應速度會比較慢。

如何解決呢?下面是兩種常見的方法:

  1. 給 key 設置隨機過期時間。
  2. 開啓 lazy-free(惰性刪除/延遲釋放) 。lazy-free 特性是 Redis 4.0 開始引入的,指的是讓 Redis 採用異步方式延遲釋放 key 使用的內存,將該操作交給單獨的子線程處理,避免阻塞主線程。

個人建議不管是否開啓 lazy-free,我們都儘量給 key 設置隨機過期時間。

Redis 生產問題

緩存穿透

什麼是緩存穿透?

緩存穿透說簡單點就是大量請求的 key 是不合理的,根本不存在於緩存中,也不存在於數據庫中 。這就導致這些請求直接到了數據庫上,根本沒有經過緩存這一層,對數據庫造成了巨大的壓力,可能直接就被這麼多請求弄宕機了。

緩存穿透

舉個例子:某個黑客故意製造一些非法的 key 發起大量請求,導致大量請求落到數據庫,結果數據庫上也沒有查到對應的數據。也就是說這些請求最終都落到了數據庫上,對數據庫造成了巨大的壓力。

有哪些解決辦法?

最基本的就是首先做好參數校驗,一些不合法的參數請求直接拋出異常信息返回給客戶端。比如查詢的數據庫 id 不能小於 0、傳入的郵箱格式不對的時候直接返回錯誤消息給客戶端等等。

1)緩存無效 key

如果緩存和數據庫都查不到某個 key 的數據就寫一個到 Redis 中去並設置過期時間,具體命令如下: SET key value EX 10086 。這種方式可以解決請求的 key 變化不頻繁的情況,如果黑客惡意攻擊,每次構建不同的請求 key,會導致 Redis 中緩存大量無效的 key 。很明顯,這種方案並不能從根本上解決此問題。如果非要用這種方式來解決穿透問題的話,儘量將無效的 key 的過期時間設置短一點比如 1 分鐘。

另外,這裏多說一嘴,一般情況下我們是這樣設計 key 的: 表名:列名:主鍵名:主鍵值

如果用 Java 代碼展示的話,差不多是下面這樣的:

public Object getObjectInclNullById(Integer id) {
    // 從緩存中獲取數據
    Object cacheValue = cache.get(id);
    // 緩存爲空
    if (cacheValue == null) {
        // 從數據庫中獲取
        Object storageValue = storage.get(key);
        // 緩存空對象
        cache.set(key, storageValue);
        // 如果存儲數據爲空,需要設置一個過期時間(300秒)
        if (storageValue == null) {
            // 必須設置過期時間,否則有被攻擊的風險
            cache.expire(key, 60 * 5);
        }
        return storageValue;
    }
    return cacheValue;
}

2)布隆過濾器

布隆過濾器是一個非常神奇的數據結構,通過它我們可以非常方便地判斷一個給定數據是否存在於海量數據中。我們需要的就是判斷 key 是否合法,有沒有感覺布隆過濾器就是我們想要找的那個“人”。

具體是這樣做的:把所有可能存在的請求的值都存放在布隆過濾器中,當用戶請求過來,先判斷用戶發來的請求的值是否存在於布隆過濾器中。不存在的話,直接返回請求參數錯誤信息給客戶端,存在的話纔會走下面的流程。

加入布隆過濾器之後的緩存處理流程圖如下。

加入布隆過濾器之後的緩存處理流程圖

但是,需要注意的是布隆過濾器可能會存在誤判的情況。總結來說就是: 布隆過濾器說某個元素存在,小概率會誤判。布隆過濾器說某個元素不在,那麼這個元素一定不在。

爲什麼會出現誤判的情況呢? 我們還要從布隆過濾器的原理來說!

我們先來看一下,當一個元素加入布隆過濾器中的時候,會進行哪些操作:

  1. 使用布隆過濾器中的哈希函數對元素值進行計算,得到哈希值(有幾個哈希函數得到幾個哈希值)。
  2. 根據得到的哈希值,在位數組中把對應下標的值置爲 1。

我們再來看一下,當我們需要判斷一個元素是否存在於布隆過濾器的時候,會進行哪些操作:

  1. 對給定元素再次進行相同的哈希計算;
  2. 得到值之後判斷位數組中的每個元素是否都爲 1,如果值都爲 1,那麼說明這個值在布隆過濾器中,如果存在一個值不爲 1,說明該元素不在布隆過濾器中。

然後,一定會出現這樣一種情況:不同的字符串可能哈希出來的位置相同。 (可以適當增加位數組大小或者調整我們的哈希函數來降低概率)

更多關於布隆過濾器的內容可以看我的這篇原創:《不瞭解布隆過濾器?一文給你整的明明白白!》 ,強烈推薦,個人感覺網上應該找不到總結的這麼明明白白的文章了。

緩存擊穿

什麼是緩存擊穿?

緩存擊穿中,請求的 key 對應的是 熱點數據 ,該數據 存在於數據庫中,但不存在於緩存中(通常是因爲緩存中的那份數據已經過期) 。這就可能會導致瞬時大量的請求直接打到了數據庫上,對數據庫造成了巨大的壓力,可能直接就被這麼多請求弄宕機了。

緩存擊穿

舉個例子 :秒殺進行過程中,緩存中的某個秒殺商品的數據突然過期,這就導致瞬時大量對該商品的請求直接落到數據庫上,對數據庫造成了巨大的壓力。

有哪些解決辦法?

  • 設置熱點數據永不過期或者過期時間比較長。
  • 針對熱點數據提前預熱,將其存入緩存中並設置合理的過期時間比如秒殺場景下的數據在秒殺結束之前不過期。
  • 請求數據庫寫數據到緩存之前,先獲取互斥鎖,保證只有一個請求會落到數據庫上,減少數據庫的壓力。

緩存穿透和緩存擊穿有什麼區別?

緩存穿透中,請求的 key 既不存在於緩存中,也不存在於數據庫中。

緩存擊穿中,請求的 key 對應的是 熱點數據 ,該數據 存在於數據庫中,但不存在於緩存中(通常是因爲緩存中的那份數據已經過期)

緩存雪崩

什麼是緩存雪崩?

我發現緩存雪崩這名字起的有點意思,哈哈。

實際上,緩存雪崩描述的就是這樣一個簡單的場景:緩存在同一時間大面積的失效,導致大量的請求都直接落到了數據庫上,對數據庫造成了巨大的壓力。 這就好比雪崩一樣,摧枯拉朽之勢,數據庫的壓力可想而知,可能直接就被這麼多請求弄宕機了。

另外,緩存服務宕機也會導致緩存雪崩現象,導致所有的請求都落到了數據庫上。

緩存雪崩

舉個例子 :數據庫中的大量數據在同一時間過期,這個時候突然有大量的請求需要訪問這些過期的數據。這就導致大量的請求直接落到數據庫上,對數據庫造成了巨大的壓力。

有哪些解決辦法?

針對 Redis 服務不可用的情況:

  1. 採用 Redis 集羣,避免單機出現問題整個緩存服務都沒辦法使用。
  2. 限流,避免同時處理大量的請求。

針對熱點緩存失效的情況:

  1. 設置不同的失效時間比如隨機設置緩存的失效時間。
  2. 緩存永不失效(不太推薦,實用性太差)。
  3. 設置二級緩存。

緩存雪崩和緩存擊穿有什麼區別?

緩存雪崩和緩存擊穿比較像,但緩存雪崩導致的原因是緩存中的大量或者所有數據失效,緩存擊穿導致的原因主要是某個熱點數據不存在與緩存中(通常是因爲緩存中的那份數據已經過期)。

如何保證緩存和數據庫數據的一致性?

細說的話可以扯很多,但是我覺得其實沒太大必要(小聲 BB:很多解決方案我也沒太弄明白)。我個人覺得引入緩存之後,如果爲了短時間的不一致性問題,選擇讓系統設計變得更加複雜的話,完全沒必要。

下面單獨對 Cache Aside Pattern(旁路緩存模式) 來聊聊。

Cache Aside Pattern 中遇到寫請求是這樣的:更新 DB,然後直接刪除 cache 。

如果更新數據庫成功,而刪除緩存這一步失敗的情況的話,簡單說兩個解決方案:

  1. 緩存失效時間變短(不推薦,治標不治本) :我們讓緩存數據的過期時間變短,這樣的話緩存就會從數據庫中加載數據。另外,這種解決辦法對於先操作緩存後操作數據庫的場景不適用。
  2. 增加 cache 更新重試機制(常用): 如果 cache 服務當前不可用導致緩存刪除失敗的話,我們就隔一段時間進行重試,重試次數可以自己定。如果多次重試還是失敗的話,我們可以把當前更新失敗的 key 存入隊列中,等緩存服務可用之後,再將緩存中對應的 key 刪除即可。

相關文章推薦:緩存和數據庫一致性問題,看這篇就夠了 - 水滴與銀彈

Redis 集羣

Redis Sentinel

  1. 什麼是 Sentinel? 有什麼用?
  2. Sentinel 如何檢測節點是否下線?主觀下線與客觀下線的區別?
  3. Sentinel 是如何實現故障轉移的?
  4. 爲什麼建議部署多個 sentinel 節點(哨兵集羣)?
  5. Sentinel 如何選擇出新的 master(選舉機制)?
  6. 如何從 Sentinel 集羣中選擇出 Leader ?
  7. Sentinel 可以防止腦裂嗎?

Redis Cluster

  1. 爲什麼需要 Redis Cluster?解決了什麼問題?有什麼優勢?
  2. Redis Cluster 是如何分片的?
  3. 爲什麼 Redis Cluster 的哈希槽是 16384 個?
  4. 如何確定給定 key 的應該分佈到哪個哈希槽中?
  5. Redis Cluster 支持重新分配哈希槽嗎?
  6. Redis Cluster 擴容縮容期間可以提供服務嗎?
  7. Redis Cluster 中的節點是怎麼進行通信的?

參考答案Redis 集羣詳解(付費)

參考

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