Redis 是一個內存數據庫,全部的數據都直接保存在內存中,那麼,一旦 Redis 進程異常退出,或服務器自己異常宕機,咱們存儲在 Redis 中的數據就憑空消失,再也找不到了。java
Redis 做爲一個優秀的數據中間件,一定是擁有本身的持久化數據備份機制的,redis 中主要有兩種持久化策略,用於將存儲在內存中的數據備份到磁盤上,而且在服務器重啓時進行備份文件重載。git
RDB 和 AOF 是 Redis 內部的兩種數據持久化策略,這是兩種不一樣的持久化策略,一種是基於內存快照,一種是基於操做日誌,那麼本篇就先來說講 RDB 這種基於內存快照的持久化策略。程序員
RDB(redis database),快照持久化策略。RDB 是 redis 默認的持久化策略,你能夠打開 redis.conf,默認會看到這三條配置。github
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 文件備份生成,與此同時,主進程依然能夠響應客戶端請求。redis
顯然,異步的 RDB 生成策略纔是主流,除了某些特殊狀況,相信不會有人會在生產環境中用 save 命令阻塞 redis 服務來生成 RDB 文件的。算法
以上咱們介紹的兩個命令,save 和 bgsave,這兩個命令須要咱們手動的在客戶端發送請求才能觸發,咱們叫作主動觸發。數據庫
而咱們以前匆匆介紹過的配置觸發,這種咱們叫作被動觸發,被動觸發有一些配置,下面咱們來看看。數組
一、save 配置bash
save 配置是一個很是重要的配置,它配置了 redis 服務器在什麼狀況下自動觸發 bgsave 異步 RDB 備份文件生成。服務器
基本語法格式:
save <seconds> <changes>
複製代碼
當 redis 數據庫在 秒內,數據庫中的 keys 發生了 次變化,那麼就會觸發 bgsave 命令的調用。
二、dbfilename 配置
dbfilename 配置項決定了生成的 RDB 文件名稱,默認配置爲 dump.rdb。
dbfilename dump.rdb
複製代碼
三、rdbcompression 配置
rdbcompression 配置的是 rdb 文件中壓縮啓用配置,基本語法格式:
rdbcompression yes(|no)
複製代碼
若是 rdbcompression 配置爲 yes,那麼即表明 redis 進行 RDB 文件生成中,若是遇到字符串對象而且其中的字符串值佔用超過 20 個字節,那麼就會對字符串進行 LZF 算法進行壓縮。
四、stop-writes-on-bgsave-error 配置
stop-writes-on-bgsave-error 配置了,若是進行 RDB 備份文件生成過程當中,遭遇錯誤,是否中止 redis 提供寫服務,以警示用戶 RDB 備份異常,默認是開啓狀態。
stop-writes-on-bgsave-error yes(|no)
複製代碼
五、dir 配置
dir 配置的是 rdb 文件存放的目錄,默認是當前目錄。
dir ./
複製代碼
六、rdbchecksum 配置
rdbchecksum 配置 redis 是否使用 CRC64 校驗算法校驗 RDB 文件是否發生損壞,默認開啓狀態,若是你須要提高性能,能夠選擇性關閉。
rdbchecksum yes(|no)
複製代碼
咱們 redisServer 結構體中有這麼兩個字段:
saveparams 結構定義以下:
struct saveparam {
time_t seconds; //秒數
int changes; //變動次數
};
複製代碼
相信你可以想到,上述配置文件中的 save 配置就對應了兩個參數,多少秒內數據庫發生了多少次的變動便觸發 bgsave。
映射到代碼就是咱們 saveparam 結構,每個 saveparam 結構都對應一行 save 配置,而最終會以 saveparam 數組的形式被讀取到 redisServer 中。
ps:介紹這個的目前是爲咱們稍後分析 RDB 文件生成的源碼實現作前置鋪墊。
除此以外,redisServer 數據結構中還有這麼兩個字段:
dirty 字段記錄了自上次成功備份 RDB 文件以後,包括 save 和 bgsave 命令,整個 redis 數據庫又發生了多少次修改。dirty_before_bgsave 字段能夠理解爲上一次 bgsave 命令備份時,數據庫總的修改次數。
還有一些跟持久化相關時間字段,上一次成功 RDB 備份的時間點,上一次 bgsave 命令開始執行時間等等。
下面咱們也粘貼粘貼源碼,分析分析看 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。
任何格式的文件都會有本身的編碼協議,Java 中的字節碼也好、圖片格式文件也好,咱們這裏的 RDB 文件也好,都是有本身的一套約定好的協議的,具體到每個字節位置該放什麼樣的字段數據,這都是約定俗成的,編碼的時候按協議寫入二進制,讀取的時候也按照協議讀取字段字節。
RDB 協議規定整個文件包括以下幾個字段:
其中,第一部分是固定的五個字節,redis 把它稱爲 Magic Number,固定的五個字符 「R」,「E」,「D」,「I」,「S」。
咱們在 redis 的 0 號數據庫中添加一個鍵值對,而後執行 save 命令生成 RDB 文件,接着打開這個二進制文件。
咱們用 od 命令,並以 ASCII 碼選項輸出二進制文件,你會發現前五個字節是咱們固定的 redis 這五個字符。
下一個字段 REDIS_VERSION 佔四個字節,描述當前 RDB 的版本,以上述爲例,redis-4.0 版本對應的 RDB 文件版本就是 0008。
下一個字段是 Aux Fields,官方稱輔助字段,是 RDB 7 之後加入的,主要包含如下這些字段信息:
接着就是 DATABASE 部分,這部分會存儲的咱們字典中的真實數據,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,標識當前數據庫容量,即有多少個鍵,咱們這裏只有一個鍵。
緊接着就是存咱們的鍵值對,這部分的格式以下:
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 這種持久化策略,再見!