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