1. 程式人生 > 其它 >Redis 核心技術與實戰 Redis 核心技術與實戰

Redis 核心技術與實戰 Redis 核心技術與實戰

Redis 核心技術與實戰

 

 

目錄

 

開篇詞 | 這樣學 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 | 哨兵機制:主庫掛了,如何不間斷服務?

主庫故障後從庫無法服務寫操作

涉及到三個問題:

  1. 主庫真的掛了嗎?
  2. 該選擇哪個從庫作為主庫?
  3. 怎麼把新主庫的相關資訊通知給從庫和客戶端呢?

哨兵機制的基本流程

哨兵其實就是一個執行在特殊模式下的 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 作為旁路快取的特性,旁路快取就意味著需要在應用程式中新增快取邏輯處理的程式碼。

刻鵠不成尚類鶩,畫虎不成反類狗。   好文要頂 關注我 收藏該文 clipboard
關注 - 2
粉絲 - 1     +加關注 0 0       « 上一篇: 使用 TIMESTAMP 或 DATETIME 追蹤行的修改時間
» 下一篇: Installing MySQL on Microsoft Windows Using a noinstall ZIP Archive posted @ 2021-01-04 23:32  clipboard  閱讀(2827)  評論(0編輯  收藏  舉報

 

目錄

 

開篇詞 | 這樣學 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 | 哨兵機制:主庫掛了,如何不間斷服務?

主庫故障後從庫無法服務寫操作

涉及到三個問題:

  1. 主庫真的掛了嗎?
  2. 該選擇哪個從庫作為主庫?
  3. 怎麼把新主庫的相關資訊通知給從庫和客戶端呢?

哨兵機制的基本流程

哨兵其實就是一個執行在特殊模式下的 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 作為旁路快取的特性,旁路快取就意味著需要在應用程式中新增快取邏輯處理的程式碼。