1. 程式人生 > >讀完這篇文章,就基本搞定了Redis資料庫

讀完這篇文章,就基本搞定了Redis資料庫

另外,Redis 也經常用來做分散式鎖。Redis 提供了多種資料型別來支援不同的業務場景。

除此之外,Redis 支援事務 、持久化、LUA 指令碼、LRU 驅動事件、多種叢集方案。

本文將從以下幾個方面全面解讀 Redis:

  • 為什麼要用 Redis / 為什麼要用快取
  • 為什麼要用 Redis 而不用 map/guava 做快取
  • Redis 和 Memcached 的區別
  • Redis 常見資料結構以及使用場景分析
  • Redis 設定過期時間
  • Redis 記憶體淘汰機制
  • Redis 持久化機制(怎麼保證 Redis 掛掉之後再重啟資料可以進行恢復)
  • Redis 事務
  • 快取雪崩和快取穿透問題解決方案
  • 如何解決 Redis 的併發競爭 Key 問題
  • 如何保證快取與資料庫雙寫時的資料一致性

為什麼要用 Redis / 為什麼要用快取?

主要從“高效能”和“高併發”這兩點來看待這個問題。

高效能

假如使用者第一次訪問資料庫中的某些資料。這個過程會比較慢,因為是從硬碟上讀取的。

將該使用者訪問的資料存在快取中,這樣下一次再訪問這些資料的時候就可以直接從快取中獲取了。

操作快取就是直接操作記憶體,所以速度相當快。如果資料庫中的對應資料改變了之後,同步改變快取中相應的資料即可!

高併發

直接操作快取能夠承受的請求是遠遠大於直接訪問資料庫的,所以我們可以考慮把資料庫中的部分資料轉移到快取中去,這樣使用者的一部分請求會直接到快取這裡而不用經過資料庫。

為什麼要用 Redis 而不用 map/guava 做快取

快取分為本地快取和分散式快取。以 Java 為例,使用自帶的 map 或者 guava 實現的是本地快取,最主要的特點是輕量以及快速,生命週期隨著 JVM 的銷燬而結束。

並且在多例項的情況下,每個例項都需要各自儲存一份快取,快取不具有一致性。

使用 Redis 或 Memcached 之類的稱為分散式快取,在多例項的情況下,各例項共用一份快取資料,快取具有一致性。

缺點是需要保持 Redis 或 Memcached 服務的高可用,整個程式架構上較為複雜。

Redis 和 Memcached 的區別

現在公司一般都是用 Redis 來實現快取,而且 Redis 自身也越來越強大了!

對於 Redis 和 Memcached 我總結了下面四點:

  • Redis 支援更豐富的資料型別(支援更復雜的應用場景):Redis 不僅僅支援簡單的 K/V 型別的資料,同時還提供 list、set、zset、hash 等資料結構的儲存。Memcache 支援簡單的資料型別 String。
  • Redis 支援資料的持久化,可以將記憶體中的資料保持在磁碟中,重啟的時候可以再次載入進行使用,而 Memecache 把資料全部存在記憶體之中。
  • 叢集模式:Memcached 沒有原生的叢集模式,需要依靠客戶端來實現往叢集中分片寫入資料;但是 Redis 目前是原生支援 Cluster 模式的。
  • Memcached 是多執行緒,非阻塞 IO 複用的網路模型;Redis 使用單執行緒的多路 IO 複用模型。

來自網路上的一張對比圖,這裡分享給大家:

Redis 常見資料結構以及使用場景分析

String

常用命令:set、get、decr、incr、mget 等。

String 資料結構是簡單的 Key-Value 型別,Value 其實不僅可以是 String,也可以是數字。常規 Key-Value 快取應用;常規計數:微博數,粉絲數等。

Hash

常用命令: hget、hset、hgetall 等。

Hash 是一個 String 型別的 Field 和 Value 的對映表,Hash 特別適合用於儲存物件。

後續操作的時候,你可以直接僅僅修改這個物件中的某個欄位的值。比如我們可以 Hash 資料結構來儲存使用者資訊,商品資訊等等。

