Redis之所以快,一個最重要的原因在於它是直接將數據存儲在內存,並直接從內存中讀取數據的,因此一個絕對不容忽視的問題便是,一旦Redis服務器宕機,內存中的數據將會完全丟失。
好在Redis官方為我們提供了兩種持久化的機制,RDB和AOF,今天我們來聊一下RDB。
什麼是RDB
RDB是Redis的一種數據持久化到磁盤的策略,是一種以內存快照形式保存Redis數據的方式。
所謂快照,就是把某一時刻的狀態以文件的形式進行全量備份到磁盤,這個快照文件就稱為RDB文件,其中RDB是Redis DataBase的縮寫。
全量備份帶來的思考
備份會不會阻塞主線程
我們知道Redis為所有客戶端處理數據時使用的是單線程,這個模型就決定了使用者需要盡量避免進行會阻塞主線程的操作。
那麼Redis在生成RDB文件的時候,會不會阻塞主線程呢?
對此,Redis提供了兩個命令來生成RDB,一個SAVE,另一個是BGSAVE。
SAVE命令會阻塞Redis的主線程,直到RDB文件創建完成為止,在此期間,Redis不能處理客戶端的任何請求。
127.0.0.1:6379> SAVE
OK
與SAVE直接阻塞主線程的做法不同,BGSAVE命令會創建一個子進程,然後由子進程負責專門寫入RDB,主進程《父進程》繼續處理命令請求,不會被阻塞。
註:主進程其實會阻塞在fork()過程中,通常情況下該指令執行的速度比較快,對性能影響不大
127.0.0.1:6379> BGSAVE
Background saving started
RDB文件實際是由rdb.c/rdbSave函數進行創建的,SAVE命令和BGSAVE命令會以不同的方式調用這個函數,下面是兩個命令的偽代碼
void SAVE(){
# 創建RDB文件
rdbSave();
}
void BGSAVE(){
# 創建子進程
pid = fork();
if (pid==0){
# 子進程創建RDB
rdbSave();
# 創建完成之後向父進程發送信息
signal_parent();
}else if (pid>0){
# 父進程《主線程》繼續處理客戶端請求,並通過輪詢等待子進程的返回信號
handle_request_and_wait_signal();
}else{
# 處理異常
…
}
}
對時刻備份還是對時段備份
現在我們已經知道如何對Redis某一時刻的狀態進行全量備份了,需要重申的是,Redis保存的是某一時刻的全量數據,而不是某一時間段內的全量數據。
為什麼要執著於某一時刻的數據,一段時間內的數據不行嗎?還真就不行!因為一個時刻的數據反映了系統的該時刻的狀態。
例如在t1時刻,Redis保存的數據狀態為
t2時刻,Redis時刻的狀態為
如果Redis保存的是一段時間內的全量數據,則在這一段時間內,數據有如下幾種可能
隻有第一條能完美表征t1時刻的系統狀態,Redis進行數據恢復時至少能恢復到t1時刻的狀態,t1時刻之後的數據可通過其他方式《如之後會介紹到的持久化的另一種方式AOF)進行補充,而其餘3種數據對數據恢復沒有任何實際意義。
備份過程中,數據能否修改
為了實現備份某一時刻數據的這個目的,如果是我們來設計Redis,我們會怎麼做呢?
一個自然的想法就是拷貝某一個時刻的Redis完整內存數據。
這裡自然就是子進程對主進程的內存進行全量拷貝了,然而這對於Redis服務幾乎是災難性的,考慮以下兩個場景:
-
Redis中存儲了大量數據,fork()時拷貝內存數據會消耗大量時間和資源,會導致主進程一段時間的不可用
-
Redis占用了10G內存,而宿主機內存資源上限僅有16G,此時無法對Redis的數據進行持久化
因此備份過程中不能進行內存數據的全量拷貝。
接下來我們需要關注的問題是,在對內存數據進行快照的過程中,數據還能被修改嗎?這個問題至關重要,因為關系到Redis在快照過程中是否能正常處理寫請求。
舉個例子,我們在時刻t為Redis進行快照,假設被內存數據量是2GB,磁盤寫入帶寬是0.2GB/S,不考慮其他因素的情況下,至少需要10S《2/0.2=10》才能完全備份。
如果在時刻t+5S時,客戶發送了一個修改目前未被寫入內存的數據A的寫請求,被改成了A’,如果此時A’被寫入磁盤,就會破壞快照的完整性,因為我們期望獲得某一時刻的全量備份。
因此,快照過程中我們不希望有數據修改的操作。
但這意味著在快照期間Redis無法處理處理的寫操作,無疑會給義務服務帶來巨大影響。
而且我們知道Redis在快照期間是依然可以處理寫請求的,接下來我們來分析一下Redis是如何解決我們剛剛提出的兩個問題的。
Redis寫時復制《COW》
寫時復制聽起來非常的高端,嚇退了不少技術愛好者,其原理其實非常非常簡單,本質上就是『有寫操作的時候復制一份』,是不是很簡單?
註:寫時復制不是Redis自身的特性,而是操作系統提供的技術手段。
操作系統是一切技術的基礎,所有技術的革新都必須建立在操作系統支持的基礎上
Redis主進程fork生成的子進程可以共享主進程的所有內存數據,fork並不會帶來明顯的性能開銷,因為不會立刻對內存進行拷貝,它會將拷貝內存的動作推遲到真正需要的時候。
想象一下,如果主進程是讀取內存數據,那麼和BGSAVE子進程並不沖突。
如果主進程要修改Redis內存中某個數據《圖中數據C》,那麼操作系統內核會將被修改的內存數據復制一份《復制的是修改之前的數據》,未被修改的內存數據依然被父子兩個進程共享,被主進程修改的內存空間歸屬於主進程,被復制出來的原始數據歸屬於子進程。
如此一來,主進程就可以在快照發生的過程中肆無忌憚地接受數據寫入的請求,子進程也仍然能夠對某一時刻的內容做快照。
註:寫時復制是建立在短時間內寫請求不多的假設之下,如果寫請求的量非常巨大,那麼內存復制的壓力自然也不會小。
間隔自動備份
除了上文介紹的手動執行的SAVE和BGSAVE方法之外,Redis還提供了配置文件的方式,可以每隔一定時間自動執行一次BGSAVE方法。
例如,我們可以在Redis配置文件中設置如下參數《如果沒有主動設置save選項,則以下配置即為默認配置》
save 900 1
save 300 10
save 60 10000
那麼只要滿足以下3個條件之一,BGSAVE命令就會被執行
- 服務器在900秒內,對數據進行了至少1次的修改
- 服務器在300秒內,對數據進行了至少10次修改
- 服務器在60秒內,對數據進行了至少10000次修改
舉個例子,以下是Redis服務器在300秒內,對數據進行了至少10次修改之後,服務器自動進行BGSAVE命令時打印的日志
1:M 24 Nov 2021 07:02:28.081 * 10 changes in 300 seconds. Saving…
1:M 24 Nov 2021 07:02:28.082 * Background saving started by pid 22
22:C 24 Nov 2021 07:02:28.142 * DB saved on disk
22:C 24 Nov 2021 07:02:28.143 * RDB: 0 MB of memory used by copy-on-write
1:M 24 Nov 2021 07:02:28.183 * Background saving terminated with success
自動保存的原理
savaparams屬性
Redis會根據配置文件中設置的保存條件《或者未配置時的默認配置》,設置服務器狀態的redisServer的saveparams屬性
struct redisServer{
…
// 保存條件配置的數組
struct saveparam *saveparams;
…
}
saveparams是一個數組,數組中每個對象都是saveparam結構,saveparam結構如下所示,每個字段分別表征save選項的參數
struct saveparam{
// 秒數
time_t seconds;
// 修改次數
int changes;
}
以默認配置為例,Redis中saveparams存儲的數據結構將會如下所示
dirty計數器和lastsave屬性
除了saveparams參數之外,redisServer還有dirty和lastsave屬性
struct redisServer{
…
// 修改次數的計數器
long dirty;
// 上一次成功執行RDB快照的時間
time_t lastsave;
// 保存條件配置的數組
struct saveparam *saveparams;
…
}
- dirty屬性保存距離上次成功執行RDB快照之後,Redis對數據進行了多少次修改操作《包括寫入、更新、刪除》
- lastsave屬性記錄了Redis上一次成功執行RDB快照的時間,是一個UNIX時間戳
Redis每進行一次寫命令都會對dirty計數器進行更新,批量操作按多次進行計數,如
redis> SADD fruits apple banana orange
dirty計數器將會增加3
如上圖所示,dirty計數器的值為101,表示Redis自上次成功進行RDB快照之後,對數據庫一共進行了101次修改操作;lastsave屬性記錄了上次成功進行RDB快照的時間1638023962《2021-11-27 22:39:22)
周期性檢查保存條件
serverCron函數默認每隔100毫秒就會執行一次,該函數的其中一個作用就是檢查save命令設置的保存條件是否被滿足,是則執行BGSAVE命令。
偽代碼如下
void serverCron(){
…
for (saveparam in server.saveparams){
// 計算距離上次成功進行RDB快照多少時間
save_interval = unixtime_now() – server.lastsave;
// 如果距離上次快照時間超過條件設置時間 && 數據庫修改次數超過條件所設置的次數,則執行快照操作
if (save_interval > saveparam.seconds && server.dirty >= saveparam.changes){
BGSAVE()
}
}
…
}
再舉個例子,假設Redis的當前狀態如下圖
那麼當時間來到1638024263《1638023962之後的第301秒),由於滿足了saveparams數組的第2個保存條件——300S之內至少進行10次修改,Redis將會執行一次BGSAVE操作。
假設BGSAVE執行4S之後完成,則此時Redis的狀態將會更新為