2萬字總結Redis常用面試題/知識點(2021最新)

大家好,我是躍哥。上次和大家分享了關於MySQL的一點小知識,那這次我們趁熱打鐵,學習下數據存儲中的Redis,在項目中也是超級實用的那種噢。

本文是開源項目 JavaGuide 其中的一篇文章。項目地址:https://github.com/Snailclimb/JavaGuide

另外,這篇文章之前發過,不過,我最近對其進行了重構完善並且修復了很多小問題。所以,在公號再同步一下!

內容很硬!強烈建議小夥伴們花 15 分鐘左右閱讀一遍!

1. 簡單介紹一下 Redis 唄!

簡單來說 Redis 就是一個使用 C 語言開發的數據庫,不過與傳統數據庫不同的是 Redis 的數據是存在內存中的 ,也就是它是內存數據庫,所以讀寫速度非常快,因此 Redis 被廣泛應用於緩存方向。

另外,Redis 除了做緩存之外,也經常用來做分佈式鎖,甚至是消息隊列。

Redis 提供了多種數據類型來支持不同的業務場景。Redis 還支持事務 、持久化、Lua 腳本、多種集羣方案。

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

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

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

分佈式緩存主要解決的是單機緩存的容量受服務器限制並且無法保存通用信息的問題。因爲,本地緩存只在當前服務裏有效,比如如果你部署了兩個相同的服務,他們兩者之間的緩存數據是無法共同的。

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

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

共同點

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

區別

  1. Redis 支持更豐富的數據類型(支持更復雜的應用場景)。Redis 不僅僅支持簡單的 k/v 類型的數據,同時還提供 list,set,zset,hash 等數據結構的存儲。Memcached 只支持最簡單的 k/v 數據類型。
  2. Redis 支持數據的持久化,可以將內存中的數據保持在磁盤中,重啓的時候可以再次加載進行使用,而 Memecache 把數據全部存在內存之中。
  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 來作爲自己項目的分佈式緩存了。

4. 緩存數據的處理流程是怎樣的?

作爲暖男一號,我給大家畫了一個草圖。

正常緩存處理流程

簡單來說就是:

  1. 如果用戶請求的數據在緩存中就直接返回。
  2. 緩存中不存在的話就看數據庫中是否存在。
  3. 數據庫中存在的話就更新緩存中的數據。
  4. 數據庫中不存在的話就返回空數據。

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

簡單,來說使用緩存主要是爲了提升用戶體驗以及應對更多的用戶。

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

img

高性能

對照上面 👆 我畫的圖。我們設想這樣的場景:

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

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

不過,要保持數據庫和緩存中的數據的一致性。如果數據庫中的對應數據改變的之後,同步改變緩存中相應的數據即可!

高併發:

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

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

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

6. Redis 常見數據結構以及使用場景分析

你可以自己本機安裝 redis 或者通過 redis 官網提供的在線 redis 環境。

try-redis

6.1. string

  1. 介紹 :string 數據結構是簡單的 key-value 類型。雖然 Redis 是用 C 語言寫的,但是 Redis 並沒有使用 C 的字符串表示,而是自己構建了一種 簡單動態字符串(simple dynamic string, SDS)。相比於 C 的原生字符串,Redis 的 SDS 不光可以保存文本數據還可以保存二進制數據,並且獲取字符串長度複雜度爲 O(1)(C 字符串爲 O(N)),除此之外,Redis 的 SDS API 是安全的,不會造成緩衝區溢出。
  2. 常用命令: set,get,strlen,exists,decr,incr,setex 等等。
  3. 應用場景: 一般常用在需要計數的場景,比如用戶的訪問次數、熱點文章的點贊轉發數量等等。

下面我們簡單看看它的使用!

普通字符串的基本操作:

127.0.0.1:6379> set key value #設置 key-value 類型的值
OK
127.0.0.1:6379> get key # 根據 key 獲得對應的 value
"value"
127.0.0.1:6379> exists key  # 判斷某個 key 是否存在
(integer) 1
127.0.0.1:6379> strlen key # 返回 key 所儲存的字符串值的長度。
(integer) 5
127.0.0.1:6379> del key # 刪除某個 key 對應的值
(integer) 1
127.0.0.1:6379> get key
(nil)Copy to clipboardErrorCopied

批量設置 :

127.0.0.1:6379> mset key1 value1 key2 value2 # 批量設置 key-value 類型的值
OK
127.0.0.1:6379> mget key1 key2 # 批量獲取多個 key 對應的 value
1) "value1"
2) "value2"Copy to clipboardErrorCopied

