Redis持久化之RDB


本文及后續文章,Redis版本均是v3.2.8

 

上篇文章介紹了RDB的優缺點,我們先來回顧下RDB的主要原理,在某個時間點把內存中所有數據保存到磁盤文件中,這個過程既可以通過人工輸入命令執行,也可以讓服務器周期性執行。

RDB持久化機制RDB的實現原理,涉及的文件為rdb.hrdb.c

 

一、初始RDB

先在Redis客戶端中執行以下命令,存入一些數據:

127.0.0.1:6379> flushdb

OK

127.0.0.1:6379> set city "beijing"

OK

127.0.0.1:6379> save

OK

127.0.0.1:6379>

 

Redis提供了save和bgsave兩個命令來生成RDB文件(即將內存數據寫入RDB文件中),執行成功后我們在磁盤中找到該RDB文件(dump.rdb),該文件存放的內容如下:

REDIS0007?redis-ver3.2.100?redis-bits繞?ctime聥阨Y?used-mem鑼?

 

我們再來看下Redis Server的版本號

RDB文件中存放的是二進制數據,從上面的文件非亂碼的內容中我們大概可以看出里面存放的各個類型的數據信息。下面我們就來介紹一下RDB的文件格式。

 

二、RDB文件結構

我們先大致看下RDB文件結構

 

1、RDB文件結構

我們看下圖中的各部分含義:

名稱 大小 說明
REDIS 5bytes 固定值,存放’R’,’E’,’D’,’I’,’S’
RDB_VERSION 4bytes

RDB版本號,在rdb.h頭文件中定義

/* The current RDB version. When the format changes in a way that is no longer backward compatible this number gets incremented. */

#define RDB_VERSION 7

DB-DATA —— 存儲真正的數據
RDB_OPCODE_EOF 1byte

255(0377),表述數據庫結束,

在rdb.h頭文件中定義

#define RDB_OPCODE_EOF        

255

checksum —— 校驗和

 

2、DB-DATA結構

名稱 大小 說明
RDB_OPCODE_SELECTDB 1byte

以前我們介紹過,當redis 服務器初始化時,會預先分配 16 個數據庫。這里我們需要將非空的數據庫信息保存在RDB文件中。

在rdb.h頭文件中定義

#define RDB_OPCODE_SELECTDB   254

db_number

1,2,5bytes

存儲數據庫的號碼。

db編號即對應的數據庫編號,每個db編號后邊到下一個RDB_OPCODE_SELECTDB標識符出現之前的所有數據都是該db下的數據。在REDIS加載 RDB 文件時,會根據這個域的值切換到相應的數據庫,以確保數據被還原到正確的數據庫中去。

key_value_pairs —— 主要數據

3、key_value_pairs結構

  • 帶過期時間

名稱 大小 說明
RDB_OPCODE_EXPIRETIME_MS 1byte 252,說明是帶過期時間的鍵值對
timestamp 8bytes 以毫秒為單位的時間戳
TYPE 8bytes 以毫秒為單位的時間戳
key ———
value ———

 

  • 不帶過期時間

名稱 大小 說明
TYPE 8bytes 以毫秒為單位的時間戳
key ———
value ———

 

TYPE的值,目前Redis主要有以下數據類型:

/* Dup object types to RDB object types. Only reason is readability (are we

 * dealing with RDB types or with in-memory object types?). */

#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

/* NOTE: WHEN ADDING NEW RDB TYPE, UPDATE rdbIsObjectType() BELOW */

 

/* 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

/* NOTE: WHEN ADDING NEW RDB TYPE, UPDATE rdbIsObjectType() BELOW */

 

 

4、RDB_OPCODE_EOF

標識數據庫部分的結束符,定義在rdb.h文件中:

#define RDB_OPCODE_EOF        255

 

5、rdb_checksum

RDB 文件所有內容的校驗和, 一個 uint_64t 類型值。

Redis在寫入RDB文件時將校驗和保存在RDB文件的末尾, 當讀取RDB時, 根據它的值對內容進行校驗。

如果Redis未開啟校驗功能,則該域的值為0。

#define CONFIG_DEFAULT_RDB_CHECKSUM 1

 

三、長度編碼

在RDB文件中有很多地方需要存儲長度信息,如字符串長度、list長度等等。如果使用固定的int或long類型來存儲該信息,在長度值比較小的時候會造成較大的空間浪費。為了節省空間,Redis設計了一套特殊的方法對長度進行編碼后再存儲。我們先來看下定義的編碼說明:

/* Defines related to the dump file format. To store 32 bits lengths for short

 * keys requires a lot of space, so we check the most significant 2 bits of

 * the first byte to interpreter the length:

 *

 * 00|000000 => if the two MSB are 00 the len is the 6 bits of this byte

 * 01|000000 00000000 =>  01, the len is 14 byes, 6 bits + 8 bits of next byte

 * 10|000000 [32 bit integer] => if it's 01, a full 32 bit len will follow

 * 11|000000 this means: specially encoded object will follow. The six bits

 *           number specify the kind of object that follows.

 *           See the RDB_ENC_* defines.

 *

 * Lengths up to 63 are stored using a single byte, most DB keys, and may

 * values, will fit inside. */

 

編碼方式 占用字節數 說明
00|000000 1byte 這一字節的其余 6 位表示長度,可以保存的最大長度是 63 (包括在內)
01|000000 00000000 2byte 長度為 14 位,當前字節 6 位,加上下個字節 8 位
10|000000 [32 bit integer] 5byte 長度由隨后的 32 位整數保存
11|000000   后跟一個特殊編碼的對象。字節中的 6 位(實際上只用到兩個bit)指定對象的類型,用來確定怎樣讀取和解析接下來的數據

 

rdb.h文件中具體定義的編碼:

  • 普通編碼方式

#define RDB_6BITLEN 0

#define RDB_14BITLEN 1

#define RDB_32BITLEN 2

#define RDB_ENCVAL 3

 

表格中前三種可以理解為普通編碼方式。

 

  • 字符串編碼方式

/* When a length of a string object stored on disk has the first two bits

 * set, the remaining two bits specify a special encoding for the object

 * accordingly to the following defines: */

#define RDB_ENC_INT8 0        /* 8 bit signed integer */

#define RDB_ENC_INT16 1       /* 16 bit signed integer */

#define RDB_ENC_INT32 2       /* 32 bit signed integer */

#define RDB_ENC_LZF 3         /* string compressed with FASTLZ */

 

表格中最后一種可以理解為字符串編碼方式。

 

1、字符串轉換為整數進行存儲

/* String objects in the form "2391" "-100" without any space and with a

 * range of values that can fit in an 8, 16 or 32 bit signed value can be

 * encoded as integers to save space */

int rdbTryIntegerEncoding(char *s, size_t len, unsigned char *enc) {

    long long value;

    char *endptr, buf[32];

 

    /* Check if it's possible to encode this value as a number */

    value = strtoll(s, &endptr, 10);

    if (endptr[0] != '\0') return 0;

    ll2string(buf,32,value);

 

    /* If the number converted back into a string is not identical

     * then it's not possible to encode the string as integer */

    if (strlen(buf) != len || memcmp(buf,s,len)) return 0;

 

    return rdbEncodeInteger(value,enc);

}

 

該函數最后調用的rdbEncodeInteger函數是真正完成特殊編碼的地方,具體定義如下:

/* Encodes the "value" argument as integer when it fits in the supported ranges

 * for encoded types. If the function successfully encodes the integer, the

 * representation is stored in the buffer pointer to by "enc" and the string

 * length is returned. Otherwise 0 is returned. */

int rdbEncodeInteger(long long value, unsigned char *enc) {

    if (value >= -(1<<7) && value <= (1<<7)-1) {

        enc[0] = (RDB_ENCVAL<<6)|RDB_ENC_INT8;

        enc[1] = value&0xFF;

        return 2;

    } else if (value >= -(1<<15) && value <= (1<<15)-1) {

        enc[0] = (RDB_ENCVAL<<6)|RDB_ENC_INT16;

        enc[1] = value&0xFF;

        enc[2] = (value>>8)&0xFF;

        return 3;

    } else if (value >= -((long long)1<<31) && value <= ((long long)1<<31)-1) {

        enc[0] = (RDB_ENCVAL<<6)|RDB_ENC_INT32;

        enc[1] = value&0xFF;

        enc[2] = (value>>8)&0xFF;

        enc[3] = (value>>16)&0xFF;

        enc[4] = (value>>24)&0xFF;

        return 5;

    } else {

        return 0;

    }

}

 

 

2、使用lzf算法進行字符串壓縮

當Redis開啟了字符串壓縮的功能后,如果一個字符串的長度超過20bytes,Redis會使用lzf算法對其進行壓縮后再存儲。

/* Save a string object as [len][data] on disk. If the object is a string

 * representation of an integer value we try to save it in a special form */

ssize_t rdbSaveRawString(rio *rdb, unsigned char *s, size_t len) {

    int enclen;

    ssize_t n, nwritten = 0;

 

    /* Try integer encoding */

    if (len <= 11) {

        unsigned char buf[5];

        if ((enclen = rdbTryIntegerEncoding((char*)s,len,buf)) > 0) {

            if (rdbWriteRaw(rdb,buf,enclen) == -1) return -1;

            return enclen;

        }

    }

 

    /* Try LZF compression - under 20 bytes it's unable to compress even

     * aaaaaaaaaaaaaaaaaa so skip it */

    if (server.rdb_compression && len > 20) {

        n = rdbSaveLzfStringObject(rdb,s,len);

        if (n == -1) return -1;

        if (n > 0) return n;

        /* Return value of 0 means data can't be compressed, save the old way */

    }

 

    /* Store verbatim */

    if ((n = rdbSaveLen(rdb,len)) == -1) return -1;

    nwritten += n;

    if (len > 0) {

        if (rdbWriteRaw(rdb,s,len) == -1) return -1;

        nwritten += len;

    }

    return nwritten;

}

 

 

四、value存儲

上面我們介紹了長度編碼,接下來繼續介紹不同數據類型的value是如何存儲的?

我們在介紹redisobject《Redis數據結構之robj》時,介紹了對象的10種編碼方式。

/* Objects encoding. Some kind of objects like Strings and Hashes can be

 * internally represented in multiple ways. The 'encoding' field of the object

 * is set to one of this fields for this object. */

#define OBJ_ENCODING_RAW 0     /* Raw representation */

#define OBJ_ENCODING_INT 1     /* Encoded as integer */

#define OBJ_ENCODING_HT 2      /* Encoded as hash table */

#define OBJ_ENCODING_ZIPMAP 3  /* Encoded as zipmap */

#define OBJ_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */

#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */

#define OBJ_ENCODING_INTSET 6  /* Encoded as intset */

#define OBJ_ENCODING_SKIPLIST 7  /* Encoded as skiplist */

#define OBJ_ENCODING_EMBSTR 8  /* Embedded sds string encoding */

#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */

 

 

1、string類型對象

我們知道,字符串類型對象的存儲結構是RDB文件中最基礎的存儲結構,其它數據類型的存儲大多建立在字符串對象存儲的基礎上。

  • OBJ_ENCODING_INT編碼的字符串

對於REDIS_ENCODING_INT編碼的字符串對象,有以下兩種保存方式:

a、如果該字符串可以用 8 bit、 16 bit或 32 bit長的有符號整型數值表示,那么就直接以整型數保存;

b、如果32bit的整數無法表示該字符串,則該字符串是一個long long類型的數,這種情況下將其轉化為字符串后存儲。

對於第一種方式,value域就是一個整型數值;

對於第二種方式,value域的結構為:

其中length域存放字符串的長度,content域存放字符序列。

