原始碼級別理解 Redis 持久化機制
文章首發於公眾號“蘑菇睡不著”,歡迎來訪~
前言
大家都知道 Redis 是一個記憶體資料庫,資料都儲存在記憶體中,這也是 Redis 非常快的原因之一。雖然速度提上來了,但是如果資料一直放在記憶體中,是非常容易丟失的。比如 伺服器關閉或宕機了,記憶體中的資料就木有了。為了解決這一問題,Redis 提供了 持久化 機制。分別是 RDB 以及 AOF 持久化。
RDB
什麼是 RDB 持久化?
RDB 持久化可以在指定的時間間隔內生成資料集的時間點快照(point-in-time snapshot)。
RDB 的優點?
- RDB 是一種表示某個即時點的 Redis 資料的緊湊檔案。RDB 檔案適用於備份。例如,你可能想要每小時歸檔最近24小時的 RDB 檔案,每天儲存近30天的 RDB 快照。這允許你很容易的恢復不同版本的資料集以容災。
- RDB 非常適合於災難恢復,作為一個緊湊的單一檔案,可以被傳輸到遠端的資料中心。
- RDB 最大化了 Redis 的效能。因為 Redis 父程序持久化時唯一需要做的是啟動(fork)一個子程序,由子程序完成所有剩餘的工作。父程序例項不需要執行像磁碟IO這樣的操作。
- RDB 在重啟儲存了大資料集的例項比 AOF 快。
RDB 的缺點?
- 當你需要在Redis停止工作(例如停電)時最小化資料丟失,RDB可能不太好。你可以配置不同的儲存點(save point)來儲存RDB檔案(例如,至少5分鐘和對資料集100次寫之後,但是你可以有多個儲存點)。然而,你通常每隔5分鐘或更久建立一個RDB快照,所以一旦Redis因為任何原因沒有正確關閉而停止工作,你就得做好最近幾分鐘資料丟失的準備了。
- RDB需要經常呼叫fork()子程序來持久化到磁碟。如果資料集很大的話,fork()比較耗時,結果就是,當資料集非常大並且CPU效能不夠強大的話,Redis會停止服務客戶端幾毫秒甚至一秒。AOF也需要fork(),但是你可以調整多久頻率重寫日誌而不會有損(trade-off)永續性(durability)。
RDB 檔案的建立與載入
有個兩個 Redis 命令可以用於生成 RDB 檔案,一個是 SAVE,另一個是 BGSAVE。
SAVE 命令會阻塞 Redis 伺服器程序,直到 RDB 檔案建立完畢為止,在伺服器程序阻塞期間,伺服器不能處理任何命令請求。
> SAVE // 一直等到 RDB 檔案建立完畢 OK
和 SAVE 命令直接阻塞伺服器程序不同的是,BGSAVE 命令會派生出一個子程序,然後由子程序負責建立 RDB 檔案,伺服器程序(父程序)繼續處理命令程序。
執行fork的時候作業系統(類Unix作業系統)會使用寫時複製(copy-on-write)策略,即fork函式發生的一刻父子程序共享同一記憶體資料,當父程序要更改其中某片資料時(如執行一個寫命令 ),作業系統會將該片資料複製一份以保證子程序的資料不受影響,所以新的RDB檔案儲存的是執行fork一刻的記憶體資料。
> BGSAVE // 派生子程序,並由子程序建立 RDB 檔案
Background saving started
生成 RDB 檔案由兩種方式:一種是手動,就是上邊介紹的用命令的方式;另一種是自動的方式。
接下來詳細介紹一下自動生成 RDB 檔案的流程。
Redis 允許使用者通過設定伺服器配置的 save 選項,讓伺服器每隔一段時間自動執行一次 BGSAVE 命令。
使用者可以通過在 redis.conf 配置檔案中的 SNAPSHOTTING 下 save 選項設定多個儲存條件,但只要其中任意一個條件被滿足,伺服器就會執行 BGSAEVE 命令。
如,以下配置:
save 900 1
save 300 10
save 60 10000
上邊三個配置的含義是:
- 伺服器在 900 秒內,對資料庫進行了至少 1 次修改。
- 伺服器在 300 秒內,對資料庫進行了至少 10 次修改。
- 伺服器在 60 秒內,對資料庫進行了至少 10000 次修改。
如果沒有手動去配置 save 選項,那麼伺服器會為 save 選項配置預設引數:
save 900 1
save 300 10
save 60 10000
接著,伺服器就會根據 save 選項的配置,去設定伺服器狀態 redisServer 結構的 saveparams 屬性:
struct redisServer{
// ...
// 記錄了儲存條件的陣列
struct saveparams *saveparams;
// ...
};
saveparams 屬性是一個數組,陣列中的每一個元素都是一個 saveparam 結構,每個 saveparam 結構都儲存了一個 save 選項設定的儲存條件:
struct saveparam {
// 秒數
time_t seconds;
// 修改數
int changes;
};
除了 saveparams 陣列之外,伺服器狀態還維持著一個 dirty 計數器,以及一個 lastsave 屬性;
struct redisServer {
// ...
// 修改計數器
long long dirty;
// 上一次執行儲存時間
time_t lastsave;
// ...
}
- dirty 計數器記錄距離上一次成功執行 SAVE 或 BGSAVE 命令之後,伺服器對資料庫狀態(伺服器中的所有資料庫)進行了多少次修改(包括寫入、刪除、更新等操作)。
- lastsave 屬性是一個 UNIX 時間戳,記錄了伺服器上一次執行 SAVE 或 BGSAVE 命令的時間。
檢查條件是否滿足觸發 RDB
Redis 的伺服器週期性操作函式 serverCron 預設每隔 100 毫秒執行一次,該函式用於對正在執行的伺服器進行維護,它的其中一項工作就是檢查 save 選項所設定的儲存條件是否已經滿足,如果滿足的話就執行 BGSAVE 命令。
Redis serverCron 原始碼解析如下:
程式會遍歷並檢查 saveparams 陣列中的所有儲存條件,只要有任意一個條件被滿足,伺服器就會執行 BGSAVE 命令。
下面是 rdbSaveBackground 的原始碼流程:
RDB 檔案結構
下圖展示了一個完整 RDB 檔案所包含的各個部分。
redis 檔案的最開頭是 REDIS 部分,這個部分的長度是 5 位元組,儲存著 “REDIS” 五個字元。通過這五個字元,程式可以在載入檔案時,快速檢查所載入的檔案是否時 RDB 檔案。
db_version 長度為 4 位元組,他的值時一個字串表示的整數,這個整數記錄了 RDB 檔案的版本號,比如 “0006” 就代表 RDB 檔案的版本為第六版。
database 部分包含著零個或任意多個數據庫,以及各個資料庫中的鍵值對資料:
- 如果伺服器的資料庫狀態為空(所有資料庫都是空的),那麼這個部分也為空,長度為 0 位元組。
- 如果伺服器的資料庫狀態為非空(有至少一個數據庫非空),那麼這個部分也為非空,根據資料庫所儲存鍵值對的數量、型別和內容不同,這個部分的長度也會有所不同。
EOF 常量的長度為 1 位元組,這個常量標誌著 RDB 檔案正文內容的結束,當讀入程式遇到這個值後,他知道所有資料庫的所有鍵值對已經載入完畢了。
check_sum 是一個 8 位元組長的無符號整數,儲存著一個校驗和,這個校驗和時程式通過對 REDIS、db_version、database、EOF 四個部分的內容進行計算得出的。伺服器在載入 RDB 檔案時,會將載入資料所計算出的校驗和與 check_sum 所記錄的校驗和進行對比,以此來檢查 RDB 是否有出錯或者損壞的情況。
舉個例子:下圖是一個 0 號資料庫和 3 號資料庫的 RDB 檔案。第一個就是 “REDIS” 表示是一個 RDB 檔案,之後的 “0006” 表示這是第六版的 REDIS 檔案,然後是兩個資料庫,之後就是 EOF 結束識別符號,最後就是 check_sum。
AOF 持久化
什麼是 AOF 持久化
AOF持久化方式記錄每次對伺服器寫的操作,當伺服器重啟的時候會重新執行這些命令來恢復原始的資料,AOF命令以redis協議追加儲存每次寫的操作到檔案末尾.Redis還能對AOF檔案進行後臺重寫,使得AOF檔案的體積不至於過大.
AOF 的優點?
- 使用AOF 會讓你的Redis更加耐久: 你可以使用不同的fsync策略:無fsync,每秒fsync,每次寫的時候fsync.使用預設的每秒fsync策略,Redis的效能依然很好(fsync是由後臺執行緒進行處理的,主執行緒會盡力處理客戶端請求),一旦出現故障,你最多丟失1秒的資料.
- AOF檔案是一個只進行追加的日誌檔案,所以不需要寫入seek,即使由於某些原因(磁碟空間已滿,寫的過程中宕機等等)未執行完整的寫入命令,你也也可使用redis-check-aof工具修復這些問題.
- Redis 可以在 AOF 檔案體積變得過大時,自動地在後臺對 AOF 進行重寫: 重寫後的新 AOF 檔案包含了恢復當前資料集所需的最小命令集合。 整個重寫操作是絕對安全的,因為 Redis 在建立新 AOF 檔案的過程中,會繼續將命令追加到現有的 AOF 檔案裡面,即使重寫過程中發生停機,現有的 AOF 檔案也不會丟失。 而一旦新 AOF 檔案建立完畢,Redis 就會從舊 AOF 檔案切換到新 AOF 檔案,並開始對新 AOF 檔案進行追加操作。
- AOF 檔案有序地儲存了對資料庫執行的所有寫入操作, 這些寫入操作以 Redis 協議的格式儲存, 因此 AOF 檔案的內容非常容易被人讀懂, 對檔案進行分析(parse)也很輕鬆。 匯出(export) AOF 檔案也非常簡單: 舉個例子, 如果你不小心執行了 FLUSHALL 命令, 但只要 AOF 檔案未被重寫, 那麼只要停止伺服器, 移除 AOF 檔案末尾的 FLUSHALL 命令, 並重啟 Redis , 就可以將資料集恢復到 FLUSHALL 執行之前的狀態。
AOF 的缺點?
- 對於相同的資料集來說,AOF 檔案的體積通常要大於 RDB 檔案的體積。
- 根據所使用的 fsync 策略,AOF 的速度可能會慢於 RDB 。 在一般情況下, 每秒 fsync 的效能依然非常高, 而關閉 fsync 可以讓 AOF 的速度和 RDB 一樣快, 即使在高負荷之下也是如此。 不過在處理巨大的寫入載入時,RDB 可以提供更有保證的最大延遲時間(latency)。
AOF持久化的實現
AOF持久化功能的實現可以分為命令追加(append)、檔案寫入、檔案同步(sync)三個步驟。
命令追加
當 AOF 持久化功能處於開啟狀態時,伺服器在執行完一個寫命令之後,會以協議格式將被執行的寫命令追加到伺服器狀態的 aof_buf 緩衝區的末尾。
struct redisServer {
// ...
// AOF 緩衝區
sds aof_buf;
// ..
};
如果客戶端向伺服器傳送以下命令:
> set KEY VALUE
OK
那麼伺服器在執行這個 set 命令之後,會將以下協議內容追加到 aof_buf 緩衝區的末尾;
*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n
AOF 檔案的寫入與同步
Redis的伺服器程序就是一個事件迴圈(loop),這個迴圈中的檔案事件負責接收客戶端
的命令請求,以及向客戶端傳送命令回覆,而時間事件則負責執行像 serverCron 函式這樣需
要定時執行的函式。
因為伺服器在處理檔案事件時可能會執行寫命令,使得一些內容被追加到aof_buf緩衝區
裡面,所以在伺服器每次結束一個事件迴圈之前,它都會呼叫 flushAppendOnlyFile 函式,考
慮是否需要將aof_buf緩衝區中的內容寫入和儲存到AOF檔案裡面,這個過程可以用以下偽代
碼錶示:
def eventLoop():
while True:
#處理檔案事件,接收命令請求以及傳送命令回覆
#處理命令請求時可能會有新內容被追加到 aof_buf緩衝區中
processFileEvents()
#處理時間事件
processTimeEvents()
#考慮是否要將 aof_buf中的內容寫入和儲存到 AOF檔案裡面
flushAppendOnlyFile()
flushAppendOnlyFile函式的行為由伺服器配置的 appendfsync 選項的值來決定,各個不同
值產生的行為如下表所示。
appendfsync 選項的值 | flushAppendOnlyFile 函式的行為 |
---|---|
always | 將 aof_buf 緩衝區中的所有內容寫入並同步到 AOF 檔案 |
everysec | 將 aof_buf 緩衝區中的所有內容寫入到 AOF 檔案,如果上次同步 AOF 檔案的時間距離現在超過一秒鐘,那麼再次對 AOF 檔案進行同步,並且這個同步操作是由一個執行緒專門負責執行的 |
no | 將 aof_buf 緩衝區中的所有內容寫入到 AOF 檔案,但並不對 AOF 檔案進行同步,何時同步由作業系統來決定 |
如果使用者沒有主動為appendfsync選項設定值,那麼appendfsync選項的預設值為everysec。
寫到這裡有的小夥伴可能會對上面說的寫入和同步含義弄混,這裡說一下:
寫入:將 aof_buf 中的資料寫入到 AOF 檔案中。
同步:呼叫 fsync 以及 fdatasync 函式,將 AOF 檔案中的資料儲存到磁碟中。
通俗地講就是,你要往一個檔案寫東西,寫的過程就是寫入,而同步則是將檔案儲存,資料落到磁碟上。
大家之前看文章的時候是不是大多都說 AOF 最多丟失一秒鐘的資料,那是因為 redis AOF 預設是 everysec 策略,這個策略每秒執行一次,所以 AOF 持久化最多丟失一秒鐘的資料。
AOF 檔案的載入與資料還原
因為AOF檔案裡面包含了重建資料庫狀態所需的所有寫命令,所以伺服器只要讀入並重新執行一遍AOF檔案裡面儲存的寫命令,就可以還原伺服器關閉之前的資料庫狀態。 Redis讀取AOF檔案並還原資料庫狀態的詳細步驟如下:
- 建立一個不帶網路連線的偽客戶端(fake client):因為Redis的命令只能在客戶端上 下文中執行,而載入AOF檔案時所使用的命令直接來源於AOF檔案而不是網路連線,所以服 務器使用了一個沒有網路連線的偽客戶端來執行AOF檔案儲存的寫命令,偽客戶端執行命令 的效果和帶網路連線的客戶端執行命令的效果完全一樣。
- 從AOF檔案中分析並讀取出一條寫命令。
- 使用偽客戶端執行被讀出的寫命令。
- 一直執行步驟2和步驟3,直到AOF檔案中的所有寫命令都被處理完畢為止。
當完成以上步驟之後,AOF檔案所儲存的資料庫狀態就會被完整地還原出來,整個過程 如下圖所示。
AOF 重寫
因為AOF持久化是通過儲存被執行的寫命令來記錄資料庫狀態的,所以隨著伺服器執行 時間的流逝,AOF檔案中的內容會越來越多,檔案的體積也會越來越大,如果不加以控制的 話,體積過大的AOF檔案很可能對Redis伺服器、甚至整個宿主計算機造成影響,並且AOF文 件的體積越大,使用AOF檔案來進行資料還原所需的時間就越多。
如 客戶端執行了以下命令是:
> rpush list "A" "B"
OK
> rpush list "C"
OK
> rpush list "D"
OK
> rpush list "E" "F"
OK
那麼光是為了記錄這個list鍵的狀態,AOF檔案就需要儲存四條命令。
對於實際的應用程度來說,寫命令執行的次數和頻率會比上面的簡單示例要高得多,所 以造成的問題也會嚴重得多。 為了解決AOF檔案體積膨脹的問題,Redis提供了AOF檔案重寫(rewrite)功能。通過該 功能,Redis伺服器可以建立一個新的AOF檔案來替代現有的AOF檔案,新舊兩個AOF檔案所 儲存的資料庫狀態相同,但新AOF檔案不會包含任何浪費空間的冗餘命令,所以新AOF檔案 的體積通常會比舊AOF檔案的體積要小得多。 在接下來的內容中,我們將介紹AOF檔案重寫的實現原理,以及BGREWEITEAOF命令 的實現原理。
雖然Redis將生成新AOF檔案替換舊AOF檔案的功能命名為“AOF檔案重寫”,但實際上, AOF檔案重寫並不需要對現有的AOF檔案進行任何讀取、分析或者寫入操作,這個功能是通 過讀取伺服器當前的資料庫狀態來實現的。
就像上面的情況,伺服器完全可以將這六條命令合併成一條。
> rpush list "A" "B" "C" "D" "E" "F"
除了上面列舉的列表鍵之外,其他所有型別的鍵都可以用同樣的方法去減少 AOF檔案中的命令數量。首先從資料庫中讀取鍵現在的值,然後用一條命令去記錄鍵值對,代替之前記錄這個鍵值對的多條命令,這就是AOF重寫功能的實現原理。
在實際中,為了避免在執行命令時造成客戶端輸入緩衝區溢位,重寫程式在處理列表、 雜湊表、集合、有序集合這四種可能會帶有多個元素的鍵時,會先檢查鍵所包含的元素數 量,如果元素的數量超過了redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD常量的值,那 麼重寫程式將使用多條命令來記錄鍵的值,而不單單使用一條命令。 在目前版本中,REDIS_AOF_REWRITE_ITEMS_PER_CMD常量的值為64,這也就是 說,如果一個集合鍵包含了超過64個元素,那麼重寫程式會用多條SADD命令來記錄這個集 合,並且每條命令設定的元素數量也為64個。
AOF 後臺重寫
AOF 重寫會執行大量的寫操作,這樣會影響主執行緒,所以redis AOF 重寫放到了子程序去執行。這樣可以達到兩個目的:
- 子程序進行AOF重寫期間,伺服器程序(父程序)可以繼續處理命令請求。
- 子程序帶有伺服器程序的資料副本,使用子程序而不是執行緒,可以在避免使用鎖的情況 下,保證資料的安全性。
但是有一個問題,當子程序重寫資料時,主程序依然在處理新的資料,這也就會造成資料不一致情況。
為了解決這種資料不一致問題,Redis伺服器設定了一個AOF重寫緩衝區,這個緩衝區在 伺服器建立子程序之後開始使用,當Redis伺服器執行完一個寫命令之後,它會同時將這個寫 命令傳送給AOF緩衝區和AOF重寫緩衝區,如下圖:
這也就是說,在子程序執行AOF重寫期間,伺服器程序需要執行以下三個工作:
- 執行客戶端發來的命令。
- 將執行後的寫命令追加到AOF緩衝區。
- 將執行後的寫命令追加到AOF重寫緩衝區。
這樣一來可以保證:
- AOF緩衝區的內容會定期被寫入和同步到AOF檔案,對現有AOF檔案的處理工作會如常 進行。
- 從建立子程序開始,伺服器執行的所有寫命令都會被記錄到AOF重寫緩衝區裡面。
當子程序完成AOF重寫工作之後,它會向父程序傳送一個訊號,父程序在接到該訊號之 後,會呼叫一個訊號處理函式,並執行以下工作:
- 將AOF重寫緩衝區中的所有內容寫入到新AOF檔案中,這時新AOF檔案所儲存的數 據庫狀態將和伺服器當前的資料庫狀態一致。
- 對新的AOF檔案進行改名,原子地(atomic)覆蓋現有的AOF檔案,完成新舊兩個 AOF檔案的替換。
這個訊號處理函式執行完畢之後,父程序就可以繼續像往常一樣接受命令請求了。
在整個AOF後臺重寫過程中,只有訊號處理函式執行時會對伺服器程序(父程序)造成 阻塞,在其他時候,AOF後臺重寫都不會阻塞父程序,這將AOF重寫對伺服器效能造成的影 響降到了最低。
Redis 混合持久化
Redis 還可以同時使用 AOF 持久化和 RDB 持久化。 在這種情況下, 當 Redis 重啟時, 它會優先使用 AOF 檔案來還原資料集, 因為 AOF 檔案儲存的資料集通常比 RDB 檔案所儲存的資料集更完整。但是 AOF 恢復比較慢,Redis 4.0 推出了混合持久化。
混合持久化: 將 rdb 檔案的內容和增量的 AOF 日誌檔案存在一起。這裡的 AOF 日誌不再是全量的日誌,而是 自持久化開始到持久化結束 的這段時間發生的增量 AOF 日誌,通常這部分 AOF 日誌很小。
於是在 Redis 重啟的時候,可以先載入 RDB 的內容,然後再重放增量 AOF 日誌就可以完全替代之前的 AOF 全量檔案重放,重啟效率因此大幅得到提升。
覺得文章不錯的話,小夥伴們麻煩點個贊、關個注、轉個發一下唄~你的支援就是我寫文章的動力。
更多精彩的文章請關注公眾號“蘑菇睡不著”。
你越主動就會越主動,我們下期見~