天天看點

深入淺出Redis:資料持久化提高可用性

作者:JAVA後端架構
深入淺出Redis:資料持久化提高可用性

1、介紹

從前面發的《深刻了解高性能Redis的本質》 中可以知道, 我們經常在資料庫層上加一層緩存(如Redis),來保證資料的通路效率。

這樣性能确實也有了大幅度的提升,但是本身Redis也是一層服務,也存在當機、故障的可能性。

一旦服務挂起,可能生産的後果包括如下幾方面:

1、Redis的資料是存在記憶體中的,是以一旦挂起,記憶體中的資料會全部丢失。

2、I/O從記憶體層級遷移到磁盤層級,性能極速下降。

3、原本通路緩存的請求會透過緩存層直接投向資料庫,給資料庫帶來極大的壓力,甚至導緻雪崩。

是以,緩存層崩潰産生的後果是災難的。為了避免當機和當機後的資料丢失, 為了保證資料的快速恢複,Redis提供了兩個持久化資料的能力, AOF(Append Only FIle)日志 和 RDB 快照。

2、關于RDB 記憶體快照

大規模高并發的分布式場景,經常會遇到問題就是Redis挂起,導緻通路失敗,而所有的請求透過緩存層投向資料庫,給資料庫造成極大的壓力。

而Redis的資料是存儲在高速緩存中,即使我們重新開機并且恢複使用,緩存池依舊是空的,因為記憶體被釋放了。

重建立立緩存的過程,對資料庫也是一個暴擊的過程,很可能會導緻整個系統調用鍊的雪崩。

我們知道,Redis 資料都是儲存在記憶體中,能不能将記憶體中的資料進一步寫到磁盤上,Redis 重新開機的時候就可以把磁盤上的資料快速恢複到記憶體中。這樣,即使Redis當機重新開機之後,依然能夠正常的提供服務。

但是不能忽略一個問題,Redis和MySQL最大的差別之一就是一個存儲在記憶體,一個持久化在磁盤。但是如果每次資料的變化(新增、修改、删除緩存)都要寫記憶體并同時寫磁盤,這樣成本太高,記憶體+磁盤,會讓 Redis 性能大大降低。而且還要保證原子性操作,避免記憶體和磁盤的資料不一緻。

2.1 使用記憶體快照

為了避免實時寫入高頻操作磁盤帶來的負面效應。Redis提供了記憶體快照政策。

我們知道,Redis 在 執行寫(增、删、改)指令過程中,記憶體中資料會持續的在變化。而記憶體快照,指的是 Redis 記憶體中的資料在某一刻的狀态。就好比如是拍照一樣,你把那一刻的資料都定格下來,持久化到磁盤上。打遊戲的同學可以想象存盤。

快照檔案我們稱之為 RDB 檔案,即 Redis DataBase 的縮寫。

Redis 通過定時執行 RDB 記憶體快照,這樣就不必每次執行寫指令都存盤,隻需要在執行記憶體快照的時候寫磁盤。這樣既保證Redis的高效讀寫,還實作了定時持久化,當機後可快速恢複資料。

深入淺出Redis:資料持久化提高可用性

如上圖,在做資料恢複時,直接将 RDB 檔案讀入記憶體完成恢複。

2.2 生成RDB政策

Redis 提供了兩種模式來生成 RDB 檔案:

  • save:由主線程來執行,同步阻塞,隻有等save完成後,才能進行新操作;
  • bgsave:執行後,會立刻傳回OK,同時調用 glibc 的函數fork産生一個子程序用于寫入 RDB 檔案,快照持久化完全交給子程序來處理。主程序繼續執行他自己的工作,非阻塞。

2.2.1 save模式

save模式是主程序執行,非常不建議使用主程序執行的方式,在 《深刻了解高性能Redis的本質》 中,

我們知道他的主操作都是在單線程模型上完成的。是以盡量避免 RDB 檔案生成影響主線程的網絡I/O和鍵值對讀寫。

2.2.2 bgsave模式

上面提到的另外一種方式,fork一個子程序來寫RDB檔案。

Redis 使用作業系統的多程序寫時複制技術 COW(Copy On Write) 來實作快照持久化,這個很重要,具體可以了解下這篇《Copy On Write機制》,寫的不錯。

Redis 在持久化時會調用 glibc 的函數fork産生一個子程序,由這個子程序來處理快照持久化的動作,子程序可以共享主程序的所有記憶體資料,是以它讀取到主程序的資料之後寫入到 RDB 檔案。而父程序繼續處理用戶端的寫操作,不受影響。

在建立 RDB 檔案時,程式會對資料庫中的鍵進行檢查,僅僅将未過期的鍵儲存到新建立的 RDB 檔案中。

當主程序執行寫指令修改資料的時候,這個資料就會複制一份副本, bgsave 子程序讀取這個副本資料寫到 RDB 檔案,是以主程序就可以直接修改原來的資料。