/* Save a long long value as either an encoded string or a string. */

ssize_t rdbSaveLongLongAsStringObject(rio *rdb, long long value) {

    unsigned char buf[32];

    ssize_t n, nwritten = 0;

    int enclen = rdbEncodeInteger(value,buf);

    if (enclen > 0) {

        return rdbWriteRaw(rdb,buf,enclen);

    } else {

        /* Encode as string */

        enclen = ll2string((char*)buf,32,value);

        serverAssert(enclen < 32);

        if ((n = rdbSaveLen(rdb,enclen)) == -1) return -1;

        nwritten += n;

        if ((n = rdbWriteRaw(rdb,buf,enclen)) == -1) return -1;

        nwritten += n;

    }

    return nwritten;

}

 

 

  • OBJ_ENCODING_RAW編碼的字符串

對於REDIS_ENCODING_RAW編碼的字符串對象,有以下三種保存方式:

a、如果該字符串可以用 8 bit、 16 bit或 32 bit長的有符號整型數值表示,那么就將字符串轉換為整型數存儲以節省空間;

b、如果服務器開啟了字符串壓縮功能,且該字符串的長度大於20bytes,則使用lzf算法對字符串壓縮后進行存儲;

c、如果不滿足上面兩個條件,Redis只能以普通字符序列的方式來保存該字符串字符串對象。

對於前面兩種方式,詳見小節【長度編碼】中已經詳細介紹過。

對於第三種方式,Redis以普通字符序列的方式來保存字符串對象,value域的存儲結構為:

其中length域存放字符串的長度,content域存放字符串本身。

/* Save a string object as [len][data] on disk. If the object is a string

 * representation of an integer value we try to save it in a special form */

ssize_t rdbSaveRawString(rio *rdb, unsigned char *s, size_t len) {

    int enclen;

    ssize_t n, nwritten = 0;

 

    /* Try integer encoding */

    if (len <= 11) {

        unsigned char buf[5];

        if ((enclen = rdbTryIntegerEncoding((char*)s,len,buf)) > 0) {

            if (rdbWriteRaw(rdb,buf,enclen) == -1) return -1;

            return enclen;

        }

    }

 

    /* Try LZF compression - under 20 bytes it's unable to compress even

     * aaaaaaaaaaaaaaaaaa so skip it */

    if (server.rdb_compression && len > 20) {

        n = rdbSaveLzfStringObject(rdb,s,len);

        if (n == -1) return -1;

        if (n > 0) return n;

        /* Return value of 0 means data can't be compressed, save the old way */

    }

 

    /* Store verbatim */

    if ((n = rdbSaveLen(rdb,len)) == -1) return -1;

    nwritten += n;

    if (len > 0) {

        if (rdbWriteRaw(rdb,s,len) == -1) return -1;

        nwritten += len;

    }

    return nwritten;

}

 

 

2、list類型對象

  • OBJ_ENCODING_LINKEDLIST編碼的list類型對象

每個節點以字符串對象的形式逐一存儲。

在RDB文件中存儲結構如下:

 

  • OBJ_ENCODING_ZIPLIST編碼的list類型對象

Redis將其當做一個字符串對象的形式進行保存。

3、hash類型對象

  • OBJ_ENCODING_ZIPLIST編碼的hash類型對象

Redis將其當做一個字符串對象的形式進行保存。

  •  OBJ_ENCODING_HT編碼的hash類型對象

hash中的每個鍵值對的key值和value值都以字符串對象的形式相鄰存儲。

在RDB文件中存儲結構如下:

4、set類型對象

  • OBJ_ENCODING_HT編碼的set類型對象

其底層使用字典dict結構進行存儲,只是該字典的value值為NULL,所以只需要存儲每個鍵值對的key值即可。每個元素以字符串對象的形式逐一存儲。

在RDB文件中存儲結構如下:

  • OBJ_ENCODING_INTSET編碼的set類型對象

Redis將其當做一個字符串對象的形式進行保存,