計數器(字符串的內容爲整數的時候可以使用):

127.0.0.1:6379> set number 1
OK
127.0.0.1:6379> incr number # 將 key 中儲存的數字值增一
(integer) 2
127.0.0.1:6379> get number
"2"
127.0.0.1:6379> decr number # 將 key 中儲存的數字值減一
(integer) 1
127.0.0.1:6379> get number
"1"Copy to clipboardErrorCopied

過期(默認爲永不過期)

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) 56Copy to clipboardErrorCopied

6.2. list

  1. 介紹list 即是 鏈表。鏈表是一種非常常見的數據結構,特點是易於數據元素的插入和刪除並且可以靈活調整鏈表長度,但是鏈表的隨機訪問困難。許多高級編程語言都內置了鏈表的實現比如 Java 中的 LinkedList,但是 C 語言並沒有實現鏈表,所以 Redis 實現了自己的鏈表數據結構。Redis 的 list 的實現爲一個 雙向鏈表,即可以支持反向查找和遍歷,更方便操作,不過帶來了部分額外的內存開銷。
  2. 常用命令: rpush,lpop,lpush,rpop,lrange,llen 等。
  3. 應用場景: 發佈與訂閱或者說消息隊列、慢查詢。

下面我們簡單看看它的使用!

通過 rpush/lpop 實現隊列:

127.0.0.1:6379> rpush myList value1 # 向 list 的頭部(右邊)添加元素
(integer) 1
127.0.0.1:6379> rpush myList value2 value3 # 向list的頭部(最右邊)添加多個元素
(integer) 3
127.0.0.1:6379> lpop myList # 將 list的尾部(最左邊)元素取出
"value1"
127.0.0.1:6379> lrange myList 0 1 # 查看對應下標的list列表, 0 爲 start,1爲 end
1) "value2"
2) "value3"
127.0.0.1:6379> lrange myList 0 -1 # 查看列表中的所有元素,-1表示倒數第一
1) "value2"
2) "value3"Copy to clipboardErrorCopied

通過 rpush/rpop 實現棧:

127.0.0.1:6379> rpush myList2 value1 value2 value3
(integer) 3
127.0.0.1:6379> rpop myList2 # 將 list的頭部(最右邊)元素取出
"value3"Copy to clipboardErrorCopied

我專門花了一個圖方便小夥伴們來理解:

redis list

通過 lrange 查看對應下標範圍的列表元素:

127.0.0.1:6379> rpush myList value1 value2 value3
(integer) 3
127.0.0.1:6379> lrange myList 0 1 # 查看對應下標的list列表, 0 爲 start,1爲 end
1) "value1"
2) "value2"
127.0.0.1:6379> lrange myList 0 -1 # 查看列表中的所有元素,-1表示倒數第一
1) "value1"
2) "value2"
3) "value3"Copy to clipboardErrorCopied

通過 lrange 命令,你可以基於 list 實現分頁查詢,性能非常高!

通過 llen 查看鏈表長度:

127.0.0.1:6379> llen myList
(integer) 3Copy to clipboardErrorCopied

6.3. hash

  1. 介紹 :hash 類似於 JDK1.8 前的 HashMap,內部實現也差不多(數組 + 鏈表)。不過,Redis 的 hash 做了更多優化。另外,hash 是一個 string 類型的 field 和 value 的映射表, 特別適合用於存儲對象,後續操作的時候,你可以直接僅僅修改這個對象中的某個字段的值。比如我們可以 hash 數據結構來存儲用戶信息,商品信息等等。
  2. 常用命令: hset,hmset,hexists,hget,hgetall,hkeys,hvals 等。
  3. 應用場景: 系統中對象數據的存儲。

下面我們簡單看看它的使用!

127.0.0.1:6379> hmset userInfoKey name "guide" description "dev" age "24"
OK
127.0.0.1:6379> hexists userInfoKey name # 查看 key 對應的 value中指定的字段是否存在。
(integer) 1
127.0.0.1:6379> hget userInfoKey name # 獲取存儲在哈希表中指定字段的值。
"guide"
127.0.0.1:6379> hget userInfoKey age
"24"
127.0.0.1:6379> hgetall userInfoKey # 獲取在哈希表中指定 key 的所有字段和值
1) "name"
2) "guide"
3) "description"
4) "dev"
5) "age"
6) "24"
127.0.0.1:6379> hkeys userInfoKey # 獲取 key 列表
1) "name"
2) "description"
3) "age"
127.0.0.1:6379> hvals userInfoKey # 獲取 value 列表
1) "guide"
2) "dev"
3) "24"
127.0.0.1:6379> hset userInfoKey name "GuideGeGe" # 修改某個字段對應的值
127.0.0.1:6379> hget userInfoKey name
"GuideGeGe"Copy to clipboardErrorCopied

