這次徹底讀透 Redis
1. Redis 管道
我們通常使用 Redis 的方式是,發送命令,命令排隊,Redis 執行,然後返回結果,這個過程稱為Round trip time(簡稱RTT, 往返時間)。但是如果有多條命令需要執行時,需要消耗 N 次 RTT,經過 N 次 IO 傳輸,這樣效率明顯很低。
於是 Redis 管道(pipeline)便產生了,一次請求/響應服務器能實現處理新的請求即使舊的請求還未被響應。這樣就可以將多個命令發送到服務器,而不用等待回復,最後在一個步驟中讀取該答覆。這就是管道(pipelining),減少了 RTT,提升了效率
重要說明: 使用管道發送命令時,服務器將被迫回復一個隊列答覆,佔用很多內存。所以,如果你需要發送大量的命令,最好是把他們按照合理數量分批次的處理,例如10K的命令,讀回復,然後再發送另一個10k的命令,等等。這樣速度幾乎是相同的,但是在回復這10k命令隊列需要非常大量的內存用來組織返回數據內容。
2. Redis 發佈訂閱
發佈訂閱是一種消息模式,發送者(sub)發送消息,訂閱者(pub)接收消息
如上圖所示,發佈訂閱基於頻道實現的,同一個頻道可以有多個訂閱者,多個發佈者。其中任意一個發佈者發佈消息到頻道中,所以訂閱者都可以收到該消息。
發佈訂閱 Redis 演示
我在服務器上啟動了 4 個 Redis 客戶端,2 個訂閱者,2 個發佈者,訂閱者訂閱的頻道為 channel01
第一步:發佈者 1 往 channel01 中發送消息 “wugongzi”
訂閱者 1 收到消息:
訂閱者 2 收到消息:
第二步:發佈者 2 往頻道中發佈消息 “hello-redis”
訂閱者 1 收到消息:
訂閱者 2 收到消息:
Redis 同時支持訂閱多個頻道:
3. Redis 過期策略
3.1 過期時間使用
Redis 可以給每個 key 都設置一個過期時間,過期時間到達後,Redis 會自動刪除這個 key。
實際生產中,我們還是要求必須要為每個 Redis 的 Key 設置一個過期時間,如果不設置過期時間,時間一久,Redis 內存就會滿了,有很多冷數據,依然存在。
設置過期時間的命令:EXPIRE key seconds
在使用過程中有一點需要注意,就是在每次更新 Redis 時,都需要重新設置過期時間,如果不設置,那個 key 就是永久的,下面給大家演示一下如何使用:
3.2 過期刪除策略
Redis keys過期有兩種方式:被動和主動方式。
當一些客戶端嘗試訪問過期 key 時,Redis 發現 key 已經過期便刪除掉這些 key
當然,這樣是不夠的,因為有些過期的 keys,可能永遠不會被訪問到。 無論如何,這些 keys 應該過期,所以 Redis 會定時刪除這些 key
具體就是Redis每秒10次做的事情:
- 測試隨機的20個keys進行相關過期檢測。
- 刪除所有已經過期的keys。
- 如果有多於25%的keys過期,重複步奏1
這是一個平凡的概率算法,基本上的假設是,Redis的樣本是這個密鑰控件,並且我們不斷重複過期檢測,直到過期的keys的百分百低於25%,這意味着,在任何給定的時刻,最多會清除1/4的過期keys。
4. Redis 事務
4.1 事務基本使用
Redis 事務可以一次執行多條命令,Redis 事務有如下特點:
- 事務是一個單獨的隔離操作:事務中的所有命令都會序列化、按順序地執行。事務在執行的過程中,不會被其他客戶端發送來的命令請求所打斷。
- 事務是一個原子操作:事務中的命令要麼全部被執行,要麼全部都不執行。
Redis 事務通過 MULTI
、EXEC
、DISCARD
、WATCH
幾個命令來實現,MULTI 命令用於開啟事務,EXEC 用於提交事務,DISCARD 用於放棄事務,WATCH 可以為 Redis 事務提供 check-and-set (CAS)行為。
4.2 事務發生錯誤
Reids 事務發生錯誤分為兩種情況
第一種:事務提交前發生錯誤,也就是在發送命令過程中發生錯誤,看演示
上面我故意將 incr 命令寫錯,從結果我們可以看到,這條 incr 沒有入隊,並且事務執行失敗,k1 和 k2 都沒有值
第二種:事務提交後發生錯誤,也就是在執行命令過程中發生錯誤,看演示
上面的事務命令中,我給 k1 設置了一個 d,然後執行自增命令,最後獲取 k1 的值,我們發現第二條命令執行發生了錯誤,但是整個事務依然提交成功了,從上面現象中可以得出,Redis 事務不支持回滾操作。如果支持的話,整個事務的命令都不應該被執行。
4.3 為什麼 Redis 不支持回滾
如果你有使用關係式數據庫的經驗, 那麼 「Redis 在事務失敗時不進行回滾,而是繼續執行餘下的命令」這種做法可能會讓你覺得有點奇怪。
以下是這種做法的優點:
- Redis 命令只會因為錯誤的語法而失敗(並且這些問題不能在入隊時發現),或是命令用在了錯誤類型的鍵上面:這也就是說,從實用性的角度來說,失敗的命令是由編程錯誤造成的,而這些錯誤應該在開發的過程中被發現,而不應該出現在生產環境中。
- 因為不需要對回滾進行支持,所以 Redis 的內部可以保持簡單且快速。
有種觀點認為 Redis 處理事務的做法會產生 bug , 然而需要注意的是, 在通常情況下, 回滾並不能解決編程錯誤帶來的問題。 舉個例子, 如果你本來想通過 incr 命令將鍵的值加上 1 , 卻不小心加上了 2 , 又或者對錯誤類型的鍵執行了 incr , 回滾是沒有辦法處理這些情況的。
4.4 放棄事務
當執行 discard 命令時, 事務會被放棄, 事務隊列會被清空, 並且客戶端會從事務狀態中退出
4.5 WATCH 命令使用
watch 使得 exec 命令需要有條件地執行: 事務只能在所有被監視鍵都沒有被修改的前提下執行, 如果這個前提不能滿足的話,事務就不會被執行
上面我用 watch 命令監聽了 k1 和 k2,然後開啟事務,在事務提交之前,k1 的值被修改了,watch 監聽到 k1 值被修改,所以事務沒有被提交
4.6 Redis 腳本和事務
從定義上來說, Redis 中的腳本本身就是一種事務, 所以任何在事務里可以完成的事, 在腳本裏面也能完成。 並且一般來說, 使用腳本要來得更簡單,並且速度更快。
因為腳本功能是 Redis 2.6 才引入的, 而事務功能則更早之前就存在了, 所以 Redis 才會同時存在兩種處理事務的方法。
5. Reids 持久化
5.1 為什麼需要持久化
我們知道 Redis 是內存數據庫,主打高性能,速度快。相比 Redis 而言,MySQL 的數據則是保存再硬盤中(其實也有內存版的 MySQL 數據庫,但是價格極其昂貴,一般公司不會使用),速度慢,但是穩定性好。你想想 Redis 數據保存在內存中,一旦服務器宕機了,數據豈不是全部都沒了,這將會出現很大問題。所以 Redis 為了彌補這一缺陷,提供數據持久化機制,即使服務器宕機,依然可以保證數據不丟失。
5.2 持久化簡介
Redis 提供了兩種持久化機制 RDB 和 AOF,適用於不同場景
- RDB持久化方式能夠在指定的時間間隔能對你的數據進行快照存儲
- AOF持久化方式記錄每次對服務器寫的操作,當服務器重啟的時候會重新執行這些命令來恢復原始的數據,AOF命令以redis協議追加保存每次寫的操作到文件末尾.Redis還能對AOF文件進行後台重寫,使得AOF文件的體積不至於過大
5.3 RDB
RDB 持久化是通過在指定時間間隔對數據進行快照,比如在 8 點鐘對數據進行持久化,那麼 Redis 會 fork 一個子進程將 8 點那一刻內存中的數據持久化到磁盤上。觸發 RDB 持久化有以下幾種方法
5.3.1 RDB 持久化方式
1、執行 save 命令
執行 save 命令進行持久會阻塞 Redis,備份期間 Redis 無法對外提供服務,一般不建議使用,使用場景為 Redis 服務器需要停機維護的情況下。
2、執行 bgsave 命令
bgsave 命令不會阻塞 Redis 主進程,持久化期間 Redis 依然可以正常對外提供服務
3、通過配置文件中配置的 save 規則來觸發
save 900 1:900s 內有 1 個 key 發生變化,則觸發 RDB 快照
save 300 10:300s 內有 10 個 key 發生變化,則觸發 RDB 快照
save 60 10000:60s 內有 10000 個 key 發生變化(新增、修改、刪除),則觸發 RDB 快照
save “”:該配置表示關閉 RDB 持久化
5.3.2 RDB 持久化原理
Redis 進行 RDB 時,會 fork 一個子進程來進行數據持久化,這樣不妨礙 Redis 繼續對外提供服務,提高效率。
曾經面試官出過這樣面試題:
假如 Redis 在 8 點觸發了 RDB 持久化,持久化用時 2 分鐘,在持久化期間,Redis 中有 100 個 key 被修改了,那麼 RDB 文件中的 key 是 8 點那一刻的數據,還是變化的呢?
先不要看答案,自己思考 1 分鐘,一個問題只有你自己思考了,才能印象深刻。
好,下面我們一起來看下這張圖:
從圖中我們可以清晰的看到,Redis 備份時,fork 了一個子進程,子進程去做持久化的工作,子進程中的 key 指向了 8 點那一刻的數據,後面 k1 的值修改了,redis 會在內存中創建一個新的值,然後主進程 k1 指針指向新的值,子進程 k1 指針依然指向 19,這樣 Redis 持久化的就是 8 點那一刻的數據,不會發生變化。同時,從圖中我們也可以看到,Redis 持久化時並不是將內存中數據全部拷貝一份進行備份。
5.3.2 RDB 優缺點
優點
- RDB是一個非常緊湊的文件,它保存了某個時間點得數據集,非常適用於數據集的備份,比如你可以在每個小時報保存一下過去24小時內的數據,同時每天保存過去30天的數據,這樣即使出了問題你也可以根據需求恢復到不同版本的數據集
- RDB在保存RDB文件時父進程唯一需要做的就是fork出一個子進程,接下來的工作全部由子進程來做,父進程不需要再做其他IO操作,所以RDB持久化方式可以最大化redis的性能
- 與AOF相比,在恢復大的數據集的時候,RDB方式會更快一些
缺點
- 如果備份間隔時間較長,RDB 會丟失較多的數據。比如 8 點備份一次,8 點半服務器宕機,那麼這半小時內的數據就會丟失了
5.4 AOF
AOF 持久化是通過日誌的方式,記錄每次 Redis 的寫操作。當服務器重啟的時候會重新執行這些命令來恢復原始的數據,AOF 命令以Redis 協議追加保存每次寫的操作到文件末尾。Redis 還能對 AOF 文件進行後台重寫,使得 AOF 文件的體積不至於過大
5.4.1 AOF 持久化配置
# 是否開啟 aof no:關閉;yes: 開啟
appendonly no
# aof 文件名
appendfilename "appendonly.aof"
# aof 同步策略
# appendfsync always # 每個命令都寫入磁盤,性能較差
appendfsync everysec # 每秒寫一次磁盤,Redis 默認配置
# appendfsync no # 由操作系統執行,默認Linux配置最多丟失30秒
# aof 重寫期間是否同步
no-appendfsync-on-rewrite no
# 重寫觸發策略
auto-aof-rewrite-percentage 100 # 觸發重寫百分比 (指定百分比為0,將禁用aof自動重寫功能)
auto-aof-rewrite-min-size 64mb # 觸發自動重寫的最低文件體積(小於64mb不自動重寫)
# 加載aof時如果有錯如何處理
# 如果該配置啟用,在加載時發現aof尾部不正確是,會向客戶端寫入一個log,但是會繼續執行,如果設置為 no ,發現錯誤就會停止,必須修復後才能重新加載。
aof-load-truncated yes
# aof 中是否使用 rdb
# 開啟該選項,觸發AOF重寫將不再是根據當前內容生成寫命令。而是先生成RDB文件寫到開頭,再將RDB生成期間的發生的增量寫命令附加到文件末尾。
aof-use-rdb-preamble yes
5.4.2 AOF 文件寫入
aof 文件是命令追加的方式,先將命令寫入緩衝區,時間到了再寫如磁盤中
appendfsync always # 每個命令都寫入磁盤,性能較差
appendfsync everysec # 每秒寫一次磁盤,Redis 默認配置
appendfsync no # 由操作系統執行,默認Linux配置最多丟失30秒
上面配置就是何時寫入磁盤中
5.4.3 AOF 重寫
aof 文件雖然丟失的數據少,但是隨着時間的增加,aof 文件體積越來越大,佔用磁盤空間越來越大,恢復時間長。所以 redis 會對 aof 文件進行重寫,以減少 aof 文件體積
下面以一個例子說明
-- 重寫前的 aof
set k1 20
set k2 40
set k1 35
set k3 34
set k2 19
-- 這裡 k1 最終的值為 35,k2 最終值為 19,所以不需要寫入兩個命令
-- 重寫後
set k1 35
set k3 34
set k2 19
混合持久化
從 Redis 4.0 版本開始,引入了混合持久化機制,純AOF方式、RDB+AOF方式,這一策略由配置參數aof-use-rdb-preamble
(使用RDB作為AOF文件的前半段)控制,默認關閉(no),設置為yes可開啟
- no:按照AOF格式寫入命令,與4.0前版本無差別;
- yes:先按照RDB格式寫入數據狀態,然後把重寫期間AOF緩衝區的內容以AOF格式寫入,文件前半部分為RDB格式,後半部分為AOF格式。
混合持久化優點如下:
- 大大減少了 aof 文件體積
- 加快了 aof 文件恢復速度,前面是 rdb ,恢復速度快
AOF 數據恢復
第一種:純 AOF
恢復時,取出 AOF 中命令,一條條執行恢復
第二種:RDB+AOF
先執行 RDB 加載流程,執行完畢後,再取出餘下命令,開始一條條執行
5.4.4 AOF 優缺點
優點
- AOF 實時性更好,丟失數據更少
- AOF 已經支持混合持久化,文件大小可以有效控制,並提高了數據加載時的效率
缺點
- 對於相同的數據集合,AOF 文件通常會比 RDB 文件大
- 在特定的 fsync 策略下,AOF 會比 RDB 略慢
- AOF 恢復速度比 RDB 慢
6. Redis 分佈式鎖
6.1 分佈式鎖介紹
學習過 Java 的同學,應該對鎖都不陌生。Java 中多個線程訪問共享資源時,會出現並發問題,我們通常利用 synchronized 或者 Lock 鎖來解決多線程並發訪問從而出現的安全問題。細心的同學可能已經發現了, synchronized 或者 Lock 鎖解決線程安全問題在單節點情況下是可行的,但是如果服務部署在多台服務器上,本地鎖就失效了。
分佈式場景下,需要採用新的解決方案,就是今天要說的 Redis 分佈式鎖。日常業務中,類似搶紅包,秒殺等場景都可以使用 Redis 分佈式鎖來解決並發問題。
6.2 分佈式鎖特點
分佈式在保障安全、高可用的情況下需要具備以下特性
- 互斥性:任意一個時刻,只能有一個客戶端獲取到鎖
- 安全性:鎖只能被持久的客戶端刪除,不能被其他人刪除
- 高可用,高性能:加鎖和解鎖消耗的性能少,時間短
- 鎖超時:當客戶端獲取鎖後出現故障,沒有立即釋放鎖,該鎖要能夠在一定時間內釋放,否則其他客戶端無法獲取到鎖
- 可重入性:客戶端獲取到鎖後,在持久鎖期間可以再次獲取到該鎖
6.3 解決方案
6.3.1 方案一:SETNX 命令
Redis 提供了一個獲取分佈式鎖的命令 SETNX
setnx key value
如果獲取鎖成功,redis 返回 1,獲取鎖失敗 redis 返回 0
客戶端使用偽代碼
if (setnx(k1,v1) == 1) {
try{
// 執行邏輯
....
}catch() {
}finally{
// 執行完成後釋放鎖
del k1;
}
}
這個命令看似可以達到我們的目的,但是不符合分佈式鎖的特性,如果客戶端在執行業務邏輯過程中,服務器宕機了,finally 中代碼還沒來得及執行,鎖沒有釋放,也就意味其他客戶端永遠無法獲取到這個鎖
6.3.2 方案二:SETNX + EXPIRE
該方案獲取鎖之後,立即給鎖加上一個過期時間,這樣即使客戶端沒有手動釋放鎖,鎖到期後也會自動釋放
我們來看下偽代碼
if (setnx(k1, v1) == 1){
expire(key, 10);
try {
//.... 你的業務邏輯
} finally {
del(key);
}
}
這個方案很完美,既可以獲取到,又不用擔心客戶端宕機。等等,這裏面真的沒有問題嗎?再仔細瞅瞅,一瞅就瞅出問題來了
if (setnx(k1, v1) == 1){
// 再剛獲取鎖之後,想要給鎖設置過期時間,此時服務器掛了
expire(key, 10); // 這條命令沒有執行
try {
//.... 你的業務邏輯
} finally {
del(key);
}
}
這裡的 setnx 命令和 expire 命令不是原子性的,他們之間執行需要一定的等等時間,雖然這個時間很短,但是依然有極小概率出現問題
6.3.3 方案三:使用 Lua 腳本
既然 setnx 和 expire 兩個命令非原子性,那麼我們讓其符合原子性即可,通過 Lua 腳本即可實現。Redis 使用單個 Lua 解釋器去運行所有腳本,並且, Redis 也保證腳本會以原子性(atomic)的方式執行: 當某個腳本正在運行的時候,不會有其他腳本或 Redis 命令被執行
具體實現如下:
if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then
redis.call('expire',KEYS[1],ARGV[2])
else
return 0
end;
這樣應該沒問題了吧,看似上面的幾個問題都很好解決了。不對,再想想,肯定還有沒考慮到的
我們再來看一段偽代碼
// 執行 lua 腳本
// 獲取 k1 鎖,過期時間 10 s
if (execlua()==1){
try {
buyGoods();
} finally {
del(key);
}
}
從圖中我們可以很清晰發現問題所在
- 客戶端 A 還未執行完畢,客戶端 B 就獲取到了鎖,這樣就可能導致並發問題
- 客戶端 A 執行完畢,開始刪除鎖。但此時的鎖為 B 所有,相當於刪除了屬於客戶端 B 的鎖,這樣肯定會發生問題
6.3.4 方案四:SET EX PX NX + 校驗唯一隨機值,再刪除
既然鎖有可能被別的客戶端刪除,那麼在刪除鎖的時候我們加上一層校驗,判斷釋放鎖是當前客戶端持有的,如果是當前客戶端,則允許刪除,否則不允許刪除。
EX second
:設置鍵的過期時間為second
秒。SET key value EX second
效果等同於SETEX key second value
。PX millisecond
:設置鍵的過期時間為millisecond
毫秒。SET key value PX millisecond
效果等同於PSETEX key millisecond value
。NX
:只在鍵不存在時,才對鍵進行設置操作。SET key value NX
效果等同於SETNX key value
。XX
:只在鍵已經存在時,才對鍵進行設置操作。
使用示例:
if(jedis.set(resource_name, random_value, "NX", "EX", 100s) == 1){ //加鎖, value 傳入一個隨機數
try {
do something //業務處理
}catch(){
}
finally {
// 判斷 value 是否相等, 相等才釋放鎖, 這裡判斷和刪除是非原子性, 真實場景下可以將這兩步放入 Lua 腳本中執行
if (random_value.equals(jedis.get(resource_name))) {
jedis.del(lockKey); //釋放鎖
}
}
}
Lua 腳本如下:
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
此方案解決了鎖被其他客戶端解除的問題,但是依然沒有解決鎖過期釋放,但是業務還沒有執行完成的問題
6.3.5 方案五:Redisson框架
方案四中並沒有解決方法未執行完成,鎖就超時釋放的問題。這裡有個方案大家比較容易想到,那就是鎖的超時時間設置長一點,比如2min,一個接口執行時間總不能比 2 min 還長,那你就等着領盒飯吧,哈哈哈。但是這麼做,一來是不能每個鎖都設置這麼久超時時間,二來是如果接口出現異常了,鎖只能 2 min 後才能釋放,其他客戶端等待時間較長。
這個問題早就有人想到了,並給出了解決方案,開源框架 Redisson 解決了這個問題。
Redisson 在方法執行期間,會不斷的檢測鎖是否到期,如果發現鎖快要到期,但是方法還沒有執行完成,便會延長鎖的過期時間,從而解決了鎖超時釋放問題。
6.3.6 方案六:Redlock
上面所介紹的分佈式鎖,都是在單台 Redis 服務器下的解決方案。真實的生產環境中,我們通常會部署多台 Redis 服務器,也就是集群模式,這種情況上述解決方案就失效了。
對於集群 Redis,Redis 的作者 antirez 提出了另一種解決方案,Redlock 算法
Redlock 算法大致流程如下:
1、獲取當前Unix時間,以毫秒為單位。
2、依次嘗試從N個實例,使用相同的key和隨機值獲取鎖。在步驟2,當向Redis設置鎖時,客戶端應該設置一個網絡連接和響應超時時間,這個超時時間應該小於鎖的失效時間。例如你的鎖自動失效時間為10秒,則超時時間應該在5-50毫秒之間。這樣可以避免服務器端Redis已經掛掉的情況下,客戶端還在死死地等待響應結果。如果服務器端沒有在規定時間內響應,客戶端應該儘快嘗試另外一個Redis實例。
3、客戶端使用當前時間減去開始獲取鎖時間(步驟1記錄的時間)就得到獲取鎖使用的時間。當且僅當從大多數(這裡是3個節點)的Redis節點都取到鎖,並且使用的時間小於鎖失效時間時,鎖才算獲取成功。
4、如果取到了鎖,key的真正有效時間等於有效時間減去獲取鎖所使用的時間(步驟3計算的結果)。
5、如果因為某些原因,獲取鎖失敗(沒有在至少N/2+1個Redis實例取到鎖或者取鎖時間已經超過了有效時間),客戶端應該在所有的Redis實例上進行解鎖(即便某些Redis實例根本就沒有加鎖成功)。
總結: 簡單總結一下就是客戶端向 Redis 集群中所有服務器發送獲取鎖的請求,只有半數以上的鎖獲取成功後,才代表鎖獲取成功,否則鎖獲取失敗
7. Redis 集群
7.1 Redis 集群的三種模式
在生產環境中,我們使用 Redis 通常採用集群模式,因為單機版 Redis 穩定性可靠性較低,而且存儲空間有限。
Redis 支持三種集群模式
- 主從複製
- 哨兵模式
- Cluster 模式
7.2 主從複製
7.2.1 主從複製概念
主從複製模式,有一個主,多個從,從而實現讀寫分離。主機負責寫請求,從機負責讀請求,減輕主機壓力
7.2.2 主從複製原理
- 從數據庫啟動成功後,連接主數據庫,發送 SYNC 命令;
- 主數據庫接收到 SYNC 命令後,開始執行 BGSAVE 命令生成 RDB 文件並使用緩衝區記錄此後執行的所有寫命令;
- 主數據庫 BGSAVE 執行完後,向所有從數據庫發送快照文件,並在發送期間繼續記錄被執行的寫命令;
- 從數據庫收到快照文件後丟棄所有舊數據,載入收到的快照;
- 主數據庫快照發送完畢後開始向從數據庫發送緩衝區中的寫命令;
- 從數據庫完成對快照的載入,開始接收命令請求,並執行來自主數據庫緩衝區的寫命令;(從數據庫初始化完成)
- 主數據庫每執行一個寫命令就會向從數據庫發送相同的寫命令,從數據庫接收並執行收到的寫命令(從數據庫初始化完成後的操作)
- 出現斷開重連後,2.8之後的版本會將斷線期間的命令傳給重數據庫,增量複製。
- 主從剛剛連接的時候,進行全量同步;全同步結束後,進行增量同步。當然,如果有需要,slave 在任何時候都可以發起全量同步。Redis 的策略是,無論如何,首先會嘗試進行增量同步,如不成功,要求從機進行全量同步。
7.2.3 主從複製優缺點
優點
- 支持主從複製,主機會自動將數據同步到從機,可以進行讀寫分離
- Slave 同樣可以接受其它 Slaves 的連接和同步請求,這樣可以有效的分載 Master 的同步壓力
- Master Server 是以非阻塞的方式為 Slaves 提供服務。所以在 Master-Slave 同步期間,客戶端仍然可以提交查詢或修改請求
缺點
- 主從不具備容錯和恢復能力,一旦主機掛了,那麼整個集群處理可讀狀態,無法處理寫請求,會丟失數據
- 主機宕機後無法自動恢復,只能人工手動恢復
- 集群存儲容量有限,容量上線就是主庫的內存的大小,無法存儲更多內容
7.3 哨兵集群
7.3.1 哨兵概念
哨兵,我們經常在電視劇中看到一些放哨的,哨兵的作用和這些放哨的差不多,起到監控作用。一旦 Redis 集群出現問題了,哨兵會立即做出相應動作,應對異常情況。
哨兵模式是基於主從複製模式上搭建的,因為主從複製模式情況下主服務器宕機,會導致整個集群不可用,需要人工干預,所以哨兵模式在主從複製模式下引入了哨兵來監控整個集群,哨兵模式架構圖如下:
7.3.2 哨兵功能
監控(Monitoring):哨兵會不斷地檢查主節點和從節點是否運作正常。
自動故障轉移(Automatic failover):當主節點不能正常工作時,哨兵會開始自動故障轉移操作,它會將失效主節點的其中一個從節點升級為新的主節點,並讓其他從節點改為複製新的主節點。
配置提供者(Configuration provider):客戶端在初始化時,通過連接哨兵來獲得當前Redis服務的主節點地址。
通知(Notification):哨兵可以將故障轉移的結果發送給客戶端。
7.3.3 下線判斷
Redis 下線分為主觀下線和客觀下線兩種
- 主觀下線:單台哨兵任務主庫處於不可用狀態
- 客觀下線:整個哨兵集群半數以上的哨兵都認為主庫處於可不用狀態
哨兵集群中任意一台服務器判斷主庫不可用時,此時會發送命令給哨兵集群中的其他服務器確認,其他服務器收到命令後會確認主庫的狀態,如果不可用,返回 YES,可用則返回 NO,當有半數的服務器都返回 YES,說明主庫真的不可用,此時需要重新選舉
7.3.4 主庫選舉
當哨兵集群判定主庫下線了,此時需要重新選舉出一個新的主庫對外提供服務。那麼該由哪個哨兵來完成這個新庫選舉和切換的動作呢?
注意:這裡不能讓每個哨兵都去選舉,可能會出現每個哨兵選舉出的新主庫都不同,這樣就沒法判定,所以需要派出一個代表
哨兵代表選擇
哨兵的選舉機制其實很簡單,就是一個Raft選舉算法: 選舉的票數大於等於num(sentinels)/2+1時,將成為領導者,如果沒有超過,繼續選舉
- 任何一個想成為 Leader 的哨兵,要滿足兩個條件:
- 第一,拿到半數以上的贊成票;
- 第二,拿到的票數同時還需要大於等於哨兵配置文件中的 quorum 值。
以 3 個哨兵為例,假設此時的 quorum 設置為 2,那麼,任何一個想成為 Leader 的哨兵只要拿到 2 張贊成票,就可以了。
新庫選擇
上面已經選舉出了哨兵代表,此時代表需要完成新主庫的選擇,新庫的選擇需要滿足以下幾個標準
- 新庫需要處於健康狀態,也就是和哨兵之間保持正常的網絡連接
- 選擇
salve-priority
從節點優先級最高(redis.conf)的 - 選擇複製偏移量最大,只複製最完整的從節點
7.3.5 故障轉移
上面一小節哨兵已經選舉出了新的主庫,故障轉移要實現新老主庫之間的切換
故障轉移流程如下:
7.3.6 哨兵模式優缺點
優點
- 實現了集群的監控,故障轉移,實現了高可用
- 擁有主從複製模式的所有優點
缺點
- 集群存儲容量有限,容量上線就是主庫的內存的大小,無法存儲更多內容
7.4 Cluser 集群
Redis 的哨兵模式實現了高可用了,但是每台 Redis 服務器上存儲的都是相同的數據,浪費內存,而且很難實現容量上的擴展。所以在 redis3.0上加入了 Cluster 集群模式,實現了 Redis 的分佈式存儲,也就是說每台 Redis 節點上存儲不同的內容。
Redis 集群的數據分片
Redis 集群沒有使用一致性hash, 而是引入了 哈希槽的概念.
Redis 集群有16384個哈希槽,每個key通過CRC16校驗後對16384取模來決定放置哪個槽.集群的每個節點負責一部分hash槽,舉個例子,比如當前集群有3個節點,那麼:
- 節點 A 包含 0 到 5500號哈希槽.
- 節點 B 包含5501 到 11000 號哈希槽.
- 節點 C 包含11001 到 16384號哈希槽.
這種結構很容易添加或者刪除節點. 比如如果我想新添加個節點D, 我需要從節點 A, B, C中得部分槽到D上. 如果我想移除節點A,需要將A中的槽移到B和C節點上,然後將沒有任何槽的A節點從集群中移除即可. 由於從一個節點將哈希槽移動到另一個節點並不會停止服務,所以無論添加刪除或者改變某個節點的哈希槽的數量都不會造成集群不可用的狀態.
8. Redis 集群實戰
環境:
- Vmware 虛擬機
- CentOS 7
- Redis 6.0.6
因為我是在本機上演示的,所以用的虛擬機
8.1 主從複製
集群信息如下:
節點 | 配置文件 | 端口 |
---|---|---|
master | redis6379.conf | 6379 |
slave1 | redis6380.conf | 6380 |
slave1 | redis6381.conf | 6380 |
第一步:準備三個 redis.conf 配置文件,配置文件信息如下
# redis6379.conf master
# 包含命令,有點復用的意思
include /soft/redis6.0.6/bin/redis.conf
pidfile redis_6379.pid
port 6379
dbfilename dump6379.rdb
logfile "redis-6379.log"
# redis6380.conf slave1
include /soft/redis6.0.6/bin/redis.conf
pidfile redis_6380.pid
port 6380
dbfilename dump6380.rdb
logfile "redis-6380.log"
# 最後一行設置了主節點的 ip 端口
replicaof 127.0.0.1 6379
# redis6381.conf slave2
include /soft/redis6.0.6/bin/redis.conf
pidfile redis_6381.pid
port 6381
dbfilename dump6381.rdb
logfile "redis-6381.log"
# 最後一行設置了主節點的 ip 端口
replicaof 127.0.0.1 6379
## 注意 redis.conf 要調整一項,設置後台運行,對咱們操作比較友好
daemonize yes
第二步:啟動服務器
-- 首先啟動 6379 這台服務器,因為他是主庫(啟動命令在 redis 安裝目錄的 bin 目錄下)
../bin/redis-server redis6379.conf
-- 接口啟動 6380 和 6381
../bin/redis-server redis6380.conf
../bin/redis-server redis6381.conf
第三步:用客戶端連接服務器
cd bin
redis-cli -p 6379
redis-cli -p 6380
redis-cli -p 6381
這裡我開了三個窗口分別連接三台 redis 服務器,方便查看
在 6379 客戶端輸入命令: info replication 可用查看集群信息
第四步:數據同步
現在集群已經搭建好了,我們在 6379 服務器寫入幾條數據,看下可不可以同步到 6380 和 6381
6379:
6380:
6381:
從圖中可用看出,數據已經成功同步了
8.2 哨兵模式
哨兵集群是在主從複製的基礎上構建的,相當於是主從+哨兵
搭建哨兵模式分為兩步:
- 搭建主從複製集群
- 添加哨兵配置
哨兵模式節點信息如下,一主二從,三個哨兵組成一個哨兵集群
節點 | 配置 | 端口 |
---|---|---|
master | redis6379.conf | 6379 |
slave1 | redis6380.conf | 6380 |
slave2 | redis6381.conf | 6381 |
sentinel1 | sentinel1.conf | 26379 |
sentinel2 | sentinel2.conf | 26380 |
sentinel3 | sentinel3.conf | 26381 |
主從複製集群的配置同上,這裡就不再贅述,下面主要介紹下哨兵的配置,哨兵的配置文件其實非常簡單
# 文件內容
# sentinel1.conf
port 26379
sentinel monitor mymaster 127.0.0.1 6379 1
# sentinel2.conf
port 26380
sentinel monitor mymaster 127.0.0.1 6379 1
# sentinel3.conf
port 26381
sentinel monitor mymaster 127.0.0.1 6379 1
配置文件創建好了以後就可以啟動了,首先啟動主從服務器,然後啟動哨兵
../bin/redis-server redis6379.conf
../bin/redis-server redis6380.conf
../bin/redis-server redis6381.conf
-- 啟動哨兵
../bin/redis-sentinel sentinel1.conf
../bin/redis-sentinel sentinel2.conf
../bin/redis-sentinel sentinel3.conf
從哨兵的啟動日誌中我們可用看到主從服務器的信息,以及其他哨兵節點的信息
故障轉移
主從同步功能上面已經演示過了,這裡主要測試一下哨兵的故障轉移
現在我手動將主節點停掉,在 6379 上執行 shutdown 命令
此時我們觀察一下哨兵的頁面:
哨兵檢測到了 6379 下線,然後選舉出了新的主庫 6380
此時我們通過 info replication 命令查看集群信息,發現 6380 已經是主庫了,他有一個從節點 6381
現在我手動將 6379 啟動,看下 6379 會不會重新變成主庫
重新啟動後,我們發現 6379 變成了 80 的從庫
8.3 Cluser 集群
官方推薦,Cluser 集群至少要部署 3 台以上的 master 節點,最好使用 3 主 3 從
節點 | 配置 | 端口 |
---|---|---|
cluster-master1 | redis7001.conf | 7001 |
cluster-master2 | redis7002.conf | 7002 |
cluster-master3 | redis7003.conf | 7003 |
cluster-slave1 | redis7004.conf | 7004 |
cluster-slave2 | redis7006.conf | 7005 |
cluster-slave3 | redis7006.conf | 7006 |
配置文件內容如下,6 個配置文件信息基本相同,編輯好一份後其他文件直接複製修改端口即可
# 端口
port 7001
# 啟用集群模式
cluster-enabled yes
# 根據你啟用的節點來命名,最好和端口保持一致,這個是用來保存其他節點的名稱,狀態等信息的
cluster-config-file nodes_7001.conf
# 超時時間
cluster-node-timeout 5000
appendonly yes
# 後台運行
daemonize yes
# 非保護模式
protected-mode no
pidfile redis_7001.pid
然後分別啟動 6 個節點
../bin/redis-server redis7001.conf
../bin/redis-server redis7002.conf
../bin/redis-server redis7003.conf
../bin/redis-server redis7004.conf
../bin/redis-server redis7005.conf
../bin/redis-server redis7006.conf
啟動集群
# 執行命令
# --cluster-replicas 1 命令的意思是創建master的時候同時創建一個slave
$ redis-cli --cluster create 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 127.0.0.1:7006 --cluster-replicas 1
啟動過程有個地方需要輸入 yes 確認:
啟動成功後可用看到控制台輸出結果:
3 個 master 節點,3 個 slave 節點,
master[0]槽位:0-5460
master[1]槽位:5461-10922
master[2]槽位:10923-16383
數據驗證
連接 7001 服務器
redis-cli -p 7001 -c 集群模式下需要加上 -c 參數
從圖中可用看出,k1 被放到 7003 主機上了,我們此時獲取 k1 ,可用正常獲取到
登錄 7003 也可以正常拿到數據
9. Redis 緩存問題
在服務端中,數據庫通常是業務上的瓶頸,為了提高並發量和響應速度,我們通常會採用 Redis 來作為緩存,讓盡量多的數據走 Redis 查詢,不直接訪問數據庫。同時 Redis 在使用過程中也會出現各種各樣的問題,面對這些問題我們該如何處理?
- 緩存穿透
- 緩存擊穿
- 緩存雪崩
- 緩存污染
9.1 緩存穿透
1、定義:
緩存穿透是指,當緩存和數據中都沒有對應記錄,但是客戶端卻一直在查詢。比如黑客攻擊系統,不斷的去查詢系統中不存在的用戶,查詢時先走緩存,緩存中沒有,再去查數據庫;或者電商系統中,用戶搜索某類商品,但是這類商品再系統中根本不存在,這次的搜索應該直接返回空
2、解決方案
- 網關層增加校驗,進行用戶鑒權,黑名單控制,接口流量控制
- 對於同一類查詢,如果緩存和數據庫都沒有獲取到數據,那麼可用用一個空緩存記錄下來,過期時間 60s,下次遇到同類查詢,直接取出緩存中的空數據返回即可
- 使用布隆過濾器,布隆過濾器可以用來判斷某個元素是否存在於集合中,利用布隆過濾器可以過濾掉一大部分無效請求
9.2 緩存擊穿
1、定義:
緩存擊穿是指,緩存中數據失效,在高並發情況下,所有用戶的請求全部都打到數據庫上,短時間造成數據庫壓力過大
2、解決方案:
- 接口限流、熔斷
- 加鎖,當第一個用戶請求到時,如果緩存中沒有,其他用戶的請求先鎖住,第一個用戶查詢數據庫後立即緩存到 Redis,然後釋放鎖,這時候其他用戶就可以直接查詢緩存
9.3 緩存雪崩
1、定義
緩存雪崩是指 Redis 中大批量的 key 在同一時間,或者某一段時間內一起過期,造成多個 key 的請求全部無法命中緩存,這些請求全部到數據庫中,給數據庫帶來很大壓力。與緩存擊穿不同,擊穿是指一個 key 過期,雪崩是指很多 key 同時過期。
2、解決方案
- 緩存過期時間設置成不同時間,不要再統一時間過期
- 如果緩存數據庫是分佈式部署,將熱點數據均勻分佈在不同的緩存數據庫中。
9.4 緩存污染
1、定義
緩存污染是指,由於歷史原因,緩存中有很多 key 沒有設置過期時間,導致很多 key 其實已經沒有用了,但是一直存放在 redis 中,時間久了,redis 內存就被佔滿了
2、解決方案
- 緩存盡量設置過期時間
- 設置緩存淘汰策略為最近最少使用的原則,然後將這些數據刪除