1. 程式人生 > >一起學Redis(9)——RDB持久化

一起學Redis(9)——RDB持久化

Redis是一個鍵值對資料庫伺服器,伺服器中包含著任意個非空資料庫,而每個非空資料庫中又可以包含任意個鍵值對,為了方便,我們把伺服器中的非空資料庫以及它們的鍵值對統稱為資料庫狀態。

因為redis是記憶體資料庫,如果不想辦法將儲存在記憶體中的資料庫狀態儲存到磁盤裡面,那麼一旦伺服器程序退出,伺服器中的資料庫狀態也會消失不見。為了解決這個問題,Redis提供了RDB持久化功能,這個功能可以將Redis在記憶體中的資料庫狀態儲存到磁盤裡面,避免資料意外丟失。RDB持久化可以手動執行,也可以根據伺服器配置選項定期執行,該功能可以將某個時間點上的資料庫狀態儲存到一個RDB檔案中。

RDB檔案的建立和載入

有兩個Redis命令可以用於生成RDB檔案,一個是SAVE,另一個是BGSAVE。
SAVE命令會阻塞Redis伺服器程序,知道RDB檔案建立完畢為止,在伺服器程序阻塞期間,伺服器不會處理任何命令請求。和SAVE命令直接阻塞伺服器程序不同,BGSAVE命令會派生出一個子程序,然後由子程序負責建立RDB檔案,伺服器程序(父程序)繼續處理命令請求。

redis-> SAVE             //等待直到RDB檔案建立完畢
OK
redis-> BGSAVE           //派生子程序,並由子程序建立RDB檔案
Background saving started

建立RDB檔案的實際工作由rdb.c/rdbSave函式完成,SAVE 和 BGSAVE會以不同的方式呼叫這個函式。

RDB檔案的載入工作是在伺服器啟動時自動執行的,所以Redis並沒有專門用於載入RDB檔案的命令,只要Redis伺服器在啟動時檢測到RDB檔案存在,它就會自動載入RDB檔案。

需要注意的是,因為AOF檔案的更新頻率通常比RDB檔案的更新頻率高,所以如果伺服器開啟了AOF持久化功能,那麼伺服器會優先使用AOF檔案來還原資料庫狀態;只有AOF功能關閉的時候,伺服器才會使用RDB檔案來還原資料庫狀態。載入RDB由rdb.c/rdbLoad函式完成。

資料庫檔案的載入圖示如下:

image

SAVE/BGSAVE 命令執行時的伺服器狀態

SAVE命令執行時,伺服器會被阻塞,客戶端所有命令請求都會被拒絕。

BGSAVE命令執行期間,子程序建立RDB檔案,Redis伺服器仍然可以繼續處理客戶端的命令請求,但是伺服器處理SAVE、BGSAVE、BGREWRITEAOF三個命令的方式會和平時有所不同。首先,在BGSAVE命令執行期間,客戶端傳送的SAVE命令會被伺服器拒絕,伺服器禁止SAVE命令和BGSAVE命令同時執行是為了避免父程序(伺服器程序)和子程序同時執行兩個rdbSave呼叫,防止產生競爭條件。其次,在BGSAVE命令執行期間,客戶端傳送的BGSAVE命令會被伺服器拒絕,因為同時執行兩個BGSAVE命令也會產生競爭條件。最後,BGREWRITEAOF 和 BGSAVE另個命令不能同時執行,

  • 如果BGSAVE 命令正在執行,那麼客戶端傳送的BGREWRITEAOF 命令會被延遲到BGSAVE命令執行完畢之後執行
  • 如果BGREWRITEAOF 命令正在執行,那麼客戶端傳送的BGSAVE 命令會被伺服器拒絕。

因為BGSAVE 和 BGREWRITEAOF 兩個命令的實際工作都由子程序執行,所以這兩個命令在操作方面並沒有衝突的地方,不能同時執行只是一個性能方面的考慮,併發出兩個程序,並且都執行大量的磁碟寫入操作,對效能影響比較大。