深入淺出Redis:資料持久化提高可用性

這既保證了快照的完整性,也允許主程序同時對資料進行修改,避免了對正常業務的影響。

2.2.3 避免過頻全量照片

雖然說Redis 使用 bgsave 函數 fork 子程序在背景完成 記憶體中的資料做快照,沒有影響父程序繼續處理用戶端的各種操作。

但是需注意一點,過于頻繁的執行全量的資料快照,必然會導緻嚴重的性能開銷:

頻繁生成 RDB 檔案寫入磁盤,磁盤壓力過大,效率降低。

fork 出來的 bgsave 子程序因為共享主線程的資料,一定程度上會阻塞主線程的運作,主線程的記憶體越大,阻塞時間越長。

2.3 總結

快照的恢複速度快,但是生成 RDB 檔案的頻率需要把握一個度,頻率過低快照間隔資料較大,丢失的資料就會比較多;頻率太快,又會消耗額外開銷,降低Redis性能。

RDB 建議采用二進制 + 資料壓縮的方式寫磁盤,檔案體積小,資料恢複速度快。

3、AOF 日志

AOF 日志存儲了 Redis 伺服器的順序指令序列,AOF 日志隻記錄對記憶體進行修改的指令記錄。

假設 AOF 日志記錄了自 Redis 執行個體建立以來所有的修改性指令序列,那麼就可以通過對一個空的 Redis 執行個體順序執行所有的指令。

也就是說,可以通過重放(replay),來建立 Redis 目前執行個體的記憶體資料結構。這種模式有沒有很熟悉,有沒有想到MySQL主從同步時候的relay log。

3.1 日志變更前後對比

AOF記錄日志有兩種模式,一種是預寫式日志,也稱寫前日志(Write Ahead Log, WAL):在實際寫資料之前,将修改的資料寫到日志檔案中。

另外一種是寫後日志:先執行寫操作,當資料存入記憶體後,再記錄日志。

預寫式日志類似 MySQL Innodb 引擎 中的 redo log,修改資料前先記錄日志,再修改。

深入淺出Redis:資料持久化提高可用性

3.2 日志格式

Redis 接收到 set keyName someValue 指令的時候,會先将資料寫到記憶體,Redis 會按照如下格式寫入 AOF 檔案。

  • *3:表示目前指令分為三個部分,每個部分都是 $ + 數字開頭,後面是3部分的具體内容:指令、鍵、值。
  • 數字:表示這部分的指令、鍵、值多占用的位元組大小。比如 $3表示這部分包含 3 個字元,也就是 set 的長度。
深入淺出Redis:資料持久化提高可用性

推薦使用寫後日志的模式,避免了額外的檢查開銷,不需要對執行的指令進行文法檢查。如果使用寫前日志的話,就需要先檢查文法是否有誤,否則日志記錄了錯誤的指令,在使用日志恢複的時候就會出錯。另外,寫後才記錄日志,不會阻塞目前的 寫 指令執行。

# set keyName someValue
*3
$3
set
$7  #長度為7
keyName
$9 #長度為9
someValue

# 執行 mset key1 1 ,key2 2 ,key33 3
# aof日志如下:
*7  # 本批指令需要往下讀7行非 $ 開始的指令
$4  #接着讀取4個位元組寬度,‘mset’長度為4,記為 $4
mset
$4  #接着讀取4個位元組寬度,‘key1’長度為4,記為 $4
key1
$1  #接着讀取1個位元組寬度,‘1’長度為1,記為 $1
1
$4
key2
$1
2
$5  #接着讀取的位元組寬度,‘$key33’長度為5,記為 $5
key33
$1
3           

3.3 可能存在的問題

可能存在丢失:比如Redis 剛執行完指令,還沒記錄日志當機了,指令資料就丢了。

AOF 避免了目前指令的阻塞,但是AOF 日志是主線程執行,将日志寫入磁盤過程中,如果磁盤壓力大就會導緻執行變慢,降低後續的操作。

3.4 寫回政策

上面的問題,在Redis高頻讀寫的時候是必然存在的,想要解決,在寫入的時候做一層緩沖就可以了,避免直塞。這時候Redis提供了一種執行政策叫寫回政策。

3.4.1 寫回政策說明

