Redis 常見問題總結


1. 簡單介紹一下 Redis 唄!

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

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

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

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

分布式緩存的話,使用的比較多的主要是 Memcached 和 Redis。不過,現在基本沒有看過還有項目使用 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. 緩存數據的處理流程是怎樣的?

作為暖男一號,我給大家畫了一個草圖。
緩存的處理流程.png

簡單來說就是:

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

5. 為什么要用 Redis/為什么要用緩存?

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

下面我們主要從“高性能”和“高並發”這兩點來看待這個問題。
使用緩存之后.png

高性能 :

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

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

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

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

高並發:

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

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

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

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

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

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,dect,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)

批量設置 :

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"

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

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"

過期

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

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"

通過 rpush/rpop 實現棧:

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

我專門花了一個圖方便小伙伴們來理解:
redis-list.png

通過 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"

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

通過 llen 查看鏈表長度:

127.0.0.1:6379> llen myList
(integer) 3

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> hset 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"

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"

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"

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 關聯到相應的事件處理器)
  • 事件處理器(連接應答處理器、命令請求處理器、命令回復處理器)
    redis事件處理器.png
    《Redis設計與實現:12章》

8. Redis 沒有使用多線程?為什么不使用多線程?

雖然說 Redis 是單線程模型,但是, 實際上,Redis 在 4.0 之后的版本中就已經加入了對多線程的支持。
redis4.0-more-thread.png

不過,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 yes

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

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

推薦閱讀:

  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) 56

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

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

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

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

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

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

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

typedef struct redisDb {
    ...

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

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命令創建快照。

AOF(append-only file)持久化

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

appendonly yes

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

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

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

為了兼顧數據和寫入性能,用戶可以考慮 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 可以通過 MULTI,EXEC,DISCARD 和 WATCH 等命令來實現事務(transaction)功能。

> MULTI
OK
> INCR foo
QUEUED
> INCR bar
QUEUED
> EXEC
1) (integer) 1
2) (integer) 1

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

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

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

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

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

Redis官網也解釋了自己為啥不支持回滾。簡單來說就是Redis開發者們覺得沒必要支持回滾,這樣更簡單便捷並且性能更好。Redis開發者覺得即使命令執行錯誤也應該在開發過程中就被發現而不是生產過程中。
redis-rollBack.png
你可以將Redis中的事務就理解為 :Redis事務提供了一種將多個命令請求打包的功能。然后,再按順序執行打包的所有命令,並且不會被中途打斷。

相關issue :issue452: 關於 Redis 事務不滿足原子性的問題 ,
推薦閱讀:https://zhuanlan.zhihu.com/p/43897838 。

16. 緩存穿透

16.1. 什么是緩存穿透?

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

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

如下圖所示,用戶的請求最終都要跑到數據庫中查詢一遍。
緩存穿透情況.png

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;
}

2)布隆過濾器

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

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

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

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

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

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

  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. 參考

作者:Snailclimb
鏈接:Redis 常見問題總結
來源:github


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM