Redis中的數據持久化策略(RDB)

NO IMAGE

Redis 是一個內存數據庫,所有的數據都直接保存在內存中,那麼,一旦 Redis 進程異常退出,或服務器本身異常宕機,我們存儲在 Redis 中的數據就憑空消失,再也找不到了。

Redis 作為一個優秀的數據中間件,必定是擁有自己的持久化數據備份機制的,redis 中主要有兩種持久化策略,用於將存儲在內存中的數據備份到磁盤上,並且在服務器重啟時進行備份文件重載。

RDB 和 AOF 是 Redis 內部的兩種數據持久化策略,這是兩種不同的持久化策略,一種是基於內存快照,一種是基於操作日誌,那麼本篇就先來講講 RDB 這種基於內存快照的持久化策略。

一、什麼是 RDB 持久化策略

RDB(redis database),快照持久化策略。RDB 是 redis 默認的持久化策略,你可以打開 redis.conf,默認會看到這三條配置。

Redis中的數據持久化策略(RDB)

save 900 1            900秒內執行一次set操作 則持久化1次  
save 300 10           300秒內執行10次set操作,則持久化1次
save 60 10000         60秒內執行10000次set操作,則持久化1次

RDB 又分為兩種,一種是同步的,調用 save 命令即可觸發 redis 進行 RDB 文件生成備份,但是這是一個同步命令,在備份完成之前,redis 服務器不響應客戶端任何請求。另一種是異步的,調用 bgsave 命令,redis 服務器 fork 一個子進程進行 RDB 文件備份生成,與此同時,主進程依然可以響應客戶端請求。

顯然,異步的 RDB 生成策略才是主流,除了某些特殊情況,相信不會有人會在生產環境中用 save 命令阻塞 redis 服務來生成 RDB 文件的。

以上我們介紹的兩個命令,save 和 bgsave,這兩個命令需要我們手動的在客戶端發送請求才能觸發,我們叫做主動觸發。

而我們之前匆匆介紹過的配置觸發,這種我們叫做被動觸發,被動觸發有一些配置,下面我們來看看。

1、save 配置

save 配置是一個非常重要的配置,它配置了 redis 服務器在什麼情況下自動觸發 bgsave 異步 RDB 備份文件生成。

基本語法格式:

save <seconds> <changes>

當 redis 數據庫在 秒內,數據庫中的 keys 發生了 次變化,那麼就會觸發 bgsave 命令的調用。

2、dbfilename 配置

dbfilename 配置項決定了生成的 RDB 文件名稱,默認配置為 dump.rdb。

dbfilename dump.rdb

3、rdbcompression 配置

rdbcompression 配置的是 rdb 文件中壓縮啟用配置,基本語法格式:

rdbcompression yes(|no)

如果 rdbcompression 配置為 yes,那麼即代表 redis 進行 RDB 文件生成中,如果遇到字符串對象並且其中的字符串值佔用超過 20 個字節,那麼就會對字符串進行 LZF 算法進行壓縮。

4、stop-writes-on-bgsave-error 配置

stop-writes-on-bgsave-error 配置了,如果進行 RDB 備份文件生成過程中,遭遇錯誤,是否停止 redis 提供寫服務,以警示用戶 RDB 備份異常,默認是開啟狀態。

stop-writes-on-bgsave-error yes(|no)

5、dir 配置

dir 配置的是 rdb 文件存放的目錄,默認是當前目錄。

dir ./

6、rdbchecksum 配置

rdbchecksum 配置 redis 是否使用 CRC64 校驗算法校驗 RDB 文件是否發生損壞,默認開啟狀態,如果你需要提升性能,可以選擇性關閉。

rdbchecksum yes(|no)

二、saveparams 和 dirty 計數器

我們 redisServer 結構體中有這麼兩個字段:

Redis中的數據持久化策略(RDB)

saveparams 結構定義如下:

struct saveparam {
time_t seconds;  //秒數
int changes;    //變更次數
};

相信你能夠想到,上述配置文件中的 save 配置就對應了兩個參數,多少秒內數據庫發生了多少次的變更便觸發 bgsave。

映射到代碼就是我們 saveparam 結構,每一個 saveparam 結構都對應一行 save 配置,而最終會以 saveparam 數組的形式被讀取到 redisServer 中。

ps:介紹這個的目前是為我們稍後分析 RDB 文件生成的源碼實現做前置鋪墊。

除此之外,redisServer 數據結構中還有這麼兩個字段:

Redis中的數據持久化策略(RDB)

dirty 字段記錄了自上次成功備份 RDB 文件之後,包括 save 和 bgsave 命令,整個 redis 數據庫又發生了多少次修改。dirty_before_bgsave 字段可以理解為上一次 bgsave 命令備份時,數據庫總的修改次數。

還有一些跟持久化相關時間字段,上一次成功 RDB 備份的時間點,上一次 bgsave 命令開始執行時間等等。

Redis中的數據持久化策略(RDB)

下面我們也粘貼粘貼源碼,分析分析看 redis 是如何進行 RDB 備份文件生成的。

int serverCron(....){
.....
//如果已經有子進程在執行 RDB 生成,或者 AOF 恢復,或者有子進程未返回
if (server.rdb_child_pid != -1 || server.aof_child_pid != -1 ||
ldbPendingChildren())
{
int statloc;
pid_t pid;
//查看這個進程是否返回信號
if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) {
int exitcode = WEXITSTATUS(statloc);
int bysignal = 0;
if (WIFSIGNALED(statloc)) bysignal = WTERMSIG(statloc);
//持久化異常,打印日誌
if (pid == -1) {
serverLog(LL_WARNING,"wait3() returned an error: %s. "
"rdb_child_pid = %d, aof_child_pid = %d",
strerror(errno),
(int) server.rdb_child_pid,
(int) server.aof_child_pid);
} else if (pid == server.rdb_child_pid) {
//成功持久化 RDB 文件,調用方法用心的RDB文件覆蓋舊的RDB文件
backgroundSaveDoneHandler(exitcode,bysignal);
if (!bysignal && exitcode == 0) receiveChildInfo();
} else if (pid == server.aof_child_pid) {
//成功執行 AOF,替換現有的 AOF文件
backgroundRewriteDoneHandler(exitcode,bysignal);
if (!bysignal && exitcode == 0) receiveChildInfo();
} else {
//子進程成功,但返回的 pid 類型異常,無法匹配
if (!ldbRemoveChild(pid)) {
serverLog(LL_WARNING,
"Warning, detected child with unmatched pid: %ld",
(long)pid);
}
}
//如果子進程未結束,不允許字典進行 rehash
updateDictResizePolicy();
closeChildInfoPipe();
}
} else{.......}
}

serverCron 每隔一百毫秒執行一次(可能後續的 redis 版本有所區別,本文基於 4.0),都會首先去判斷 RDB 或 AOF 子進程是否成功完成,如果成功會進行舊文件替換覆蓋操作等。我們繼續看 else 部分。

int serverCron(....){
.....
if (server.rdb_child_pid != -1 || server.aof_child_pid != -1 ||
ldbPendingChildren())
{
..........
}
else{
//如果未有子進程做 RDB 文件生成
//遍歷 saveparams 數組,取出我們配置文件中的 save 配置項
for (j = 0; j < server.saveparamslen; j++) {
struct saveparam *sp = server.saveparams+j;
//根據我們之前介紹的 dirty 計數器判斷 save 配置條件是否滿足
if (server.dirty >= sp->changes &&
server.unixtime-server.lastsave > sp->seconds &&
(server.unixtime-server.lastbgsave_try >
CONFIG_BGSAVE_RETRY_DELAY ||
server.lastbgsave_status == C_OK))
{
//記錄日誌
serverLog(LL_NOTICE,"%d changes in %d seconds. Saving...",
sp->changes, (int)sp->seconds);
rdbSaveInfo rsi, *rsiptr;
rsiptr = rdbPopulateSaveInfo(&rsi);
//核心方法,進行 RDB 文件生成
rdbSaveBackground(server.rdb_filename,rsiptr);
break;
}
}
//AOF 下篇我們在介紹,本篇看 RDB
if (server.aof_state == AOF_ON &&
server.rdb_child_pid == -1 &&
server.aof_child_pid == -1 &&
server.aof_rewrite_perc &&
server.aof_current_size > server.aof_rewrite_min_size)
{
long long base = server.aof_rewrite_base_size ?
server.aof_rewrite_base_size : 1;
long long growth = (server.aof_current_size*100/base) - 100;
if (growth >= server.aof_rewrite_perc) {
serverLog(LL_NOTICE,"Starting automatic rewriting of AOF on %lld%% growth",growth);
rewriteAppendOnlyFileBackground();
}
}
}
}

如果未有子進程進行 RDB 文件生成,那麼遍歷循環我們的 save 配置項是否滿足,如果滿足則調用 rdbSaveBackground 進行真正的 RDB 文件生成。我們繼續看看這個核心方法:

int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {
pid_t childpid;
long long start;
if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR;
server.dirty_before_bgsave = server.dirty;
server.lastbgsave_try = time(NULL);
openChildInfoPipe();
start = ustime();
if ((childpid = fork()) == 0) {
int retval;
closeListeningSockets(0);
redisSetProcTitle("redis-rdb-bgsave");
retval = rdbSave(filename,rsi);
if (retval == C_OK) {
size_t private_dirty = zmalloc_get_private_dirty(-1);
if (private_dirty) {
serverLog(LL_NOTICE,
"RDB: %zu MB of memory used by copy-on-write",
private_dirty/(1024*1024));
}
server.child_info_data.cow_size = private_dirty;
sendChildInfo(CHILD_INFO_TYPE_RDB);
}
exitFromChild((retval == C_OK) ? 0 : 1);
} else {
server.stat_fork_time = ustime()-start;
server.stat_fork_rate = (double) zmalloc_used_memory() * 1000000 / server.stat_fork_time / (1024*1024*1024); /* GB per second. */
latencyAddSampleIfNeeded("fork",server.stat_fork_time/1000);
if (childpid == -1) {
closeChildInfoPipe();
server.lastbgsave_status = C_ERR;
serverLog(LL_WARNING,"Can't save in background: fork: %s",
strerror(errno));
return C_ERR;
}
serverLog(LL_NOTICE,"Background saving started by pid %d",childpid);
server.rdb_save_time_start = time(NULL);
server.rdb_child_pid = childpid;
server.rdb_child_type = RDB_CHILD_TYPE_DISK;
updateDictResizePolicy();
return C_OK;
}
return C_OK; 
}

rdbSaveBackground 核心的是 fork 函數和 rdbSave 函數的調用。fork 函數其實是一個系統調用,他會複製出一個子進程出來,子進程和父進程幾乎一模一樣的內存數據。

fork 函數是阻塞的,當子進程複製出來後,程序的後續代碼段會由父子進程同時執行,也就是說,fork 之後,接下來的代碼,父子進程會併發執行,但系統不保證執行順序。

父進程中,fork 函數返回值等於子進程的進程 id,子進程中 fork 函數返回值等於零。

所以,rdbSaveBackground 函數的核心邏輯也就很清晰了,fork 成功之後,子進程調用 rdbSave 進行 RDB 文件寫入,併產生一個“temp-%d.rdb”的臨時文件,而父進程記錄一些日誌信息、子進程進程號,時間等信息。

至於 rdbSave 函數是怎麼寫入 RDB 文件的,這個也很簡單,RDB 文件是有固定的協議規範的,程序只要按照協議寫入數據即可,關於這個協議,我們等下詳細說它。

總結一下,serverCron 這個定期執行的函數,會將配置文件中的 save 配置進行讀取,並判斷條件是否滿足,如果條件滿足則調用 rdbSaveBackground 函數 fork 出一個子進程完成 RDB 文件的寫入,生成臨時文件,並確保臨時文件寫入成功後,再替換舊 RDB 文件,最後退出子進程。

ps:fork 函數複製出來的子進程一定要記得退出,否則每一次主進程都會複製一個子進程,最終導致服務 OOM。

RDB 文件結構分析

任何格式的文件都會有自己的編碼協議,Java 中的字節碼也好、圖片格式文件也好,我們這裡的 RDB 文件也好,都是有自己的一套約定好的協議的,具體到每一個字節位置該放什麼樣的字段數據,這都是約定俗成的,編碼的時候按協議寫入二進制,讀取的時候也按照協議讀取字段字節。

RDB 協議規定整個文件包括如下幾個字段:

Redis中的數據持久化策略(RDB)

其中,第一部分是固定的五個字節,redis 把它稱為 Magic Number,固定的五個字符 “R”,“E”,“D”,“I”,“S”。

Redis中的數據持久化策略(RDB)

我們在 redis 的 0 號數據庫中添加一個鍵值對,然後執行 save 命令生成 RDB 文件,接著打開這個二進制文件。

Redis中的數據持久化策略(RDB)

我們用 od 命令,並以 ASCII 碼選項輸出二進制文件,你會發現前五個字節是我們固定的 redis 這五個字符。

下一個字段 REDIS_VERSION 佔四個字節,描述當前 RDB 的版本,以上述為例,redis-4.0 版本對應的 RDB 文件版本就是 0008。

下一個字段是 Aux Fields,官方稱輔助字段,是 RDB 7 以後加入的,主要包含以下這些字段信息:

  1. redis-ver:版本號
  2. redis-bits:OS Arch
  3. ctime:RDB文件創建時間
  4. used-mem:使用內存大小
  5. repl-stream-db:在server.master客戶端中選擇的數據庫
  6. repl-id:當前實例 replication ID
  7. repl-offset:當前實例複製的偏移量

接著就是 DATABASE 部分,這部分會存儲的我們字典中的真實數據,redis 中多個數據庫,生成 RDB 文件的時候只會對有數據的數據庫進行寫入,而這部分的格式如下:

Redis中的數據持久化策略(RDB)

對應到我們上述例子中,就是這一部分:

Redis中的數據持久化策略(RDB)

我們的 rdb.h 文件頭中有這麼一些常量的定義:

#define RDB_OPCODE_AUX        250
#define RDB_OPCODE_RESIZEDB   251
#define RDB_OPCODE_EXPIRETIME_MS 252
#define RDB_OPCODE_EXPIRETIME 253
#define RDB_OPCODE_SELECTDB   254
#define RDB_OPCODE_EOF        255

十六進制 fe 轉換成十進制就是 254,對應的就是 RDB_OPCODE_SELECTDB,標識即將打開某數據庫,所以其後跟著的就是即將要打開的數據庫編號,我們這裡是零號數據庫。

十六進制 fb 轉換成十進制就是 251,對應的就是 RDB_OPCODE_RESIZEDB,標識當前數據庫容量,即有多少個鍵,我們這裡只有一個鍵。

緊接著就是存我們的鍵值對,這部分的格式如下:

Redis中的數據持久化策略(RDB)

type 佔一個字節標識當前鍵值對的類型,即對象類型,有如下可選類型:

#define RDB_TYPE_STRING 0
#define RDB_TYPE_LIST   1
#define RDB_TYPE_SET    2
#define RDB_TYPE_ZSET   3
#define RDB_TYPE_HASH   4
#define RDB_TYPE_ZSET_2 5 
#define RDB_TYPE_MODULE 6
#define RDB_TYPE_MODULE_2 7 
/* Object types for encoded objects. */
#define RDB_TYPE_HASH_ZIPMAP    9
#define RDB_TYPE_LIST_ZIPLIST  10
#define RDB_TYPE_SET_INTSET    11
#define RDB_TYPE_ZSET_ZIPLIST  12
#define RDB_TYPE_HASH_ZIPLIST  13
#define RDB_TYPE_LIST_QUICKLIST 14

key 始終是字符串,由字符串長度前綴加上自身內容構成,後跟 value 的內容。

EOF 字段標識 RDB 文件的結尾,佔一個字節,並固定值等於 255 也就是十六進制 ff,這是能從 rdb.h 文件頭中找到的。

CHECK_SUM 字段存儲的是 RDB 文件的校驗和,佔八個字節,用於校驗 RDB 文件是否損壞。

以上,我們就簡單介紹了 RDB 文件的構成,其實也只是點到為止啊,每一種類型的對象進行編碼的時候都是不一樣的,還要一些壓縮對象的手法等等等等,我們這裡也不可能全部詳盡。

總的來說,對 RDB 文件構成有個基本瞭解就行,實際上也很少有人沒事去分析 RDB 文件裡的數據的,即便是有也是通過工具進行分析的,比如 rdb-tools 等,人工分析也太炸裂了。

好了,關於 RDB 我們就簡單介紹到這,下一篇我們研究研究 AOF 這種持久化策略,再見!


關注公眾不迷路,一個愛分享的程序員。

公眾號回覆「1024」加作者微信一起探討學習!

每篇文章用到的所有案例代碼素材都會上傳我個人 github

github.com/SingleYam/o…

歡迎來踩!

Redis中的數據持久化策略(RDB)

相關文章

值得期待的JavaScript新特性

手把手教你做最好的電商搜索引擎(1)類目預測

二叉樹遍歷Java(遞歸+迭代):前序、中序和後續遍歷(雙棧法+Deque法)

Go如何解析JSON數據?