redis源碼分析(三)--rdb持久化

Redis rdb持久化

  Redis支持兩種持久化方式:rdb與aof。rdb將一個節點上的內存數據序列化後存儲到磁盤中,序列化的數據以儘量節約空間的方式存儲,並不是徹底的ascii表示。它的優勢在於節約空間,恢復速度快,缺點在於每一次操做都須要對整個內存數據進行序列化,而且持久化過程當中的修改被丟失。而aof將數據以操做命令的方式進行存儲,從aof恢復數據即從aof文件讀入命令再執行命令。它的優勢是能夠記錄持久化過程當中的產生的命令,而缺點在於徹底以ascii編碼,使用空間更多,且恢復速度更慢。這篇文件章主要對rdb的持久化方式進行大體介紹。redis

  Redis中有多種數據類型,如string、set、zset、hash、list等,而且具備多個db(一個db便是一個字典,存儲了string、list、set等類型的數據),爲了完成不一樣類型數據的序列化,Redis設計了相應的持久化格式。數組

1. 序列化string

  序列化string時,首先會存儲string的長度,而後存儲string的真實值。爲了節約空間,對於不一樣長度的string,redis使用盡量少的空間來存儲它的長度,所以設計了以下的字符串長度存儲格式:     緩存

  1. len < 1<<6,使用1個字節編碼長度,該字節的高2bits爲00,低6bits表明長度
  2. 1<<6 <= len < 1<<14,使用2個字節編碼長度,第1個字節的高兩bits爲01,後續14bits表明長度
  3. 1<<14 <= len <= UINT32_MAX,使用5個字節編碼長度,第1個字節爲0x80,後續4字節表明長度
  4. UINT32_MAX < len,使用9個字節編碼長度,第1個字節爲0x81,後續8字節表明長度

Redis中相應的定義以下:socket

/* 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|XXXXXX => if the two MSB are 00 the len is the 6 bits of this byte
 * 01|XXXXXX XXXXXXXX =>  01, the len is 14 byes, 6 bits + 8 bits of next byte
 * 10|000000 [32 bit integer] => A full 32 bit len in net byte order will follow
 * 10|000001 [64 bit integer] => A full 64 bit len in net byte order will follow
 * 11|OBKIND 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. */

舉例,對string「hello world」編碼結果以下:ide

0xC 「hello, world」

0xC以1個字節表示字符串長度爲12字節,後續12字節即爲字符串的真實值。因爲是非前綴碼,所以根據第1個字節的值徹底能夠進行區分是哪一種格式,反序列化時首先讀1個字節的內容,而後根據該字節的值便可選擇正確的操做。函數

  redis中rdb.c源文件中的rdbSaveLen函數完成長度的編碼,而rdbLoadLen完成長度值的解碼。ui

2. 序列化int

  當須要序列化的值爲整形值時,整形值會以二進制的形式直接進行序列化,而不是轉換成ascii碼的字符串形式進行序列化,從而進一步節約空間。此外,根據整形值的大小,redis一樣以儘量少的空間來存儲該值。與字符串相似,首先有一個字節的高2bits設置爲11表明後續的值是一個整形值,同時以低6bits的值區分使用的字節數。this

  1.   (value >= -(1<<7) && value <= (1<<7)-1),低6bit值爲0,後續1個字節存儲整形值。
  2.   (value >= -(1<<15) && value <= (1<<15)-1),低6btis值爲1,後續2個字節存儲整形值。
  3.   (value >= -((long long)1<<31) && value <= ((long long)1<<31)-1),低6bits值爲2,後續4個字節存儲整形值。

總體形式以下:  編碼

11000000[8 bit integer]
11000001[16 bit integer]
11000002[32 bit integer]

舉例,序列化整形值0x300的結果以下:spa

0xC1 0x0300

0xC1表示後續是一個以int16形式存儲的整形值,0x0300即爲該整形值。因爲第1個字節的高2bits爲11,與string類型表示長度的格式不相同,所此能夠區分是string仍是int型。

  此外,若第1個字節的高2bits爲11,低6bits爲3(RDB_ENC_LZF),那麼後續的數據爲壓縮數據,該字節後跟着兩個長度值,分別表示壓縮後的數據長度與壓縮前的數據長度,再後面纔是真正的數據。

