Redis持久化之RDB

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

 

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

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

 

1、初始RDB數組

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

127.0.0.1:6379> flushdb數據結構

OKless

127.0.0.1:6379> set city "beijing"ide

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的文件格式。

 

2、RDB文件結構

咱們先大體看下RDB文件結構

 

一、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 —— 校驗和

 

二、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 —— 主要數據

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

 

 

四、RDB_OPCODE_EOF

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

#define RDB_OPCODE_EOF        255

 

五、rdb_checksum

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

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

若是Redis未開啓校驗功能,則該域的值爲0。

#define CONFIG_DEFAULT_RDB_CHECKSUM 1

 

3、長度編碼

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

 

表格中最後一種能夠理解爲字符串編碼方式。

 

一、字符串轉換爲整數進行存儲

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

    }

}

 

 

二、使用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;

}

 

 

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

 

 

一、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;

}

 

 

二、list類型對象

  • OBJ_ENCODING_LINKEDLIST編碼的list類型對象

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

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

 

  • OBJ_ENCODING_ZIPLIST編碼的list類型對象

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

三、hash類型對象

  • OBJ_ENCODING_ZIPLIST編碼的hash類型對象

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

  •  OBJ_ENCODING_HT編碼的hash類型對象

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

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

四、set類型對象

  • OBJ_ENCODING_HT編碼的set類型對象

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

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

  • OBJ_ENCODING_INTSET編碼的set類型對象

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

五、zset類型對象

  • OBJ_ENCODING_ZIPLIST編碼的zset類型對象

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

  • OBJ_ENCODING_QUICKLIST編碼的zset類型對象

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

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

 

5、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選項配置條件是否已經達到,能夠觸發執行的呢?

一、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配置的個數。

 

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

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

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

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

 

三、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文件中。

 

6、總結

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

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

  • SAVE 執行期間, AOF 寫入能夠在後臺線程進行, BGREWRITEAOF 能夠在子進程進行,因此這三種操做能夠同時進行。

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

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

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

 

 

--EOF--

相關文章
相關標籤/搜索