6.4. set

  1. 介紹 : set 類似於 Java 中的 HashSet 。Redis 中的 set 類型是一種無序集合,集合中的元素沒有先後順序。當你需要存儲一個列表數據,又不希望出現重複數據時,set 是一個很好的選擇,並且 set 提供了判斷某個成員是否在一個 set 集合內的重要接口,這個也是 list 所不能提供的。可以基於 set 輕易實現交集、並集、差集的操作。比如:你可以將一個用戶所有的關注人存在一個集合中,將其所有粉絲存在一個集合。Redis 可以非常方便的實現如共同關注、共同粉絲、共同喜好等功能。這個過程也就是求交集的過程。
  2. 常用命令: sadd,spop,smembers,sismember,scard,sinterstore,sunion 等。
  3. 應用場景: 需要存放的數據不能重複以及需要獲取多個數據源交集和並集等場景

下面我們簡單看看它的使用!

127.0.0.1:6379> sadd mySet value1 value2 # 添加元素進去
(integer) 2
127.0.0.1:6379> sadd mySet value1 # 不允許有重複元素
(integer) 0
127.0.0.1:6379> smembers mySet # 查看 set 中所有的元素
1) "value1"
2) "value2"
127.0.0.1:6379> scard mySet # 查看 set 的長度
(integer) 2
127.0.0.1:6379> sismember mySet value1 # 檢查某個元素是否存在set 中,只能接收單個元素
(integer) 1
127.0.0.1:6379> sadd mySet2 value2 value3
(integer) 2
127.0.0.1:6379> sinterstore mySet3 mySet mySet2 # 獲取 mySet 和 mySet2 的交集並存放在 mySet3 中
(integer) 1
127.0.0.1:6379> smembers mySet3
1) "value2"Copy to clipboardErrorCopied

6.5. sorted set

  1. 介紹: 和 set 相比,sorted set 增加了一個權重參數 score,使得集合中的元素能夠按 score 進行有序排列,還可以通過 score 的範圍來獲取元素的列表。有點像是 Java 中 HashMap 和 TreeSet 的結合體。
  2. 常用命令: zadd,zcard,zscore,zrange,zrevrange,zrem 等。
  3. 應用場景: 需要對數據根據某個權重進行排序的場景。比如在直播系統中,實時排行信息包含直播間在線用戶列表,各種禮物排行榜,彈幕消息(可以理解爲按消息維度的消息排行榜)等信息。
127.0.0.1:6379> zadd myZset 3.0 value1 # 添加元素到 sorted set 中 3.0 爲權重
(integer) 1
127.0.0.1:6379> zadd myZset 2.0 value2 1.0 value3 # 一次添加多個元素
(integer) 2
127.0.0.1:6379> zcard myZset # 查看 sorted set 中的元素數量
(integer) 3
127.0.0.1:6379> zscore myZset value1 # 查看某個 value 的權重
"3"
127.0.0.1:6379> zrange  myZset 0 -1 # 順序輸出某個範圍區間的元素,0 -1 表示輸出所有元素
1) "value3"
2) "value2"
3) "value1"
127.0.0.1:6379> zrange  myZset 0 1 # 順序輸出某個範圍區間的元素,0 爲 start  1 爲 stop
1) "value3"
2) "value2"
127.0.0.1:6379> zrevrange  myZset 0 1 # 逆序輸出某個範圍區間的元素,0 爲 start  1 爲 stop
1) "value1"
2) "value2"Copy to clipboardErrorCopied

6.6 bitmap

  1. 介紹: bitmap 存儲的是連續的二進制數字(0 和 1),通過 bitmap, 只需要一個 bit 位來表示某個元素對應的值或者狀態,key 就是對應元素本身 。我們知道 8 個 bit 可以組成一個 byte,所以 bitmap 本身會極大的節省儲存空間。
  2. 常用命令: setbitgetbitbitcountbitop
  3. 應用場景: 適合需要保存狀態信息(比如是否簽到、是否登錄...)並需要進一步對這些信息進行分析的場景。比如用戶簽到情況、活躍用戶情況、用戶行爲統計(比如是否點贊過某個視頻)。