3. RDB_TYPE_*與RDB_OPCODE_*

  Redis是一個key-value緩存系統,全部的值都以key-value的形式存儲在db字典中。可是redis中value的類型並不只限於string,它還能夠是結構體類型,如set、list、hash等。爲了序列化這些類型,redis中首先會以一個字節存儲數據類型,而後若是是複合類型,會存儲該類型的成員數目,而後遍歷成員值並以基本的int或者string類型進行存儲。

  Redis中定義了以下的類型值:

/* Map object types to RDB object types. Macros starting with OBJ_ are for
 * memory storage and may change. Instead RDB types must be fixed because
 * we store them on disk. */
#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 /* ZSET version 2 with doubles stored in binary. */
#define RDB_TYPE_MODULE 6
#define RDB_TYPE_MODULE_2 7 /* Module value with annotations for parsing without
                               the generating module being loaded. */
/* 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
#define RDB_TYPE_STREAM_LISTPACKS 15
/* NOTE: WHEN ADDING NEW RDB TYPE, UPDATE rdbIsObjectType() BELOW */

這些類型值表明了被序列化的value的類型,如list、set等,此外還表明了value的具體實現方式,如set可使用hash表實現,相應類型值爲RDB_TYPE_SET,也可使用一個有序的整形數組實現,對應類型值爲RDB_TYPE_SET_INTSET。反序列化時,首先讀取1個字節的值判斷後續數據的類型,而後進行相應的類型重建。

舉例,一個string類型的key-value對 「key1」 「hello, world」的序列化結果爲:

RDB_TYPE_STRING 0x4 「key1」 0xC 「hello, world」

首先1個字節的類型值RDB_TYPE_STRING,而後一個字節的長度值0x4,即後面有4個字節的關鍵字字符串,而後一個字節的長度值0xC,即後面有12個字節的字符串值。

反序列化時:

  1. 首先讀取1個字節得RDB_TYPE_STRING,獲得後續爲一個string對象
  2. 而後讀取長度值0x4,並讀取相應的4個字節獲得關鍵字,
  3. 最後再讀取長度0xC,並讀取相應的12字節值獲得value值。

舉例,一個list類型的key-value對:」key2」 2 3 4 0x300的序列化結果爲:

RDB_TYPE_LIST 0x4 「key2」 0x4 0xc0 0x2 0xc0 0x3 0xc0 0x4 0xC1 0x0300

首先是1個字節的類型值RDB_TYPE_LIST,而後長度值爲0x4,表示有4個字節的關鍵字字符串,而後是0x4表示該list有4個成員,而後4個成員依次以整形的格式序列化。

反序列化時:

  1. 首先讀取1個字節獲得RDB_TYPE_LIST,獲得後續爲一個list對象,
  2. 讀取關鍵字的長度爲0x4,讀取4字節獲得」key2」
  3. 讀取長度0x4,獲得list成員數目爲4
  4. 讀取長度,而後讀取整形值,循環4次,完成list對象的重建。

  除了RDB_TYPE*與redis中存儲的數據類型對應,還有一類RDB_OPCODE*表示一些其它的數據,如:RDB_OPCODE_EXPIRETIME表示後面的數據是該對象的超時時間;RDB_OPCODE_SELECTDB表示接下來的數據是一個db索引,直到遇到下一個RDB_OPCODE_SELECTDB以前,全部反序列化的數據都應該存儲在該索引的db字典中。Redis中定義了以下類型的RDB_OPCODE*值:

/* Special RDB opcodes (saved/loaded with rdbSaveType/rdbLoadType). */
#define RDB_OPCODE_MODULE_AUX 247   /* Module auxiliary data. */
#define RDB_OPCODE_IDLE       248   /* LRU idle time. */
#define RDB_OPCODE_FREQ       249   /* LFU frequency. */
#define RDB_OPCODE_AUX        250   /* RDB aux field. */
#define RDB_OPCODE_RESIZEDB   251   /* Hash table resize hint. */
#define RDB_OPCODE_EXPIRETIME_MS 252    /* Expire time in milliseconds. */
#define RDB_OPCODE_EXPIRETIME 253       /* Old expire time in seconds. */
#define RDB_OPCODE_SELECTDB   254   /* DB number of the following keys. */
#define RDB_OPCODE_EOF        255   /* End of the RDB file. */

