Redis 核心技术与实战
目錄
?
開篇詞 | 這樣學(xué) Redis,才能技高一籌
01 | 基本架構(gòu):一個(gè)鍵值數(shù)據(jù)庫包含什么?
02 | 數(shù)據(jù)結(jié)構(gòu):快速的Redis有哪些慢操作?
鍵和值用什么結(jié)構(gòu)組織?
為什么哈希表操作變慢了?
有哪些底層數(shù)據(jù)結(jié)構(gòu)?
不同操作的復(fù)雜度
03 | 高性能IO模型:為什么單線程Redis能那么快?
Redis 為什么用單線程?
單線程 Redis 為什么那么快?
基本 IO 模型與阻塞點(diǎn)
非阻塞模式
基于多路復(fù)用的高性能 I/O 模型
04 | AOF日志:宕機(jī)了,Redis如何避免數(shù)據(jù)丟失?
AOF 日志是如何實(shí)現(xiàn)的?
三種寫回策略
日志文件太大了怎么辦?
AOF 重寫會阻塞嗎?
05 | 內(nèi)存快照:宕機(jī)后,Redis如何實(shí)現(xiàn)快速恢復(fù)?
給哪些內(nèi)存數(shù)據(jù)做快照?
快照時(shí)數(shù)據(jù)能修改嗎?
可以每秒做一次快照嗎?
06 | 數(shù)據(jù)同步:主從庫如何實(shí)現(xiàn)數(shù)據(jù)一致?
主從庫間如何進(jìn)行第一次同步?
主從級聯(lián)模式分擔(dān)全量復(fù)制時(shí)的主庫壓力
主從庫間網(wǎng)絡(luò)斷了怎么辦?
07 | 哨兵機(jī)制:主庫掛了,如何不間斷服務(wù)?
哨兵機(jī)制的基本流程
主觀下線和客觀下線
如何選定新主庫?
08 | 哨兵集群:哨兵掛了,主從庫還能切換嗎?
基于 pub/sub 機(jī)制的哨兵集群組成
基于 pub/sub 機(jī)制的客戶端事件通知
由哪個(gè)哨兵執(zhí)行主從切換?
23 | 旁路緩存:Redis是如何工作的?
緩存的特征
Redis 緩存處理請求的兩種情況
Redis 作為旁路緩存的使用操作
緩存的類型
小結(jié)
開篇詞 | 這樣學(xué) Redis,才能技高一籌
Redis 知識全景圖
Redis 問題畫像圖
01 | 基本架構(gòu):一個(gè)鍵值數(shù)據(jù)庫包含什么?
從 SimpleKV 到 Redis
02 | 數(shù)據(jù)結(jié)構(gòu):快速的Redis有哪些慢操作?
Redis 數(shù)據(jù)類型和底層數(shù)據(jù)結(jié)構(gòu)的對應(yīng)關(guān)系
鍵和值用什么結(jié)構(gòu)組織?
全局哈希表
為什么哈希表操作變慢了?
哈希表的哈希沖突
漸進(jìn)式 rehash
有哪些底層數(shù)據(jù)結(jié)構(gòu)?
集合類型的底層數(shù)據(jù)結(jié)構(gòu)主要有 5 種:整數(shù)數(shù)組、雙向鏈表、哈希表、壓縮列表和跳表。
壓縮列表的查找
跳表的快速查找過程
數(shù)據(jù)結(jié)構(gòu)的時(shí)間復(fù)雜度
不同操作的復(fù)雜度
- 單元素操作是基礎(chǔ);
- 范圍操作非常耗時(shí);
- 統(tǒng)計(jì)操作通常高效;
- 例外情況只有幾個(gè)。
03 | 高性能IO模型:為什么單線程Redis能那么快?
為什么單線程的 Redis 能那么快?
Redis 是單線程,主要是指 Redis 的網(wǎng)絡(luò) IO 和鍵值對讀寫是由一個(gè)線程來完成的,這也是 Redis 對外提供鍵值存儲服務(wù)的主要流程。但 Redis 的其他功能,比如持久化、異步刪除、集群數(shù)據(jù)同步等,其實(shí)是由額外的線程執(zhí)行的。
Redis 為什么用單線程?
多線程的開銷
線程數(shù)與系統(tǒng)吞吐率
一個(gè)關(guān)鍵的瓶頸在于,系統(tǒng)中通常會存在被多線程同時(shí)訪問的共享資源,比如一個(gè)共享的數(shù)據(jù)結(jié)構(gòu)。當(dāng)有多個(gè)線程要修改這個(gè)共享資源時(shí),為了保證共享資源的正確性,就需要有額外的機(jī)制進(jìn)行保證,而這個(gè)額外的機(jī)制,就會帶來額外的開銷。
多線程編程模式面臨的共享資源的并發(fā)訪問控制問題。
并發(fā)訪問控制一直是多線程開發(fā)中的一個(gè)難點(diǎn)問題,如果沒有精細(xì)的設(shè)計(jì),比如說,只是簡單地采用一個(gè)粗粒度互斥鎖,就會出現(xiàn)不理想的結(jié)果:即使增加了線程,大部分線程也在等待獲取訪問共享資源的互斥鎖,并行變串行,系統(tǒng)吞吐率并沒有隨著線程的增加而增加。
而且,采用多線程開發(fā)一般會引入同步原語來保護(hù)共享資源的并發(fā)訪問,這也會降低系統(tǒng)代碼的易調(diào)試性和可維護(hù)性。為了避免這些問題,Redis 直接采用了單線程模式。
單線程 Redis 為什么那么快?
通常來說,單線程的處理能力要比多線程差很多,但是 Redis 卻能使用單線程模型達(dá)到每秒數(shù)十萬級別的處理能力,這是為什么呢?
一方面,Redis 的大部分操作在內(nèi)存上完成,再加上它采用了高效的數(shù)據(jù)結(jié)構(gòu),例如哈希表和跳表,這是它實(shí)現(xiàn)高性能的一個(gè)重要原因。另一方面,就是 Redis 采用了多路復(fù)用機(jī)制,使其在網(wǎng)絡(luò) IO 操作中能并發(fā)處理大量的客戶端請求,實(shí)現(xiàn)高吞吐率。
基本 IO 模型與阻塞點(diǎn)
Redis基本 IO 模型
非阻塞模式
Redis 套接字類型與非阻塞設(shè)置
基于多路復(fù)用的高性能 I/O 模型
Linux 中的 IO 多路復(fù)用機(jī)制是指一個(gè)線程處理多個(gè) IO 流,就是我們經(jīng)常聽到的 select/epoll 機(jī)制。簡單來說,在 Redis 只運(yùn)行單線程的情況下,該機(jī)制允許內(nèi)核中,同時(shí)存在多個(gè)監(jiān)聽套接字和已連接套接字。內(nèi)核會一直監(jiān)聽這些套接字上的連接請求或數(shù)據(jù)請求。一旦有請求到達(dá),就會交給 Redis 線程處理,這就實(shí)現(xiàn)了一個(gè) Redis 線程處理多個(gè) IO 流的效果。
為了在請求到達(dá)時(shí)能通知到 Redis 線程,select/epoll 提供了基于事件的回調(diào)機(jī)制,即針對不同事件的發(fā)生,調(diào)用相應(yīng)的處理函數(shù)。
這些事件會被放進(jìn)一個(gè)事件隊(duì)列,Redis 單線程對該事件隊(duì)列不斷進(jìn)行處理。這樣一來,Redis 無需一直輪詢是否有請求實(shí)際發(fā)生,這就可以避免造成 CPU 資源浪費(fèi)。同時(shí),Redis 在對事件隊(duì)列中的事件進(jìn)行處理時(shí),會調(diào)用相應(yīng)的處理函數(shù),這就實(shí)現(xiàn)了基于事件的回調(diào)。因?yàn)?Redis 一直在對事件隊(duì)列進(jìn)行處理,所以能及時(shí)響應(yīng)客戶端請求,提升 Redis 的響應(yīng)性能。
04 | AOF日志:宕機(jī)了,Redis如何避免數(shù)據(jù)丟失?
AOF 日志是如何實(shí)現(xiàn)的?
寫后日志
Redis AOF操作過程
AOF 里記錄的是 Redis 收到的每一條命令,這些命令是以文本形式保存的。
Redis AOF 日志內(nèi)容
但是,為了避免額外的檢查開銷,Redis 在向 AOF 里面記錄日志的時(shí)候,并不會先去對這些命令進(jìn)行語法檢查。
寫后日志這種方式,就是先讓系統(tǒng)執(zhí)行命令,只有命令能執(zhí)行成功,才會被記錄到日志中,否則,系統(tǒng)就會直接向客戶端報(bào)錯(cuò)。所以,Redis 使用寫后日志這一方式的一大好處是,可以避免出現(xiàn)記錄錯(cuò)誤命令的情況。
除此之外,AOF 還有一個(gè)好處:它是在命令執(zhí)行后才記錄日志,所以不會阻塞當(dāng)前的寫操作。
不過,AOF 也有兩個(gè)潛在的風(fēng)險(xiǎn)。
首先,如果剛執(zhí)行完一個(gè)命令,還沒有來得及記日志就宕機(jī)了,那么這個(gè)命令和相應(yīng)的數(shù)據(jù)就有丟失的風(fēng)險(xiǎn)。
其次,AOF 雖然避免了對當(dāng)前命令的阻塞,但可能會給下一個(gè)操作帶來阻塞風(fēng)險(xiǎn)。
這兩個(gè)風(fēng)險(xiǎn)都是和 AOF 寫回磁盤的時(shí)機(jī)相關(guān)的。
三種寫回策略
AOF 配置項(xiàng) appendfsync 的三個(gè)可選值。
- Always,同步寫回:每個(gè)寫命令執(zhí)行完,立馬同步地將日志寫回磁盤;
- Everysec,每秒寫回:每個(gè)寫命令執(zhí)行完,只是先把日志寫到 AOF 文件的內(nèi)存緩沖區(qū),每隔一秒把緩沖區(qū)中的內(nèi)容寫入磁盤;
- No,操作系統(tǒng)控制的寫回:每個(gè)寫命令執(zhí)行完,只是先把日志寫到 AOF 文件的內(nèi)存緩沖區(qū),由操作系統(tǒng)決定何時(shí)將緩沖區(qū)內(nèi)容寫回磁盤。
針對避免主線程阻塞和減少數(shù)據(jù)丟失問題,這三種寫回策略都無法做到兩全其美。
- “同步寫回”可以做到基本不丟數(shù)據(jù),但是它在每一個(gè)寫命令后都有一個(gè)慢速的落盤操作,不可避免地會影響主線程性能;
- 雖然“操作系統(tǒng)控制的寫回”在寫完緩沖區(qū)后,就可以繼續(xù)執(zhí)行后續(xù)的命令,但是落盤的時(shí)機(jī)已經(jīng)不在 Redis 手中了,只要 AOF 記錄沒有寫回磁盤,一旦宕機(jī)對應(yīng)的數(shù)據(jù)就丟失了;
- “每秒寫回”采用一秒寫回一次的頻率,避免了“同步寫回”的性能開銷,雖然減少了對系統(tǒng)性能的影響,但是如果發(fā)生宕機(jī),上一秒內(nèi)未落盤的命令操作仍然會丟失。所以,這只能算是,在避免影響主線程性能和避免數(shù)據(jù)丟失兩者間取了個(gè)折中。
三種策略的寫回時(shí)機(jī),以及優(yōu)缺點(diǎn)
想要獲得高性能,就選擇 No 策略;如果想要得到高可靠性保證,就選擇 Always 策略;如果允許數(shù)據(jù)有一點(diǎn)丟失,又希望性能別受太大影響的話,那么就選擇 Everysec 策略。
AOF 是以文件的形式在記錄接收到的所有寫命令。隨著接收的寫命令越來越多,AOF 文件會越來越大。
小心 AOF 文件過大帶來的性能問題。
- 一是,文件系統(tǒng)本身對文件大小有限制,無法保存過大的文件;
- 二是,如果文件太大,之后再往里面追加命令記錄的話,效率也會變低;
- 三是,如果發(fā)生宕機(jī),AOF 中記錄的命令要一個(gè)個(gè)被重新執(zhí)行,用于故障恢復(fù),如果日志文件太大,整個(gè)恢復(fù)過程就會非常緩慢,這就會影響到 Redis 的正常使用。
日志文件太大了怎么辦?
AOF 重寫機(jī)制就是在重寫時(shí),Redis 根據(jù)數(shù)據(jù)庫的現(xiàn)狀創(chuàng)建一個(gè)新的 AOF 文件,也就是說,讀取數(shù)據(jù)庫中的所有鍵值對,然后對每一個(gè)鍵值對用一條命令記錄它的寫入。
為什么重寫機(jī)制可以把日志文件變小呢?
實(shí)際上,重寫機(jī)制具有“多變一”功能。所謂的“多變一”,也就是說,舊日志文件中的多條命令,在重寫后的新日志中變成了一條命令。
AOF 文件是以追加的方式,逐一記錄接收到的寫命令的。當(dāng)一個(gè)鍵值對被多條寫命令反復(fù)修改時(shí),AOF 文件會記錄相應(yīng)的多條命令。但是,在重寫的時(shí)候,是根據(jù)這個(gè)鍵值對當(dāng)前的最新狀態(tài),為它生成對應(yīng)的寫入命令。
AOF 重寫減少日志大小
AOF 重寫會阻塞嗎?
和 AOF 日志由主線程寫回不同,重寫過程是由后臺子進(jìn)程 bgrewriteaof 來完成的,這也是為了避免阻塞主線程,導(dǎo)致數(shù)據(jù)庫性能下降。
重寫的過程:“一個(gè)拷貝,兩處日志”。
“一個(gè)拷貝”就是指,每次執(zhí)行重寫時(shí),主線程 fork 出后臺的 bgrewriteaof 子進(jìn)程。此時(shí),fork 會把主線程的內(nèi)存拷貝一份給 bgrewriteaof 子進(jìn)程,這里面就包含了數(shù)據(jù)庫的最新數(shù)據(jù)。然后,bgrewriteaof 子進(jìn)程就可以在不影響主線程的情況下,逐一把拷貝的數(shù)據(jù)寫成操作,記入重寫日志。
“兩處日志”又是什么呢?因?yàn)橹骶€程未阻塞,仍然可以處理新來的操作。此時(shí),如果有寫操作,第一處日志就是指正在使用的 AOF 日志,Redis 會把這個(gè)操作寫到它的緩沖區(qū)。這樣一來,即使宕機(jī)了,這個(gè) AOF 日志的操作仍然是齊全的,可以用于恢復(fù)。而第二處日志,就是指新的 AOF 重寫日志。這個(gè)操作也會被寫到重寫日志的緩沖區(qū)。這樣,重寫日志也不會丟失最新的操作。等到拷貝數(shù)據(jù)的所有操作記錄重寫完成后,重寫日志記錄的這些最新操作也會寫入新的 AOF 文件,以保證數(shù)據(jù)庫最新狀態(tài)的記錄。此時(shí),我們就可以用新的 AOF 文件替代舊文件了。
AOF 非阻塞的重寫過程
05 | 內(nèi)存快照:宕機(jī)后,Redis如何實(shí)現(xiàn)快速恢復(fù)?
另一種持久化方法:內(nèi)存快照。所謂內(nèi)存快照,就是指內(nèi)存中的數(shù)據(jù)在某一個(gè)時(shí)刻的狀態(tài)記錄。
和 AOF 相比,RDB 記錄的是某一時(shí)刻的數(shù)據(jù),并不是操作,所以,在做數(shù)據(jù)恢復(fù)時(shí),我們可以直接把 RDB 文件讀入內(nèi)存,很快地完成恢復(fù)。
考慮兩個(gè)關(guān)鍵問題:
- 對哪些數(shù)據(jù)做快照?這關(guān)系到快照的執(zhí)行效率問題;
- 做快照時(shí),數(shù)據(jù)還能被增刪改嗎?這關(guān)系到 Redis 是否被阻塞,能否同時(shí)正常處理請求。
給哪些內(nèi)存數(shù)據(jù)做快照?
Redis 的數(shù)據(jù)都在內(nèi)存中,為了提供所有數(shù)據(jù)的可靠性保證,它執(zhí)行的是全量快照,也就是說,把內(nèi)存中的所有數(shù)據(jù)都記錄到磁盤中,這就類似于給 100 個(gè)人拍合影,把每一個(gè)人都拍進(jìn)照片里。這樣做的好處是,一次性記錄了所有數(shù)據(jù),一個(gè)都不少。
針對任何操作,我們都會提一個(gè)靈魂之問:“它會阻塞主線程嗎?”
RDB 文件的生成是否會阻塞主線程?
Redis 提供了兩個(gè)命令來生成 RDB 文件,分別是 save 和 bgsave。
- save:在主線程中執(zhí)行,會導(dǎo)致阻塞;
- bgsave:創(chuàng)建一個(gè)子進(jìn)程,專門用于寫入 RDB 文件,避免了主線程的阻塞,這也是 Redis RDB 文件生成的默認(rèn)配置。
可以通過 bgsave 命令來執(zhí)行全量快照,這既提供了數(shù)據(jù)的可靠性保證,也避免了對 Redis 的性能影響。
快照時(shí)數(shù)據(jù)能修改嗎?
一個(gè)常見的誤區(qū),bgsave 避免阻塞和正常處理寫操作并不是一回事。此時(shí),主線程的確沒有阻塞,可以正常接收請求,但是,為了保證快照完整性,它只能處理讀操作,因?yàn)椴荒苄薷恼趫?zhí)行快照的數(shù)據(jù)。
為了快照而暫停寫操作,肯定是不能接受的。所以這個(gè)時(shí)候,Redis 就會借助操作系統(tǒng)提供的寫時(shí)復(fù)制技術(shù)(Copy-On-Write, COW),在執(zhí)行快照的同時(shí),正常處理寫操作。
簡單來說,bgsave 子進(jìn)程是由主線程 fork 生成的,可以共享主線程的所有內(nèi)存數(shù)據(jù)。bgsave 子進(jìn)程運(yùn)行后,開始讀取主線程的內(nèi)存數(shù)據(jù),并把它們寫入 RDB 文件。
此時(shí),如果主線程對這些數(shù)據(jù)也都是讀操作(例如圖中的鍵值對 A),那么,主線程和 bgsave 子進(jìn)程相互不影響。但是,如果主線程要修改一塊數(shù)據(jù)(例如圖中的鍵值對 C),那么,這塊數(shù)據(jù)就會被復(fù)制一份,生成該數(shù)據(jù)的副本。然后,bgsave 子進(jìn)程會把這個(gè)副本數(shù)據(jù)寫入 RDB 文件,而在這個(gè)過程中,主線程仍然可以直接修改原來的數(shù)據(jù)。
寫時(shí)復(fù)制機(jī)制保證快照期間數(shù)據(jù)可修改
這既保證了快照的完整性,也允許主線程同時(shí)對數(shù)據(jù)進(jìn)行修改,避免了對正常業(yè)務(wù)的影響。
可以每秒做一次快照嗎?
快照機(jī)制下的數(shù)據(jù)丟失
雖然 bgsave 執(zhí)行時(shí)不阻塞主線程,但是,如果頻繁地執(zhí)行全量快照,也會帶來兩方面的開銷。
- 一方面,頻繁將全量數(shù)據(jù)寫入磁盤,會給磁盤帶來很大壓力,多個(gè)快照競爭有限的磁盤帶寬,前一個(gè)快照還沒有做完,后一個(gè)又開始做了,容易造成惡性循環(huán)。
- 另一方面,bgsave 子進(jìn)程需要通過 fork 操作從主線程創(chuàng)建出來。雖然,子進(jìn)程在創(chuàng)建后不會再阻塞主線程,但是,fork 這個(gè)創(chuàng)建過程本身會阻塞主線程,而且主線程的內(nèi)存越大,阻塞時(shí)間越長。如果頻繁 fork 出 bgsave 子進(jìn)程,這就會頻繁阻塞主線程了。
增量快照,就是指,做了一次全量快照后,后續(xù)的快照只對修改的數(shù)據(jù)進(jìn)行快照記錄,這樣可以避免每次全量快照的開銷。
增量快照的前提是,我們需要記住哪些數(shù)據(jù)被修改了。
增量快照示意圖
雖然跟 AOF 相比,快照的恢復(fù)速度快,但是,快照的頻率不好把握,如果頻率太低,兩次快照間一旦宕機(jī),就可能有比較多的數(shù)據(jù)丟失。如果頻率太高,又會產(chǎn)生額外開銷,那么,還有什么方法既能利用 RDB 的快速恢復(fù),又能以較小的開銷做到盡量少丟數(shù)據(jù)呢?
Redis 4.0 中提出了一個(gè)混合使用 AOF 日志和內(nèi)存快照的方法。簡單來說,內(nèi)存快照以一定的頻率執(zhí)行,在兩次快照之間,使用 AOF 日志記錄這期間的所有命令操作。
這樣一來,快照不用很頻繁地執(zhí)行,這就避免了頻繁 fork 對主線程的影響。而且,AOF 日志也只用記錄兩次快照間的操作,也就是說,不需要記錄所有操作了,因此,就不會出現(xiàn)文件過大的情況了,也可以避免重寫開銷。
內(nèi)存快照和 AOF 混合使用
06 | 數(shù)據(jù)同步:主從庫如何實(shí)現(xiàn)數(shù)據(jù)一致?
Redis 具有高可靠性,又是什么意思呢?
其實(shí),這里有兩層含義:一是數(shù)據(jù)盡量少丟失,二是服務(wù)盡量少中斷。AOF 和 RDB 保證了前者,而對于后者,Redis 的做法就是增加副本冗余量,將一份數(shù)據(jù)同時(shí)保存在多個(gè)實(shí)例上。即使有一個(gè)實(shí)例出現(xiàn)了故障,需要過一段時(shí)間才能恢復(fù),其他實(shí)例也可以對外提供服務(wù),不會影響業(yè)務(wù)使用。
多實(shí)例保存同一份數(shù)據(jù),聽起來好像很不錯(cuò),但是,我們必須要考慮一個(gè)問題:這么多副本,它們之間的數(shù)據(jù)如何保持一致呢?數(shù)據(jù)讀寫操作可以發(fā)給所有的實(shí)例嗎?
Redis 提供了主從庫模式,以保證數(shù)據(jù)副本的一致,主從庫之間采用的是讀寫分離的方式。
- 讀操作:主庫、從庫都可以接收;寫操作:首先到主庫執(zhí)行,然后,主庫將寫操作同步給從庫。
- 寫操作:首先到主庫執(zhí)行,然后,主庫將寫操作同步給從庫。
Redis 主從庫和讀寫分離
那么,為什么要采用讀寫分離的方式呢?
主從庫模式一旦采用了讀寫分離,所有數(shù)據(jù)的修改只會在主庫上進(jìn)行,不用協(xié)調(diào)三個(gè)實(shí)例。主庫有了最新的數(shù)據(jù)后,會同步給從庫,這樣,主從庫的數(shù)據(jù)就是一致的。
那么,主從庫同步是如何完成的呢?主庫數(shù)據(jù)是一次性傳給從庫,還是分批同步?要是主從庫間的網(wǎng)絡(luò)斷連了,數(shù)據(jù)還能保持一致嗎?
主從庫間如何進(jìn)行第一次同步?
當(dāng)我們啟動多個(gè) Redis 實(shí)例的時(shí)候,它們相互之間就可以通過 replicaof(Redis 5.0 之前使用 slaveof)命令形成主庫和從庫的關(guān)系,之后會按照三個(gè)階段完成數(shù)據(jù)的第一次同步。
主從庫第一次同步的流程
第一階段是主從庫間建立連接、協(xié)商同步的過程,主要是為全量復(fù)制做準(zhǔn)備。在這一步,從庫和主庫建立起連接,并告訴主庫即將進(jìn)行同步,主庫確認(rèn)回復(fù)后,主從庫間就可以開始同步了。
FULLRESYNC 響應(yīng)表示第一次復(fù)制采用的全量復(fù)制,也就是說,主庫會把當(dāng)前所有的數(shù)據(jù)都復(fù)制給從庫。
在第二階段,主庫將所有數(shù)據(jù)同步給從庫。從庫收到數(shù)據(jù)后,在本地完成數(shù)據(jù)加載。這個(gè)過程依賴于內(nèi)存快照生成的 RDB 文件。
最后,也就是第三個(gè)階段,主庫會把第二階段執(zhí)行過程中新收到的寫命令,再發(fā)送給從庫。
主從級聯(lián)模式分擔(dān)全量復(fù)制時(shí)的主庫壓力
通過分析主從庫間第一次數(shù)據(jù)同步的過程,你可以看到,一次全量復(fù)制中,對于主庫來說,需要完成兩個(gè)耗時(shí)的操作:生成 RDB 文件和傳輸 RDB 文件。
如果從庫數(shù)量很多,而且都要和主庫進(jìn)行全量復(fù)制的話,就會導(dǎo)致主庫忙于 fork 子進(jìn)程生成 RDB 文件,進(jìn)行數(shù)據(jù)全量同步。fork 這個(gè)操作會阻塞主線程處理正常請求,從而導(dǎo)致主庫響應(yīng)應(yīng)用程序的請求速度變慢。此外,傳輸 RDB 文件也會占用主庫的網(wǎng)絡(luò)帶寬,同樣會給主庫的資源使用帶來壓力。那么,有沒有好的解決方法可以分擔(dān)主庫壓力呢?
“主 - 從 - 從”模式
可以通過“主 - 從 - 從”模式將主庫生成 RDB 和傳輸 RDB 的壓力,以級聯(lián)的方式分散到從庫上。
簡單來說,我們在部署主從集群的時(shí)候,可以手動選擇一個(gè)從庫(比如選擇內(nèi)存資源配置較高的從庫),用于級聯(lián)其他的從庫。然后,我們可以再選擇一些從庫(例如三分之一的從庫),在這些從庫上執(zhí)行如下命令,讓它們和剛才所選的從庫,建立起主從關(guān)系。
這樣一來,這些從庫就會知道,在進(jìn)行同步時(shí),不用再和主庫進(jìn)行交互了,只要和級聯(lián)的從庫進(jìn)行寫操作同步就行了,這就可以減輕主庫上的壓力。
級聯(lián)的“主-從-從”模式
一旦主從庫完成了全量復(fù)制,它們之間就會一直維護(hù)一個(gè)網(wǎng)絡(luò)連接,主庫會通過這個(gè)連接將后續(xù)陸續(xù)收到的命令操作再同步給從庫,這個(gè)過程也稱為基于長連接的命令傳播,可以避免頻繁建立連接的開銷。
主從庫間網(wǎng)絡(luò)斷了怎么辦?
網(wǎng)絡(luò)斷了之后,主從庫會采用增量復(fù)制的方式繼續(xù)同步。
全量復(fù)制是同步所有數(shù)據(jù),而增量復(fù)制只會把主從庫網(wǎng)絡(luò)斷連期間主庫收到的命令,同步給從庫。
那么,增量復(fù)制時(shí),主從庫之間具體是怎么保持同步的呢?
當(dāng)主從庫斷連后,主庫會把斷連期間收到的寫操作命令,寫入 replication buffer,同時(shí)也會把這些操作命令也寫入 repl_backlog_buffer 這個(gè)緩沖區(qū)。repl_backlog_buffer 是一個(gè)環(huán)形緩沖區(qū),主庫會記錄自己寫到的位置,從庫則會記錄自己已經(jīng)讀到的位置。
Redis repl_backlog_buffer 的使用
Redis 增量復(fù)制流程
不過,有一個(gè)地方我要強(qiáng)調(diào)一下,因?yàn)?repl_backlog_buffer 是一個(gè)環(huán)形緩沖區(qū),所以在緩沖區(qū)寫滿后,主庫會繼續(xù)寫入,此時(shí),就會覆蓋掉之前寫入的操作。如果從庫的讀取速度比較慢,就有可能導(dǎo)致從庫還未讀取的操作被主庫新寫的操作覆蓋了,這會導(dǎo)致主從庫間的數(shù)據(jù)不一致。
因此,我們要想辦法避免這一情況,一般而言,我們可以調(diào)整 repl_backlog_size 這個(gè)參數(shù)。這個(gè)參數(shù)和所需的緩沖空間大小有關(guān)。緩沖空間的計(jì)算公式是:緩沖空間大小 = 主庫寫入命令速度 * 操作大小 - 主從庫間網(wǎng)絡(luò)傳輸命令速度 * 操作大小。在實(shí)際應(yīng)用中,考慮到可能存在一些突發(fā)的請求壓力,我們通常需要把這個(gè)緩沖空間擴(kuò)大一倍,即 repl_backlog_size = 緩沖空間大小 * 2,這也就是 repl_backlog_size 的最終值。
Redis 的主從庫同步的基本原理,總結(jié)來說,有三種模式:全量復(fù)制、基于長連接的命令傳播,以及增量復(fù)制。
07 | 哨兵機(jī)制:主庫掛了,如何不間斷服務(wù)?
主庫故障后從庫無法服務(wù)寫操作
涉及到三個(gè)問題:
哨兵機(jī)制的基本流程
哨兵其實(shí)就是一個(gè)運(yùn)行在特殊模式下的 Redis 進(jìn)程,主從庫實(shí)例運(yùn)行的同時(shí),它也在運(yùn)行。哨兵主要負(fù)責(zé)的就是三個(gè)任務(wù):監(jiān)控、選主(選擇主庫)和通知。
監(jiān)控是指哨兵進(jìn)程在運(yùn)行時(shí),周期性地給所有的主從庫發(fā)送 PING 命令,檢測它們是否仍然在線運(yùn)行。如果從庫沒有在規(guī)定時(shí)間內(nèi)響應(yīng)哨兵的 PING 命令,哨兵就會把它標(biāo)記為“下線狀態(tài)”;同樣,如果主庫也沒有在規(guī)定時(shí)間內(nèi)響應(yīng)哨兵的 PING 命令,哨兵就會判定主庫下線,然后開始自動切換主庫的流程。
這個(gè)流程首先是執(zhí)行哨兵的第二個(gè)任務(wù),選主。主庫掛了以后,哨兵就需要從很多個(gè)從庫里,按照一定的規(guī)則選擇一個(gè)從庫實(shí)例,把它作為新的主庫。這一步完成后,現(xiàn)在的集群里就有了新主庫。
然后,哨兵會執(zhí)行最后一個(gè)任務(wù):通知。在執(zhí)行通知任務(wù)時(shí),哨兵會把新主庫的連接信息發(fā)給其他從庫,讓它們執(zhí)行 replicaof 命令,和新主庫建立連接,并進(jìn)行數(shù)據(jù)復(fù)制。同時(shí),哨兵會把新主庫的連接信息通知給客戶端,讓它們把請求操作發(fā)到新主庫上。
哨兵機(jī)制的三項(xiàng)任務(wù)與目標(biāo)
在這三個(gè)任務(wù)中,通知任務(wù)相對來說比較簡單,哨兵只需要把新主庫信息發(fā)給從庫和客戶端,讓它們和新主庫建立連接就行,并不涉及決策的邏輯。但是,在監(jiān)控和選主這兩個(gè)任務(wù)中,哨兵需要做出兩個(gè)決策:
- 在監(jiān)控任務(wù)中,哨兵需要判斷主庫是否處于下線狀態(tài);
- 在選主任務(wù)中,哨兵也要決定選擇哪個(gè)從庫實(shí)例作為主庫。
主觀下線和客觀下線
哨兵進(jìn)程會使用 PING 命令檢測它自己和主、從庫的網(wǎng)絡(luò)連接情況,用來判斷實(shí)例的狀態(tài)。
如果檢測的是從庫,那么,哨兵簡單地把它標(biāo)記為“主觀下線”就行了,因?yàn)閺膸斓南戮€影響一般不太大,集群的對外服務(wù)不會間斷。
但是,如果檢測的是主庫,那么,哨兵還不能簡單地把它標(biāo)記為“主觀下線”,開啟主從切換。因?yàn)楹苡锌赡艽嬖谶@么一個(gè)情況:那就是哨兵誤判了,其實(shí)主庫并沒有故障。可是,一旦啟動了主從切換,后續(xù)的選主和通知操作都會帶來額外的計(jì)算和通信開銷。
誤判一般會發(fā)生在集群網(wǎng)絡(luò)壓力較大、網(wǎng)絡(luò)擁塞,或者是主庫本身壓力較大的情況下。
那怎么減少誤判呢?
哨兵機(jī)制,它通常會采用多實(shí)例組成的集群模式進(jìn)行部署,這也被稱為哨兵集群。引入多個(gè)哨兵實(shí)例一起來判斷,就可以避免單個(gè)哨兵因?yàn)樽陨砭W(wǎng)絡(luò)狀況不好,而誤判主庫下線的情況。同時(shí),多個(gè)哨兵的網(wǎng)絡(luò)同時(shí)不穩(wěn)定的概率較小,由它們一起做決策,誤判率也能降低。
在判斷主庫是否下線時(shí),不能由一個(gè)哨兵說了算,只有大多數(shù)的哨兵實(shí)例,都判斷主庫已經(jīng)“主觀下線”了,主庫才會被標(biāo)記為“客觀下線”,這個(gè)叫法也是表明主庫下線成為一個(gè)客觀事實(shí)了。這個(gè)判斷原則就是:少數(shù)服從多數(shù)。同時(shí),這會進(jìn)一步觸發(fā)哨兵開始主從切換流程。
客觀下線的判斷
簡單來說,“客觀下線”的標(biāo)準(zhǔn)就是,當(dāng)有 N 個(gè)哨兵實(shí)例時(shí),最好要有 N/2 + 1 個(gè)實(shí)例判斷主庫為“主觀下線”,才能最終判定主庫為“客觀下線”。這樣一來,就可以減少誤判的概率,也能避免誤判帶來的無謂的主從庫切換。
如何選定新主庫?
一般來說,我把哨兵選擇新主庫的過程稱為“篩選 + 打分”。簡單來說,我們在多個(gè)從庫中,先按照一定的篩選條件,把不符合條件的從庫去掉。然后,我們再按照一定的規(guī)則,給剩下的從庫逐個(gè)打分,將得分最高的從庫選為新主庫。
新主庫的選擇過程
首先來看篩選的條件。
在選主時(shí),除了要檢查從庫的當(dāng)前在線狀態(tài),還要判斷它之前的網(wǎng)絡(luò)連接狀態(tài)。
配置項(xiàng) down-after-milliseconds * 10
接下來就要給剩余的從庫打分了。我們可以分別按照三個(gè)規(guī)則依次進(jìn)行三輪打分,這三個(gè)規(guī)則分別是從庫優(yōu)先級、從庫復(fù)制進(jìn)度以及從庫 ID 號。只要在某一輪中,有從庫得分最高,那么它就是主庫了,選主過程到此結(jié)束。如果沒有出現(xiàn)得分最高的從庫,那么就繼續(xù)進(jìn)行下一輪。
基于復(fù)制進(jìn)度的新主庫選主原則
首先,哨兵會按照在線狀態(tài)、網(wǎng)絡(luò)狀態(tài),篩選過濾掉一部分不符合要求的從庫,然后,依次按照優(yōu)先級、復(fù)制進(jìn)度、ID 號大小再對剩余的從庫進(jìn)行打分,只要有得分最高的從庫出現(xiàn),就把它選為新主庫。
08 | 哨兵集群:哨兵掛了,主從庫還能切換嗎?
如果有哨兵實(shí)例在運(yùn)行時(shí)發(fā)生了故障,主從庫還能正常切換嗎?
實(shí)際上,一旦多個(gè)實(shí)例組成了哨兵集群,即使有哨兵實(shí)例出現(xiàn)故障掛掉了,其他哨兵還能繼續(xù)協(xié)作完成主從庫切換的工作,包括判定主庫是不是處于下線狀態(tài),選擇新主庫,以及通知從庫和客戶端。
如果你部署過哨兵集群的話就會知道,在配置哨兵的信息時(shí),我們只需要用到下面的這個(gè)配置項(xiàng),設(shè)置主庫的 IP 和端口,并沒有配置其他哨兵的連接信息。
sentinel monitor <master-name> <ip> <redis-port> <quorum>Redis
Copy
這些哨兵實(shí)例既然都不知道彼此的地址,又是怎么組成集群的呢?
基于 pub/sub 機(jī)制的哨兵集群組成
哨兵實(shí)例之間可以相互發(fā)現(xiàn),要?dú)w功于 Redis 提供的 pub/sub 機(jī)制,也就是發(fā)布 / 訂閱機(jī)制。
哨兵只要和主庫建立起了連接,就可以在主庫上發(fā)布消息了,比如說發(fā)布它自己的連接信息(IP 和端口)。同時(shí),它也可以從主庫上訂閱消息,獲得其他哨兵發(fā)布的連接信息。當(dāng)多個(gè)哨兵實(shí)例都在主庫上做了發(fā)布和訂閱操作后,它們之間就能知道彼此的 IP 地址和端口。
為了區(qū)分不同應(yīng)用的消息,Redis 會以頻道的形式,對這些消息進(jìn)行分門別類的管理。所謂的頻道,實(shí)際上就是消息的類別。當(dāng)消息類別相同時(shí),它們就屬于同一個(gè)頻道。反之,就屬于不同的頻道。只有訂閱了同一個(gè)頻道的應(yīng)用,才能通過發(fā)布的消息進(jìn)行信息交換。
在主從集群中,主庫上有一個(gè)名為“sentinel:hello”的頻道,不同哨兵就是通過它來相互發(fā)現(xiàn),實(shí)現(xiàn)互相通信的。
哨兵集群
那么,哨兵是如何知道從庫的 IP 地址和端口的呢?
哨兵 INFO 命令
如何在客戶端通過監(jiān)控了解哨兵進(jìn)行主從切換的過程呢?比如說,主從切換進(jìn)行到哪一步了?這其實(shí)就是要求,客戶端能夠獲取到哨兵集群在監(jiān)控、選主、切換這個(gè)過程中發(fā)生的各種事件。
基于 pub/sub 機(jī)制的客戶端事件通知
從本質(zhì)上說,哨兵就是一個(gè)運(yùn)行在特定模式下的 Redis 實(shí)例,只不過它并不服務(wù)請求操作,只是完成監(jiān)控、選主和通知的任務(wù)。所以,每個(gè)哨兵實(shí)例也提供 pub/sub 機(jī)制,客戶端可以從哨兵訂閱消息。哨兵提供的消息訂閱頻道有很多,不同頻道包含了主從庫切換過程中的不同關(guān)鍵事件。
哨兵提供的消息訂閱頻道
有了 pub/sub 機(jī)制,哨兵和哨兵之間、哨兵和從庫之間、哨兵和客戶端之間就都能建立起連接了,再加上主庫下線判斷和選主依據(jù),哨兵集群的監(jiān)控、選主和通知三個(gè)任務(wù)就基本可以正常工作了。
由哪個(gè)哨兵執(zhí)行主從切換?
確定由哪個(gè)哨兵執(zhí)行主從切換的過程,和主庫“客觀下線”的判斷過程類似,也是一個(gè)“投票仲裁”的過程。
任何一個(gè)實(shí)例只要自身判斷主庫“主觀下線”后,就會給其他實(shí)例發(fā)送 is-master-down-by-addr 命令。接著,其他實(shí)例會根據(jù)自己和主庫的連接情況,做出 Y 或 N 的響應(yīng),Y 相當(dāng)于贊成票,N 相當(dāng)于反對票。
主庫“客觀下線”
一個(gè)哨兵獲得了仲裁所需的贊成票數(shù)后,就可以標(biāo)記主庫為“客觀下線”。這個(gè)所需的贊成票數(shù)是通過哨兵配置文件中的 quorum 配置項(xiàng)設(shè)定的。贊成票包括哨兵自己的一張贊成票。
此時(shí),這個(gè)哨兵就可以再給其他哨兵發(fā)送命令,表明希望由自己來執(zhí)行主從切換,并讓所有其他哨兵進(jìn)行投票。這個(gè)投票過程稱為“Leader 選舉”。因?yàn)樽罱K執(zhí)行主從切換的哨兵稱為 Leader,投票過程就是確定 Leader。
在投票過程中,任何一個(gè)想成為 Leader 的哨兵,要滿足兩個(gè)條件:第一,拿到半數(shù)以上的贊成票;第二,拿到的票數(shù)同時(shí)還需要大于等于哨兵配置文件中的 quorum 值。
3 個(gè)哨兵、quorum 為 2 的選舉過程
在 T4 時(shí)刻,S2 才收到 T1 時(shí) S1 發(fā)送的投票命令。因?yàn)?S2 已經(jīng)在 T3 時(shí)同意了 S3 的投票請求,此時(shí),S2 給 S1 回復(fù) N,表示不同意 S1 成為 Leader。發(fā)生這種情況,是因?yàn)?S3 和 S2 之間的網(wǎng)絡(luò)傳輸正常,而 S1 和 S2 之間的網(wǎng)絡(luò)傳輸可能正好擁塞了,導(dǎo)致投票請求傳輸慢了。
哨兵集群能夠進(jìn)行成功投票,很大程度上依賴于選舉命令的正常網(wǎng)絡(luò)傳播。如果網(wǎng)絡(luò)壓力較大或有短時(shí)堵塞,就可能導(dǎo)致沒有一個(gè)哨兵能拿到半數(shù)以上的贊成票。所以,等到網(wǎng)絡(luò)擁塞好轉(zhuǎn)之后,再進(jìn)行投票選舉,成功的概率就會增加。
23 | 旁路緩存:Redis是如何工作的?
緩存的特征
計(jì)算機(jī)系統(tǒng)中的三層存儲結(jié)構(gòu)
計(jì)算機(jī)系統(tǒng)中,默認(rèn)有兩種緩存
緩存的第一個(gè)特征:在一個(gè)層次化的系統(tǒng)中,緩存一定是一個(gè)快速子系統(tǒng),數(shù)據(jù)存在緩存中時(shí),能避免每次從慢速子系統(tǒng)中存取數(shù)據(jù)。
緩存的第二個(gè)特征:緩存系統(tǒng)的容量大小總是小于后端慢速系統(tǒng)的,我們不可能把所有數(shù)據(jù)都放在緩存系統(tǒng)中。
Redis 緩存處理請求的兩種情況
把 Redis 用作緩存時(shí),我們會把 Redis 部署在數(shù)據(jù)庫的前端,業(yè)務(wù)應(yīng)用在訪問數(shù)據(jù)時(shí),會先查詢 Redis 中是否保存了相應(yīng)的數(shù)據(jù)。此時(shí),根據(jù)數(shù)據(jù)是否存在緩存中,會有兩種情況。
- 緩存命中:Redis 中有相應(yīng)數(shù)據(jù),就直接讀取 Redis,性能非常快。
- 緩存缺失:Redis 中沒有保存相應(yīng)數(shù)據(jù),就從后端數(shù)據(jù)庫中讀取數(shù)據(jù),性能就會變慢。而且,一旦發(fā)生緩存缺失,為了讓后續(xù)請求能從緩存中讀取到數(shù)據(jù),我們需要把缺失的數(shù)據(jù)寫入 Redis,這個(gè)過程叫作緩存更新。緩存更新操作會涉及到保證緩存和數(shù)據(jù)庫之間的數(shù)據(jù)一致性問題。
發(fā)生緩存命中或缺失時(shí),應(yīng)用讀取數(shù)據(jù)的情況
使用 Redis 緩存時(shí),我們基本有三個(gè)操作:
- 應(yīng)用讀取數(shù)據(jù)時(shí),需要先讀取 Redis;
- 發(fā)生緩存缺失時(shí),需要從數(shù)據(jù)庫讀取數(shù)據(jù);
- 發(fā)生緩存缺失時(shí),還需要更新緩存。
那么,這些操作具體是由誰來做的呢?
Redis 作為旁路緩存的使用操作
Redis 是一個(gè)獨(dú)立的系統(tǒng)軟件,和業(yè)務(wù)應(yīng)用程序是兩個(gè)軟件,當(dāng)我們部署了 Redis 實(shí)例后,它只會被動地等待客戶端發(fā)送請求,然后再進(jìn)行處理。所以,如果應(yīng)用程序想要使用 Redis 緩存,我們就要在程序中增加相應(yīng)的緩存操作代碼。所以,我們也把 Redis 稱為旁路緩存,也就是說,讀取緩存、讀取數(shù)據(jù)庫和更新緩存的操作都需要在應(yīng)用程序中來完成。
那么,使用 Redis 緩存時(shí),具體來說,我們需要在應(yīng)用程序中增加三方面的代碼:
- 當(dāng)應(yīng)用程序需要讀取數(shù)據(jù)時(shí),我們需要在代碼中顯式調(diào)用 Redis 的 GET 操作接口,進(jìn)行查詢;
- 如果緩存缺失了,應(yīng)用程序需要再和數(shù)據(jù)庫連接,從數(shù)據(jù)庫中讀取數(shù)據(jù);
- 當(dāng)緩存中的數(shù)據(jù)需要更新時(shí),我們也需要在應(yīng)用程序中顯式地調(diào)用 SET 操作接口,把更新的數(shù)據(jù)寫入緩存。
為了使用緩存,Web 應(yīng)用程序需要有一個(gè)表示緩存系統(tǒng)的實(shí)例對象 redisCache,還需要主動調(diào)用 Redis 的 GET 接口,并且要處理緩存命中和緩存缺失時(shí)的邏輯,例如在緩存缺失時(shí),需要更新緩存。
緩存的類型
按照 Redis 緩存是否接受寫請求,我們可以把它分成只讀緩存和讀寫緩存。
只讀緩存
當(dāng) Redis 用作只讀緩存時(shí),應(yīng)用要讀取數(shù)據(jù)的話,會先調(diào)用 Redis GET 接口,查詢數(shù)據(jù)是否存在。而所有的數(shù)據(jù)寫請求,會直接發(fā)往后端的數(shù)據(jù)庫,在數(shù)據(jù)庫中增刪改。對于刪改的數(shù)據(jù)來說,如果 Redis 已經(jīng)緩存了相應(yīng)的數(shù)據(jù),應(yīng)用需要把這些緩存的數(shù)據(jù)刪除,Redis 中就沒有這些數(shù)據(jù)了。
當(dāng)應(yīng)用再次讀取這些數(shù)據(jù)時(shí),會發(fā)生緩存缺失,應(yīng)用會把這些數(shù)據(jù)從數(shù)據(jù)庫中讀出來,并寫到緩存中。這樣一來,這些數(shù)據(jù)后續(xù)再被讀取時(shí),就可以直接從緩存中獲取了,能起到加速訪問的效果。
只讀緩存
只讀緩存直接在數(shù)據(jù)庫中更新數(shù)據(jù)的好處是,所有最新的數(shù)據(jù)都在數(shù)據(jù)庫中,而數(shù)據(jù)庫是提供數(shù)據(jù)可靠性保障的,這些數(shù)據(jù)不會有丟失的風(fēng)險(xiǎn)。
讀寫緩存
對于讀寫緩存來說,除了讀請求會發(fā)送到緩存進(jìn)行處理(直接在緩存中查詢數(shù)據(jù)是否存在),所有的寫請求也會發(fā)送到緩存,在緩存中直接對數(shù)據(jù)進(jìn)行增刪改操作。
但是,和只讀緩存不一樣的是,在使用讀寫緩存時(shí),最新的數(shù)據(jù)是在 Redis 中,而 Redis 是內(nèi)存數(shù)據(jù)庫,一旦出現(xiàn)掉電或宕機(jī),內(nèi)存中的數(shù)據(jù)就會丟失。
根據(jù)業(yè)務(wù)應(yīng)用對數(shù)據(jù)可靠性和緩存性能的不同要求,我們會有同步直寫和異步寫回兩種策略。其中,同步直寫策略優(yōu)先保證數(shù)據(jù)可靠性,而異步寫回策略優(yōu)先提供快速響應(yīng)。
同步直寫是指,寫請求發(fā)給緩存的同時(shí),也會發(fā)給后端數(shù)據(jù)庫進(jìn)行處理,等到緩存和數(shù)據(jù)庫都寫完數(shù)據(jù),才給客戶端返回。
而異步寫回策略,則是優(yōu)先考慮了響應(yīng)延遲。此時(shí),所有寫請求都先在緩存中處理。等到這些增改的數(shù)據(jù)要被從緩存中淘汰出來時(shí),緩存將它們寫回后端數(shù)據(jù)庫。
同步直寫和異步寫回
小結(jié)
緩存的兩個(gè)特征,分別是在分層系統(tǒng)中,數(shù)據(jù)暫存在快速子系統(tǒng)中有助于加速訪問;緩存容量有限,緩存寫滿時(shí),數(shù)據(jù)需要被淘汰。而 Redis 天然就具有高性能訪問和數(shù)據(jù)淘汰機(jī)制,正好符合緩存的這兩個(gè)特征的要求,所以非常適合用作緩存。
Redis 作為旁路緩存的特性,旁路緩存就意味著需要在應(yīng)用程序中新增緩存邏輯處理的代碼。
總結(jié)
以上是生活随笔為你收集整理的Redis 核心技术与实战的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 编写第二个Spring程序——AOP实现
- 下一篇: Redis哨兵模式(sentinel)学