# SETBIT 會返回之前位的值(默認是 0)這裏會生成 7 個位
127.0.0.1:6379> setbit mykey 7 1
(integer) 0
127.0.0.1:6379> setbit mykey 7 0
(integer) 1
127.0.0.1:6379> getbit mykey 7
(integer) 0
127.0.0.1:6379> setbit mykey 6 1
(integer) 0
127.0.0.1:6379> setbit mykey 8 1
(integer) 0
# 通過 bitcount 統計被被設置爲 1 的位的數量。
127.0.0.1:6379> bitcount mykey
(integer) 2Copy to clipboardErrorCopied

針對上面提到的一些場景,這裏進行進一步說明。

使用場景一:用戶行爲分析 很多網站爲了分析你的喜好,需要研究你點贊過的內容。

# 記錄你喜歡過 001 號小姐姐
127.0.0.1:6379> setbit beauty_girl_001 uid 1Copy to clipboardErrorCopied

使用場景二:統計活躍用戶

使用時間作爲 key,然後用戶 ID 爲 offset,如果當日活躍過就設置爲 1

那麼我該如何計算某幾天/月/年的活躍用戶呢(暫且約定,統計時間內只要有一天在線就稱爲活躍),有請下一個 redis 的命令

# 對一個或多個保存二進制位的字符串 key 進行位元操作,並將結果保存到 destkey 上。
# BITOP 命令支持 AND 、 OR 、 NOT 、 XOR 這四種操作中的任意一種參數
BITOP operation destkey key [key ...]Copy to clipboardErrorCopied

初始化數據:

127.0.0.1:6379> setbit 20210308 1 1
(integer) 0
127.0.0.1:6379> setbit 20210308 2 1
(integer) 0
127.0.0.1:6379> setbit 20210309 1 1
(integer) 0Copy to clipboardErrorCopied

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

127.0.0.1:6379> bitop and desk1 20210308 20210309
(integer) 1
127.0.0.1:6379> bitcount desk1
(integer) 1Copy to clipboardErrorCopied

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

127.0.0.1:6379> bitop or desk2 20210308 20210309
(integer) 1
127.0.0.1:6379> bitcount desk2
(integer) 2Copy to clipboardErrorCopied

使用場景三:用戶在線狀態

對於獲取或者統計用戶在線狀態,使用 bitmap 是一個節約空間且效率又高的一種方法。

只需要一個 key,然後用戶 ID 爲 offset,如果在線就設置爲 1,不在線就設置爲 0。

7. Redis 單線程模型詳解

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

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

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

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

另外, Redis 服務器是一個事件驅動程序,服務器需要處理兩類事件:1. 文件事件; 2. 時間事件。

時間事件不需要多花時間瞭解,我們接觸最多的還是 文件事件(客戶端進行讀取寫入等操作,涉及一系列網絡通信)。

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

Redis 基於 Reactor 模式開發了自己的網絡事件處理器:這個處理器被稱爲文件事件處理器(file event handler)。文件事件處理器使用 I/O 多路複用(multiplexing)程序來同時監聽多個套接字,並根據套接字目前執行的任務來爲套接字關聯不同的事件處理器。

當被監聽的套接字準備好執行連接應答(accept)、讀取(read)、寫入(write)、關 閉(close)等操作時,與操作相對應的文件事件就會產生,這時文件事件處理器就會調用套接字之前關聯好的事件處理器來處理這些事件。

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

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

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

《Redis設計與實現:12章》

8. Redis 沒有使用多線程?爲什麼不使用多線程?

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

redis4.0 more thread

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

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

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

我覺得主要原因有下面 3 個:

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

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

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

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

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

io-threads-do-reads yesCopy to clipboardErrorCopied

開啓多線程後,還需要設置線程數,否則是不生效的。同樣需要修改 redis 配置文件 redis.conf :

io-threads 4 #官網建議4核的機器建議設置爲2或3個線程,8核的建議設置爲6個線程Copy to clipboardErrorCopied

推薦閱讀:

  1. Redis 6.0 新特性-多線程連環 13 問!
  2. 爲什麼 Redis 選擇單線程模型

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

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

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

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

127.0.0.1:6379> exp 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) 56Copy to clipboardErrorCopied

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

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

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

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

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

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

redis過期字典

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