這些特殊類型後續值的長度一般是固定的,如RDB_OPCODE_EXPIRETIME以32bit表示超時時間,單位爲s;RDB_OPCODE_EXPIRETIME_MS後面是64bit表示的超時時間,單位爲ms;RDB_OPCODE_SELECTDB後續是使用的db索引,以len的編碼方式進行存儲,而RDB_OPCODE_AUX表示一些key-value對,而這些key, value都是string與int等基本類型。

  除了rdb文件開頭的固定字節的magic碼,全部rdb序列化的數據都有一個前置的RDB_TYPE_*值或者RDB_OPCODE_*值,它表示了後續數據的存儲方式,從而反序列化時採起正確的操做。

4. RDB序列化流程

1. 序列化前置信息,如magic識別碼,版本號,時間戳

2.遍歷db,序列化每個db

   2.1 序列化RDB_OPCODE_SELETCTDB

   2.2 序列化RDB_OPCODE_RESIZEDB,存儲該db的大小

   2.3 序列化db中的每個key-value對

       2.3.1 序列化超時時間RDB_OPCODE_EXPIRETIME

       2.3.2 序列化lru值,RDB_OPCODE_IDLE

       2.3.3 序列化LFU值,RDB_OPCODE_FREQ

       2.3.4 序列化值類型

       2.3.5 序列化key(string)

       2.3.6 序列化value

在前一篇文件中介紹過redis的rio抽象層,而這些序列化操做正是以rio做爲接口,以rio爲目的地,既能夠將序列化內容輸出到文件,也能夠將序列化內容輸出到多個sockets中。普通的持久化操做使用文件做爲輸出對象,而在master-slave中的數據同步可能會使用到sockets做爲輸出對象,經過rio的抽象,將序列化與底層io進行解藕。

  redis中的序列化函數調用棧以下:

          

 

左圖是輸出對象爲文件的序列化調用關係,右圖是輸出對象爲sockets的序列化調用關係。

5. RDB反序列化流程

 1.讀取9字節magic標誌,並驗證

循環進行2-3步

2. 讀取1字節RDB_TYPE_*或者RDB_OPCODE_*標誌

3. 根據RDB_OPCODE_*或者RDB_TYPE_*的值作相應的處理

上述第3步中,若是須要讀取string或者int這種基本類型,處理過程爲:

  1. 調用rdbLoadLen讀取長度
  2. 根據rdbLoadLen返回值讀取string或者整形值

若是對應的類型爲複合類型,如list、set等,處理過程爲:

  1. 調用rdbLoadLen讀取複合類型成員數目
  2. 循環讀取成員值,直到指定數目的值被讀出。讀取成員的操做即讀取string或者int基本類型。

反序列化的輸入爲文件,即便序列化的輸出目的地爲sockets,接收端也會先將數據存儲到一個文件中,而後再從文件反序列化。反序列化的調用棧爲

  1. rdbLoad,初始化rio爲文件流
  2. rdbLoadRio以rio爲輸入,從文件中讀取數據並完成反序列化。

6. 後臺進程

 因爲redis是單線程模式,所以它選擇將持久化操做放在子進程中進行,不然持久化過程當中將中止響應請求。

  根據fork函數的特性,子進程建立後與父進程擁有相同的內存內容,所以fork函數調用後子進程即獲得了此時db中的完整內容。而且因爲copy-on-write特性,並不會發生大量的內存copy,僅在write操做發生時,相應的內存頁才進行一個copy生成副本,即該操做也不會特別耗時。

  但相應的,父進程繼續接受客戶端的命令,修改的內容並不會反應到子進程的內存中,所以rdb持久化過程當中出現的修改將會丟失。

相關文章
相關標籤/搜索