RDB 檔案載入時的伺服器狀態

伺服器載入 RDB 檔案時,會一直處於阻塞狀態,直到載入工作完成為止。

自動間隔性儲存

因為BGSAVE命令可以在不阻塞伺服器程序的情況下執行,所以Redis允許使用者通過設定伺服器配置的save選項,讓伺服器每隔一段時間自動執行一次BGSAVE命令。使用者可以通過save選項設定多個儲存條件,但只要其中任意一個被滿足,伺服器就會執行BGSAVE命令。

save 900 1
save 300 10
save 60 10000

如果像上面這樣配置,那麼只要滿足下面三個條件的任意一個,BGSAVE就會被執行:

  1. 伺服器在900秒之內,對資料庫進行了1次修改
  2. 伺服器在300秒之內,對資料庫進行了10次修改
    3, 伺服器在60秒之內,對資料庫進行了至少10000次修改
設定儲存條件

當redis伺服器啟動時,使用者可以通過指定配置檔案或者傳入啟動引數的方式設定save選項,如果使用者沒有主動設定save選項,那麼伺服器會為save選項設定預設條件。

接著,伺服器程式會根據save選項所設定的儲存條件,設定伺服器狀態rediServer結構的saveparams屬性:

struct redisServer {
    /* RDB persistence */
    long long dirty;                /* Changes to DB from the last save */
    long long dirty_before_bgsave;  /* Used to restore dirty on failed BGSAVE */
    pid_t rdb_child_pid;            /* PID of RDB saving child */
    struct saveparam *saveparams;   /* Save points array for RDB */
    int saveparamslen;              /* Number of saving points */
    char *rdb_filename;             /* Name of RDB file */
    int rdb_compression;            /* Use compression in RDB? */
    int rdb_checksum;               /* Use RDB checksum? */
    time_t lastsave;                /* Unix time of last successful save */
    time_t lastbgsave_try;          /* Unix time of last attempted bgsave */
    time_t rdb_save_time_last;      /* Time used by last RDB save run. */
    time_t rdb_save_time_start;     /* Current RDB save start time. */
    int rdb_bgsave_scheduled;       /* BGSAVE when possible if true. */
    int rdb_child_type;             /* Type of save by active child. */
    int lastbgsave_status;          /* C_OK or C_ERR */
    int stop_writes_on_bgsave_err;  /* Don't allow writes if can't BGSAVE */
    int rdb_pipe_write_result_to_parent; /* RDB pipes used to return the state */
    int rdb_pipe_read_result_from_child; /* of each slave in diskless SYNC. */
};

該屬性是一個數組,陣列中的每個元素都是一個saveparam結構,每個saveparam結構都儲存了save選項設定的儲存條件:

struct saveparam {
    time_t seconds;
    int changes;
}
dirty計數器和lastsave屬性

除了saveparam陣列之外,伺服器狀態還維持著一個dirty計數器,以及一個lastsave屬性:

  • dirty計數器記錄距離上一次成功執行SAVE命令或者BGSAVE命令之後,伺服器對資料庫狀態(伺服器中所有資料庫)進行了多少次修改(包括寫入、刪除、更新等操作)。
  • lastsave屬性是一個unix時間戳,記錄了伺服器上一次成功執行SAVE命令或者BGSAVE命令的時間。

當程式成功執行一個數據庫修改命令後,程式就會對dirty計數器進行更新,命令改了多少次資料庫,dirty計數器的值就會增加多少。

檢查儲存條件是否滿足

Redis的伺服器週期性操作函式serverCron預設每隔100毫秒就會執行一次,該函式用於對正在執行的伺服器進行維護,它的其中一項工作就是檢查save選項所設定的儲存條件是否已經滿足,如果滿足的化,就執行BGSAVE命令。

以下虛擬碼展示了serverCron函式檢查儲存條件的過程:

def serverCron():
    # ...
    # 遍歷所有儲存條件
    for saveparam in server.saveparams:
        save_interval = unixtime_now() - server.lastsave
        if server.dirty >= savaparam.changes and save_interval > saveparam.seconds:
            BGSAVE()

程式會遍歷並檢查saveparam陣列中的所有儲存條件,只要有任意一個滿足條件,那麼伺服器就會執行BGSAVE命令。

RDB檔案結構

REDIS db_version databases EOF check_sum

常量用全大寫字元表示,變數和資料用全小寫字母表示。RDB檔案最開頭是REDIS部分,這個部分的長度為5位元組,儲存著”REDIS”五個字元。程式通過這個5個字元,
檢查該檔案為RDB檔案。db_version長度為4位元組,它的值是一個字串表示的整數,記錄RDB檔案的版本號。databases包含著零個或多個數據庫,以及各個資料庫中的鍵值對資料。
+ 如果伺服器狀態為空,那麼這個部分也為空,長度0位元組
+ 如果伺服器狀態不為空,那麼這個部分根據資料庫所儲存的鍵值對數量、型別和內容不同,長度也有所不同

EOF為常量,一個位元組,標誌RDB檔案正文內容的結束。check_sum是一個8位元組長的無符號整數,儲存著一個校驗和,通過前面四部分內容計算得出,在載入RDb檔案時,會將載入資料的檢驗和和RDB檔案中記錄
的校驗和進行比對,以此來檢查RDB檔案是否有出錯或者損壞的情況。

databases部分

一個RDB檔案的databases部分可以儲存任意多個非空資料庫。每個非空資料庫在RDB檔案中都可以儲存為 SELECTDB、db_number、key_value_pairs 三個部分。

SELECTDB db_number key_value_pairs

SELECTDB常量的長度為1位元組,當讀入程式遇到這個值的時候,它知道接下來要讀入的將是一個數據庫號碼。db_number儲存著一個數據庫號碼,根據號碼的大小不同,這個部分的長度可以是1位元組、2位元組或者5位元組。
當程式讀入db_number部分之後,伺服器會呼叫SELECT命令,根據讀入的資料庫號碼進行資料庫切換,使得讀入的鍵值對可以載入到正確的資料庫中。key_value_pairs部分儲存了資料庫中所有鍵值對資料,如果
鍵值對帶有過期時間,那麼過期時間也會和鍵值對儲存在一起。根據鍵值對的數量、型別、內容以及是否有過期時間,這個部分的長度有所不同。

key_value_pairs部分
TYPE key value

不帶過期時間由上面所示的三部分組成,帶過期時間的如下:

EXPIRETIME_MS ms TYPE key value

EXPIRETIME_MS 為長度1位元組的常量,告知讀入程式,下面是以毫秒為單位的過期時間。ms是一個8位元組的帶符號整數,毫秒為單位的Unix時間戳。TYPE 長度1位元組,是 REDIS_RDB_TYPE_* 中的一種,*可以為
STRING、LIST、SET、ZSET、HASH、LIST_ZIPLIST、SET_INTSET、ZSET_ZIPLIST、HASH_ZIPLIST。每個type常量都代表了物件型別或者底層編碼,當伺服器讀入RDB檔案中的鍵值對資料時,程式會根據TYPE的值來
決定如何讀入和解釋value的資料。key總是字串物件,value則是具體的值物件。

value 部分

具體value對應的各個物件的 RDB 檔案內容結構這裡不展開介紹,有興趣的可以檢視redis官方手冊。其中key/value的生成的RDB檔案可以通過redis配置選項:rdbcompression yes/no 控制 RDB 檔案是否使用LZF演算法
進行壓縮,主要是對字串物件壓縮。

分析RDB檔案

Redis自身帶有RDB檔案檢查攻速 redis-check-dump,網上也能找到很多處理RDB檔案的工具,所以人工分析RDB檔案的意義不大,工作學習中如果有這樣的需求,可以直接使用工具分析。