為了提高日志檔案的寫入效率,寫回政策會做如下變化:

  • 當你調用 write 函數将資料寫入到檔案時,這時候不是真正的落盤,而是将寫入資料暫存在作業系統的記憶體緩沖區裡。
  • 待到緩沖區的空間被填滿、或者超過了指定的門檻值時候,才真正地将緩沖區中的資料寫入到磁盤裡面。
  • 這種做法顯然提高了效率,但也為寫入資料帶來了安全性問題,如果伺服器發生了單機,那麼儲存在記憶體緩沖區裡面的寫入資料就會丢失。
  • 為此,系統提供了fsync和fdatasync兩個同步函數,它們可以強制讓作業系統立即将緩沖區中的資料寫入到硬碟裡面,進而確定寫入資料的安全性。
  • Redis 提供的 AOF 配置項 appendfsync 寫回政策直接決定 AOF 持久化功能的效率和安全性,以下是 appendfsync 的3個枚舉:
  • always:同步寫回,寫指令執行完 即将緩沖區内容回寫到 AOF 檔案。
  • everysec:每秒寫回,寫指令執行完,日志寫到 AOF 檔案緩沖區,緩沖區每隔一秒再把内容同步到磁盤。
  • no:作業系統控制,寫執行執行完畢,把日志寫到 AOF 檔案記憶體緩沖區,由作業系統決定何時回寫到磁盤。

寫磁盤會帶來性能上的損耗,是以寫回的政策要根據實際情況做一個取舍,比如你是偏向性能還是可靠性。

always 同步寫回可以做到資料不丢失,但是每次執行寫指令都需要寫入磁盤,性能最差。

everysec 每秒寫回,避免了同步寫回的性能開銷,但是如果服務發生當機,會有大約1s時間周期的資料丢失,這種模式是在性能和可靠性之間做了妥協。

no 作業系統控制,執行寫指令後就寫入 AOF 檔案緩沖,再執行後續的寫磁盤指令,性能最好,但有可能丢失更多的資料。

3.4.2 寫回政策的選擇

我們可以根據服務的實際情況來抉擇政策,看是偏向高性能還是高可靠。

  • 高性能需求,選擇 No 政策
  • 高可靠性保證,就選擇 Always 政策
  • 如果能夠接受資料存在少量丢失,又希望性能較好的話,就選擇 Everysec 政策

4、混合RDF/AOF 方式模式

現實情況下,無論使用RDB或者AOF都差點意思。使用 rdb 來恢複記憶體狀态,勢必會丢失一部分資料。使用 AOF 日志重放,重放對性能有一定的影響,而且在 Redis 執行個體很大的情況下,需要花費很長的時間。

Redis 4.0 解決了這個問題,才用了一個新的持久化模式——混合持久化,該 混合模式 預設是關閉狀态的。

将 RDB 檔案的内容和 rdb快照時間點之後的增量的 AOF 日志檔案存在一起。這時候 AOF 日志不需要再是全量的日志,而是最近一次快照時間點之後到當下發生的增量 AOF 日志,通常這部分 AOF 日志很小。

是以執行有如下順序:

1、查找rdb内容,如果存在先加載 rdb内容再 重放剩餘的 aof。

2、沒有rdb内容,直接以aof格式重放整個檔案。

這樣快照就不用頻繁的執行,同時由于 AOF 隻需要記錄最近一次快照之後的資料,不需要記錄所有的操作,避免了出現單次重放檔案過大的問題。

深入淺出Redis:資料持久化提高可用性

5、總結

RDB提供了快照模式,記錄某個時間的Redis記憶體狀态。RDB設計了 bgsave 和寫時複制,盡可能避免執行快照期間對讀寫指令的影響,但是頻繁快照會給磁盤帶來壓力以及 fork 阻塞主線程。需把握頻率。

AOF 日志存儲了 Redis 服務的順序指令序列,通過重放(replay)指令來寫入日志檔案,并通過寫回政策來避免高頻讀寫給Redis帶來壓力。

RDB快照的照片時間間隔,必然會帶來資料缺失,如果允許分鐘級别的資料丢失,可以隻使用 RDB。

如果隻用 AOF,寫回政策優先使用 everysec 的配置選項,因為它在可靠性和性能之間取了一個平衡。

資料不能丢失時,記憶體快照和 AOF 的混合使用是一個很好的選擇。

為幫助開發者們提升面試技能、有機會入職BATJ等大廠公司,特别制作了這個專輯——這一次整體放出。

大緻内容包括了: Java 集合、JVM、多線程、并發程式設計、設計模式、Spring全家桶、Java、MyBatis、ZooKeeper、Dubbo、Elasticsearch、Memcached、MongoDB、Redis、MySQL、RabbitMQ、Kafka、Linux、Netty、Tomcat等大廠面試題等、等技術棧!

深入淺出Redis:資料持久化提高可用性

歡迎大家關注公衆号【Java爛豬皮】,回複【666】,擷取以上最新Java後端架構VIP學習資料以及視訊學習教程,然後一起學習,一文在手,面試我有。

每一個專欄都是大家非常關心,和非常有價值的話題,如果我的文章對你有所幫助,還請幫忙點贊、好評、轉發一下,你的支援會激勵我輸出更高品質的文章,非常感謝!

深入淺出Redis:資料持久化提高可用性

繼續閱讀