比如下面我就用 Hash 型別存放了我本人的一些資訊:

  1. key=JavaUser293847 
  2. value={ 
  3.   “id”: 1, 
  4.   “name”: “SnailClimb”, 
  5.   “age”: 22, 
  6.   “location”: “Wuhan, Hubei” 

List

常用命令:lpush、rpush、lpop、rpop、lrange 等。

List 就是連結串列,Redis List 的應用場景非常多,也是 Redis 最重要的資料結構之一。

比如微博的關注列表,粉絲列表,訊息列表等功能都可以用 Redis 的 List 結構來實現。

Redis List 的實現為一個雙向連結串列,即可以支援反向查詢和遍歷,更方便操作,不過帶來了部分額外的記憶體開銷。

另外可以通過 lrange 命令,就是從某個元素開始讀取多少個元素,可以基於 List 實現分頁查詢。

這是很棒的一個功能,基於 Redis 實現簡單的高效能分頁,可以做類似微博那種下拉不斷分頁的東西(一頁一頁的往下走),效能高。

Set

常用命令:sadd、spop、smembers、sunion 等。

Set 對外提供的功能與 List 類似是一個列表的功能,特殊之處在於 Set 是可以自動排重的。

當你需要儲存一個列表資料,又不希望出現重複資料時,Set 是一個很好的選擇。

並且 Set 提供了判斷某個成員是否在一個 Set 集合內的重要介面,這個也是 List 所不能提供的。你可以基於 Set 輕易實現交集、並集、差集的操作。

比如:在微博應用中,可以將一個使用者所有的關注人存在一個集合中,將其所有粉絲存在一個集合。Redis 可以非常方便的實現如共同關注、共同粉絲、共同喜好等功能。

這個過程也就是求交集的過程,具體命令如下:

  1. sinterstore key1 key2 key3 將交集存在key1內 

Sorted Set

常用命令:zadd、zrange、zrem、zcard 等。

和 Set 相比,Sorted Set 增加了一個權重引數 Score,使得集合中的元素能夠按 Score 進行有序排列。

舉例:在直播系統中,實時排行資訊包含直播間線上使用者列表,各種禮物排行榜,彈幕訊息(可以理解為按訊息維度的訊息排行榜)等資訊,適合使用 Redis 中的 Sorted Set 結構進行儲存。

Redis 設定過期時間

Redis 中有個設定過期時間的功能,即對儲存在 Redis 資料庫中的值可以設定一個過期時間。作為一個快取資料庫,這是非常實用的。

如我們一般專案中的 Token 或者一些登入資訊,尤其是簡訊驗證碼都是有時間限制的,按照傳統的資料庫處理方式,一般都是自己判斷過期,這樣無疑會嚴重影響專案效能。

我們 Set Key 的時候,都可以給一個 Expire Time,就是過期時間,通過過期時間我們可以指定這個 Key 可以存活的時間。

如果你設定了一批 Key 只能存活 1 個小時,那麼接下來 1 小時後,Redis 是怎麼對這批 Key 進行刪除的?

答案是:定期刪除+惰性刪除。通過名字大概就能猜出這兩個刪除方式的意思了:

  • 定期刪除:Redis 預設是每隔 100ms 就隨機抽取一些設定了過期時間的 Key,檢查其是否過期,如果過期就刪除。

注意這裡是隨機抽取的。為什麼要隨機呢?你想一想假如 Redis 存了幾十萬個 Key ,每隔 100ms 就遍歷所有的設定過期時間的 Key 的話,就會給 CPU 帶來很大的負載!

  • 惰性刪除 :定期刪除可能會導致很多過期 Key 到了時間並沒有被刪除掉。所以就有了惰性刪除。

假如你的過期 Key,靠定期刪除沒有被刪除掉,還停留在記憶體裡,除非你的系統去查一下那個 Key,才會被 Redis 給刪除掉。這就是所謂的惰性刪除,也是夠懶的哈!

但是僅僅通過設定過期時間還是有問題的。我們想一下:如果定期刪除漏掉了很多過期 Key,然後你也沒及時去查,也就沒走惰性刪除,此時會怎麼樣?

如果大量過期 Key 堆積在記憶體裡,導致 Redis 記憶體塊耗盡了。怎麼解決這個問題呢?

Redis 記憶體淘汰機制

MySQL 裡有 2000w 資料,Redis 中只存 20w 的資料,如何保證 Redis 中的資料都是熱點資料?

Redis 配置檔案 redis.conf 中有相關注釋,我這裡就不貼了,大家可以自行查閱或者通過這個網址檢視:http://download.redis.io/redis-stable/redis.conf

Redis 提供 6 種資料淘汰策略:

  • volatile-lru:從已設定過期時間的資料集(server.db[i].expires)中挑選最近最少使用的資料淘汰。
  • volatile-ttl:從已設定過期時間的資料集(server.db[i].expires)中挑選將要過期的資料淘汰。
  • volatile-random:從已設定過期時間的資料集(server.db[i].expires)中任意選擇資料淘汰。
  • allkeys-lru:當記憶體不足以容納新寫入資料時,在鍵空間中,移除最近最少使用的key(這個是最常用的)。
  • allkeys-random:從資料集(server.db[i].dict)中任意選擇資料淘汰。
  • no-enviction:禁止驅逐資料,也就是說當記憶體不足以容納新寫入資料時,新寫入操作會報錯。這個應該沒人使用吧!

Redis 持久化機制

怎麼保證 Redis 掛掉之後再重啟資料可以進行恢復?很多時候我們需要持久化資料也就是將記憶體中的資料寫入到硬盤裡面。

大部分原因是為了之後重用資料(比如重啟機器、機器故障之後恢復資料),或者是為了防止系統故障而將資料備份到一個遠端位置。

Redis 不同於 Memcached 的很重要一點就是,Redis 支援持久化,而且支援兩種不同的持久化操作。

Redis 的一種持久化方式叫快照(snapshotting,RDB),另一種方式是隻追加檔案(append-only file,AOF)。

這兩種方法各有千秋,下面我會詳細講這兩種持久化方法是什麼,怎麼用,如何選擇適合自己的持久化方法。

快照(snapshotting)持久化(RDB)

Redis 可以通過建立快照來獲得儲存在記憶體裡面的資料在某個時間點上的副本。

Redis 建立快照之後,可以對快照進行備份,可以將快照複製到其他伺服器從而建立具有相同資料的伺服器副本(Redis 主從結構,主要用來提高 Redis 效能),還可以將快照留在原地以便重啟伺服器的時候使用。

快照持久化是 Redis 預設採用的持久化方式,在 redis.conf 配置檔案中預設有此下配置:

  1. save 900 1              #在900秒(15分鐘)之後,如果至少有1個key發生變化,Redis就會自動觸發BGSAVE命令建立快照。 
  2. save 300 10            #在300秒(5分鐘)之後,如果至少有10個key發生變化,Redis就會自動觸發BGSAVE命令建立快照。 
  3. save 60 10000        #在60秒(1分鐘)之後,如果至少有10000個key發生變化,Redis就會自動觸發BGSAVE命令建立快照。 

AOF(append-only file)持久化

與快照持久化相比,AOF 持久化的實時性更好,因此已成為主流的持久化方案。

預設情況下 Redis 沒有開啟 AOF(append only file)方式的持久化,可以通過 appendonly 引數開啟:

  1. appendonly yes 

開啟 AOF 持久化後每執行一條會更改 Redis 中的資料的命令,Redis 就會將該命令寫入硬碟中的 AOF 檔案。

AOF 檔案的儲存位置和 RDB 檔案的位置相同,都是通過 dir 引數設定的,預設的檔名是 appendonly.aof。

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

  1. appendfsync always     #每次有資料修改發生時都會寫入AOF檔案,這樣會嚴重降低Redis的速度 
  2. appendfsync everysec  #每秒鐘同步一次,顯示地將多個寫命令同步到硬碟 
  3. appendfsync no      #讓作業系統決定何時進行同步 

為了兼顧資料和寫入效能,使用者可以考慮 appendfsync everysec 選項 ,讓 Redis 每秒同步一次 AOF 檔案,Redis 效能幾乎沒受到任何影響。

而且這樣即使出現系統崩潰,使用者最多隻會丟失一秒之內產生的資料。當硬碟忙於執行寫入操作的時候,Redis 還會優雅的放慢自己的速度以便適應硬碟的最大寫入速度。

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 檔案重寫操作。

Redis 事務

Redis 通過 MULTI、EXEC、WATCH 等命令來實現事務(transaction)功能。

事務提供了一種將多個命令請求打包,然後一次性、按順序地執行多個命令的機制。

並且在事務執行期間,伺服器不會中斷事務而改去執行其他客戶端的命令請求,它會將事務中的所有命令都執行完畢,然後才去處理其他客戶端的命令請求。

在傳統的關係式資料庫中,常常用 ACID 性質來檢驗事務功能的可靠性和安全性。

在 Redis 中,事務總是具有原子性(Atomicity)、一致性(Consistency)和隔離性(Isolation),並且當 Redis 執行在某種特定的持久化模式下時,事務也具有永續性(Durability)。

快取雪崩和快取穿透問題解決方案

快取雪崩

簡介:快取同一時間大面積的失效,所以,後面的請求都會落到資料庫上,造成資料庫短時間內承受大量請求而崩掉。

解決辦法:

  • 事前:儘量保證整個 Redis 叢集的高可用性,發現機器宕機儘快補上。選擇合適的記憶體淘汰策略。
  • 事中:本地 Ehcache 快取 + Hystrix 限流&降級,避免 MySQL 崩掉。
  • 事後:利用 Redis 持久化機制儲存的資料儘快恢復快取。

快取穿透

簡介:一般是黑客故意去請求快取中不存在的資料,導致所有的請求都落到資料庫上,造成資料庫短時間內承受大量請求而崩掉。

解決辦法:有很多種方法可以有效地解決快取穿透問題,最常見的則是採用布隆過濾器,將所有可能存在的資料雜湊到一個足夠大的 bitmap 中。

一個一定不存在的資料會被這個 bitmap 攔截掉,從而避免了對底層儲存系統的查詢壓力。

另外也有一個更為簡單粗暴的方法(我們採用的就是這種),如果一個查詢返回的資料為空(不管是資料不存在,還是系統故障),我們仍然把這個空結果進行快取,但它的過期時間會很短,最長不超過五分鐘。

如何解決 Redis 的併發競爭 Key 問題

所謂 Redis 的併發競爭 Key 的問題也就是多個系統同時對一個 Key 進行操作,但是最後執行的順序和我們期望的順序不同,這樣也就導致了結果的不同!

推薦一種方案:分散式鎖(ZooKeeper 和 Redis 都可以實現分散式鎖)。(如果不存在 Redis 的併發競爭 Key 問題,不要使用分散式鎖,這樣會影響效能)

基於 ZooKeeper 臨時有序節點可以實現的分散式鎖。大致思想為:每個客戶端對某個方法加鎖時,在 ZooKeeper 上的與該方法對應的指定節點的目錄下,生成一個唯一的瞬時有序節點。

判斷是否獲取鎖的方式很簡單,只需要判斷有序節點中序號最小的一個。 當釋放鎖的時候,只需將這個瞬時節點刪除即可。

同時,其可以避免服務宕機導致的鎖無法釋放,而產生的死鎖問題。完成業務流程後,刪除對應的子節點釋放鎖。

在實踐中,當然是以可靠性為主。所以首推 ZooKeeper。

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

你只要用快取,就可能會涉及到快取與資料庫雙儲存雙寫,你只要是雙寫,就一定會有資料一致性的問題,那麼你如何解決一致性問題?

一般來說,就是如果你的系統不是嚴格要求快取+資料庫必須一致性的話,快取可以稍微的跟資料庫偶爾有不一致的情況。

最好不要做這個方案,讀請求和寫請求序列化,串到一個記憶體佇列裡去,這樣就可以保證一定不會出現不一致的情況。

序列化之後,就會導致系統的吞吐量會大幅度的降低,用比正常情況下多幾倍的機器去支撐線上的一個請求。

參考文章:

  • https://segmentfault.com/q/1010000009106416
  • Redis設計與實現(第二版)
  • https://www.jianshu.com/p/8bddd381de06
  • https://blog.csdn.net/zeb_perfect/article/details/54135506
  • Java工程師面試突擊第1季(可能是史上最好的Java面試突擊課程)-中華石杉老師。連結: https://pan.baidu.com/s/18pp6g1xKVGCfUATf_nMrOA,密碼:5i58