typedef struct redisDb {
    ...

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

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

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

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

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

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

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

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

13. 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

14. Redis 持久化機制(怎麼保證 Redis 掛掉之後再重啓數據可以進行恢復)

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

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

快照(snapshotting)持久化(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命令創建快照。Copy to clipboardErrorCopied

AOF(append-only file)持久化

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

appendonly yesCopy to clipboardErrorCopied

開啓 AOF 持久化後每執行一條會更改 Redis 中的數據的命令,Redis 就會將該命令寫入硬盤中的 AOF 文件。AOF 文件的保存位置和 RDB 文件的位置相同,都是通過 dir 參數設置的,默認的文件名是 appendonly.aof。

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

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

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

相關 issue :783:Redis 的 AOF 方式

拓展:Redis 4.0 對於持久化機制的優化

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

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

補充內容:AOF 重寫

AOF 重寫可以產生一個新的 AOF 文件,這個新的 AOF 文件和原有的 AOF 文件所保存的數據庫狀態一樣,但體積更小。

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

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

15. Redis 事務

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

> MULTI
OK
> SET USER "Guide哥"
QUEUED
> GET USER
QUEUED
> EXEC
1) OK
2) "Guide哥"Copy to clipboardErrorCopied

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

這個過程是這樣的:

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

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

> MULTI
OK
> SET USER "Guide哥"
QUEUED
> GET USER
QUEUED
> DISCARD
OKCopy to clipboardErrorCopied

WATCH 命令用於監聽指定的鍵,當調用 EXEC 命令執行事務時,如果一個被 WATCH 命令監視的鍵被修改的話,整個事務都不會執行,直接返回失敗。

> WATCH USER
OK
> MULTI
> SET USER "Guide哥"
OK
> GET USER
Guide哥
> EXEC
ERR EXEC without MULTICopy to clipboardErrorCopied

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

redis事務

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

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

Redis 是不支持 roll back 的,因而不滿足原子性的(而且不滿足持久性)。

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

redis roll back

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

相關 issue :

  • issue452: 關於 Redis 事務不滿足原子性的問題 。
  • Issue491:關於 redis 沒有事務回滾?

16. 緩存穿透

16.1. 什麼是緩存穿透?

緩存穿透說簡單點就是大量請求的 key 根本不存在於緩存中,導致請求直接到了數據庫上,根本沒有經過緩存這一層。舉個例子:某個黑客故意製造我們緩存中不存在的 key 發起大量請求,導致大量請求落到數據庫。

16.2. 緩存穿透情況的處理流程是怎樣的?

如下圖所示,用戶的請求最終都要跑到數據庫中查詢一遍。

緩存穿透情況

16.3. 有哪些解決辦法?

最基本的就是首先做好參數校驗,一些不合法的參數請求直接拋出異常信息返回給客戶端。比如查詢的數據庫 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;
}Copy to clipboardErrorCopied

2)布隆過濾器

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

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

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

image

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

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

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

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

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

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

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

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

17. 緩存雪崩

17.1. 什麼是緩存雪崩?

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

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

舉個例子:系統的緩存模塊出了問題比如宕機導致不可用。造成系統的所有訪問,都要走數據庫。

還有一種緩存雪崩的場景是:有一些被大量訪問數據(熱點緩存)在某一時刻大面積失效,導致對應的請求直接落到了數據庫上。 這樣的情況,有下面幾種解決辦法:

舉個例子 :秒殺開始 12 個小時之前,我們統一存放了一批商品到 Redis 中,設置的緩存過期時間也是 12 個小時,那麼秒殺開始的時候,這些秒殺的商品的訪問直接就失效了。導致的情況就是,相應的請求直接就落到了數據庫上,就像雪崩一樣可怕。

17.2. 有哪些解決辦法?

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

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

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

  1. 設置不同的失效時間比如隨機設置緩存的失效時間。
  2. 緩存永不失效。

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

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

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

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

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

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

19. 參考

  • 《Redis 開發與運維》
  • 《Redis 設計與實現》
  • Redis 命令總結:http://Redisdoc.com/string/set.html
  • 通俗易懂的 Redis 數據結構基礎教程:https://juejin.im/post/5b53ee7e5188251aaa2d2e16
  • WHY Redis choose single thread (vs multi threads): https://medium.com/@jychen7/sharing-redis-single-thread-vs-multi-threads-5870bd44d153






0、重磅!兩萬字長文總結,梳理 Java 入門進階哪些事(推薦收藏)

1、講真的:我達成了一個優秀的小目標

本文分享自微信公衆號 - 程序員小躍(runningdimple)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

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