5、zset類型對象

  • OBJ_ENCODING_ZIPLIST編碼的zset類型對象

Redis將其當做一個字符串對象的形式進行保存。

  • OBJ_ENCODING_QUICKLIST編碼的zset類型對象

對於其中一個元素,先存儲其元素值value再存儲其分值score。zset的元素值是一個字符串對象,按字符串形式存儲,分值是一個double類型的數值,Redis先將其轉換為字符串對象再存儲。

在RDB文件中存儲結構如下:

 

五、RDB如何完成存儲

  • save命令

save是在Redis進程中執行的,由於Redis是單線程實現,所以當save命令在執行時會阻塞Redis服務器一直到該命令執行完成為止。

  • bgsave命令

與save命令不同的是,bgsave命令會先fork出一個子進程,然后在子進程中生成RDB文件。由於在子進程中執行IO操作,所以bgsave命令不會阻塞Redis服務器進程,Redis服務器進程在此期間可以繼續對外提供服務。

bgsave命令由rdbSaveBackground函數實現,從該函數的實現中可以看出:為了提高性能,Redis服務器在bgsave命令執行期間會拒絕執行新到來的其它bgsave命令。

 

這里就不再列出rdbSave函數和rdbSaveBackground函數的具體實現,請移步到rdb.c文件中查看。

 

上篇文章《Redis持久化persistence》中,介紹了redis.conf中配置"觸發執行"的配置:

<seconds> <changes>

 

表示如果在secons指定的時間(秒)內對Redis數據庫DB至少進行了changes次修改,則執行一次bgsave命令

 

我們思考一個問題:Redis是如何判斷save選項配置條件是否已經達到,可以觸發執行的呢?

1、save選項配置條件如何存儲?

在server.h頭文件中,定義了saveparam結構體來保存save配置選項,該結構體的定義如下:

 

struct saveparam {

    time_t seconds; // 秒數

    int changes;      // 修改次數

};

 

Redis默認提供或用戶輸入的save選項則保存在 redisServer結構體中:

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. */

....

}

我們可以看到redisServer結構體中的saveparams字段是一個數組,里面一個元素就是一個save配置,而saveparamslen字段則指明了save配置的個數。

 

2、修改的次數和時間記錄如何存儲?

我們從redisServer結構體中,知道dirty和lastsave字段

  • dirty的值表示自最近一次執行save或bgsave以來對數據庫DB的修改(即執行寫入、更新、刪除操作的)次數;

  • lastsave是最近一次成功執行save或bgsave命令的時間戳。

 

3、Redis如何判斷是否滿足save選項配置的條件?

到目前為止,我們已經有了記錄save配置的redisServer.saveparams數組,告訴Redis如果滿足save配置的條件則執行一次bgsave命令。此外我們也有了redisServer.dirty和redisServer.lastsave兩個字段,分別記錄了對數據庫DB的修改(即執行寫入、更新、刪除操作的)次數和最近一次執行save或bgsave命令的時間戳。

 

接下來我們只要周期性地比較一下redisServer.saveparams和redisServer.dirty、redisServer.lastsave就可以判斷出是否需要執行bgsave命令。

這個周期性執行檢查功能的函數就是serverCron函數,定義在server.c文件中。

 

六、總結

  • rdbSave 會將數據庫數據保存到 RDB 文件,並在保存完成之前阻塞調用者。

  • SAVE 命令直接調用 rdbSave ,阻塞 Redis 主進程; BGSAVE 用子進程調用 rdbSave ,主進程仍可繼續處理命令請求。

  • SAVE 執行期間, AOF 寫入可以在后台線程進行, BGREWRITEAOF 可以在子進程進行,所以這三種操作可以同時進行。

  • 為了避免產生競爭條件, BGSAVE 執行時, SAVE 命令不能執行。

  • 為了避免性能問題, BGSAVE 和 BGREWRITEAOF 不能同時執行。

  • RDB 文件使用不同的格式來保存不同類型的值。

 

 

--EOF--


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM