前言
相信各位對 Redis 的這兩種持久化機制都不陌生,簡單來說,RDB 就是對數據的全量備份,AOF 則是增量備份,而從 4.0 版本開始引入了混合方式,以 7.2.3 版本為例,會生成三類文件:RDB、AOF 和記錄 aof 文件的元數據信息文件,如下圖所示,這時的 AOF 可以看作是一種差異備份。
接下來本文將結合具體的備份文件,通過分析其結構,從另一種角度來看兩種持久化方式的差異。
RDB
首先是對 RDB 全量備份文件的解析,想要生成 RDB 文件,有兩種方式,一種是手動方式:使用 save(阻塞)或者 bgsave(非阻塞)命令生成,一種是在配置文件中增加save m n
(表示在 m 內,至少出現了 n 次變更就會執行 bgsave 命令)配置來實現。
下面就以一個具體的dump.rdb
(在 0 號庫中有一條鍵為 hello,值為 world 的記錄)文件為例來解析其文件格式,由于 RDB 文件是二進制格式,這里使用了一個在線的十六進制編輯器進行查看:
下文均是結合 Redis 7.2.3 版本的源碼的 rdb.c 文件進行解析,對應源碼地址。
0x00 Redis 版本
52 45 44 49 53 30 30 31 31
,根據源碼snprintf(magic,sizeof(magic),"REDIS%04d",RDB_VERSION);
可以看到這里前五位是固定值REDIS
,后四位用于標識RDB
的版本對應11。
0x01 輔助信息
這部分涉及數據較多,先放出源碼:
if (rdbSaveAuxFieldStrStr(rdb,"redis-ver",REDIS_VERSION) == -1) return -1;
if (rdbSaveAuxFieldStrInt(rdb,"redis-bits",redis_bits) == -1) return -1;
if (rdbSaveAuxFieldStrInt(rdb,"ctime",time(NULL)) == -1) return -1;
if (rdbSaveAuxFieldStrInt(rdb,"used-mem",zmalloc_used_memory()) == -1) return -1;
if (rdbSaveAuxFieldStrInt(rdb, "aof-base", aof_base) == -1) return -1;
結合編輯器右側的信息,可以發現這部分數據下圖中選中的數據:
-
redis-ver(Redis 版本)
這部分對應
FA 09 72 65 64 69 73 2D 76 65 72 05 37 2E 32 32 2E 33
,其中開頭的FA(250)
代表這部分數據是 AUX 屬性字段,根據源碼#define RDB_OPCODE_AUX 250
可以了解到。然后是09 72 65 64 69 73 2D 76 65 72
,09 代表隨后的 9個字節是屬性名,即redis-ver
,最后是05 37 2E 32 32 2E 33
,其中 05 代表隨后的 5 個字節是屬性名對應的字段值,即 Redis 的版本號7.2.3
。 -
redis-bits(位架構)
這部分對應
FA 0A 72 65 64 69 73 2D 62 69 74 73 C0 40
。參考 1 可知開始的FA
代表AUX
,OA
代表隨后的 10 字節是屬性名,即redis-bits
。但是隨后的C0
就不再是代表值的長度了,這里先說明C0
代表后續的一個字節按照整數進行讀取,對應0x40(64),即代表是 Redis 的 64位架構。下面我們再來說明為什么會有以上的區別:其實代表值長度的不一定只有一個字節,這里會根據前兩位進行判斷(C0 對應
1100 0000
):-
如果前兩位是 00 ,那么后續的 6 位(可表示 0 ~ 63)就代表實際的字符串長度。
-
如果前兩位是 01,那么接下來的一個字節也會用于表示長度,加上第一個剩下的 6 位,總共 14 位(可表示0 ~ 16383)代表實際的字符串長度。
-
如果前兩位是 10,那么剩下 6 位的值如果是 0,就代表隨后的 32 字節代表具體長度,如果剩下 6 位的值是 1,就代表隨后的 64 字節代表具體長度。
-
如果前兩位是 11,則需要根據整個字節的值再進行判斷,如果是
C0
就代表將隨后的 1 字節表示整數,如果是C1
就代表隨后的 2 字節表示整數,如果是C2
就代表隨后的 4 字節表示整數,如果是C3
就代表隨后的內容是使用LZF 壓縮算法
處理后的內容。
-
-
ctime(文件創建時間)
這部分對應
FA 05 63 74 69 6D 65 C2 44 11 57 65
,參考 1 可知開始的FA
代表AUX
,05
代表隨后的 5 字節是屬性名,即ctime
。參考 2 中解析,可知隨后的C2
代表后續的 4 字節即44 11 57 65
表示整數,由于需要按照小端序讀取,因此對應的內容是0x65571144
,即秒級時間戳,如下圖所示: -
used-mem(內存使用大小)
這部分對應
FA 08 75 73 65 64 2D 6D 65 6D C2 40 15 12 00
,參考 1 可知開始的FA
代表AUX
,08
代表隨后的 8 字節是屬性名,即used-mem
。參考 3 ,可知隨后的C2
代表后續的 4 字節即40 15 12 00
表示整數,對應的內容是0x00121540
,即 Redis 在 創建 rdb 文件前占用的內存是 1185088 字節(1.13 MB)。 -
aof-base (是否為 aof 基準文件)
這部分對應
FA 08 61 6F 66 2D 62 61 73 65 C0 00
,參考 1 可知開始的FA
代表AUX
,08
代表隨后的 8 字節是屬性名,即aof-base
。參考 2 中解析,可知隨后的C0
代表后續的 1 字節即00
表示整數,即該 RDB 文件不是作為 AOF 的基準文件,后文中可以看到在 AOF 中生成的 RDB 文件中該值為 1。
0x02 數據部分
FE 00 FB 01 00 00 05 68 65 6C 6C 6F 05 77 6F 72 6C 64
,這部分開始對應具體的數據信息,先展示源碼:
/* save all databases, skip this if we're in functions-only mode */
if (!(req & SLAVE_REQ_RDB_EXCLUDE_DATA)) {for (j = 0; j < server.dbnum; j++) {if (rdbSaveDb(rdb, j, rdbflags, &key_counter) == -1) goto werr;}
}// 以下內容是 rdbSaveDb 函數內的語句/* Write the SELECT DB opcode */
if ((res = rdbSaveType(rdb,RDB_OPCODE_SELECTDB)) < 0) goto werr;
written += res;
if ((res = rdbSaveLen(rdb, dbid)) < 0) goto werr;
written += res;
/* Write the RESIZE DB opcode. */
unsigned long long expires_size = dbSize(db, DB_EXPIRES);
if ((res = rdbSaveType(rdb,RDB_OPCODE_RESIZEDB)) < 0) goto werr;
written += res;
if ((res = rdbSaveLen(rdb,db_size)) < 0) goto werr;
written += res;
if ((res = rdbSaveLen(rdb,expires_size)) < 0) goto werr;
written += res;
可以看出這部分是遍歷所有的數據庫內容然后進行保存,下面再結合具體的內容進行介紹。
首先是FE 00
,其中FE(254)
對應RDB_OPCODE_SELECTDB
常量是查詢數據庫的標志,00
即代表 0 號數據庫。
然后是FB 01 00
,其中FB(251)
對應RDB_OPCODE_RESIZEDB
常量是查詢該數據庫大小的標志,根據if ((res = rdbSaveLen(rdb,db_size)) < 0) goto werr;
知道01
代表數據庫的大小,即只有一條數據,根據if ((res = rdbSaveLen(rdb,expires_size)) < 0) goto werr;
知道00
代表沒有包含過期標志的數據。
最后是00 05 68 65 6C 6C 6F 05 77 6F 72 6C 64
,代表具體的數據內容。其中開始的00
代表類型是字符串,參考源碼可知(RDB_TYPE_STRING 的值是 0):
/* Save the object type of object "o". */
int rdbSaveObjectType(rio *rdb, robj *o) {switch (o->type) {case OBJ_STRING:return rdbSaveType(rdb,RDB_TYPE_STRING);case OBJ_LIST:if (o->encoding == OBJ_ENCODING_QUICKLIST || o->encoding == OBJ_ENCODING_LISTPACK)return rdbSaveType(rdb, RDB_TYPE_LIST_QUICKLIST_2);elseserverPanic("Unknown list encoding");case OBJ_SET:if (o->encoding == OBJ_ENCODING_INTSET)return rdbSaveType(rdb,RDB_TYPE_SET_INTSET);else if (o->encoding == OBJ_ENCODING_HT)return rdbSaveType(rdb,RDB_TYPE_SET);else if (o->encoding == OBJ_ENCODING_LISTPACK)return rdbSaveType(rdb,RDB_TYPE_SET_LISTPACK);elseserverPanic("Unknown set encoding");case OBJ_ZSET:if (o->encoding == OBJ_ENCODING_LISTPACK)return rdbSaveType(rdb,RDB_TYPE_ZSET_LISTPACK);else if (o->encoding == OBJ_ENCODING_SKIPLIST)return rdbSaveType(rdb,RDB_TYPE_ZSET_2);elseserverPanic("Unknown sorted set encoding");case OBJ_HASH:if (o->encoding == OBJ_ENCODING_LISTPACK)return rdbSaveType(rdb,RDB_TYPE_HASH_LISTPACK);else if (o->encoding == OBJ_ENCODING_HT)return rdbSaveType(rdb,RDB_TYPE_HASH);elseserverPanic("Unknown hash encoding");case OBJ_STREAM:return rdbSaveType(rdb,RDB_TYPE_STREAM_LISTPACKS_3);case OBJ_MODULE:return rdbSaveType(rdb,RDB_TYPE_MODULE_2);default:serverPanic("Unknown object type");}return -1; /* avoid warning */
}
隨后的05 68 65 6C 6C 6F
中的 05
表示鍵的長度是5,對應68 65 6C 6C 6F
即hello
。最后的05 77 6F 72 6C 64
代表值的長度也是 5,內容是77 6F 72 6C 64
即world
。
0x03 尾部信息
FF 18 7F 33 2E 0F C6 20 19
,根據源碼#define RDB_OPCODE_EOF 255
可知,FF(25)
是文件的 EOF 即結束標志。隨后的 8 位根據源碼可知對應 CRC64 校驗碼:
/* EOF opcode */
if (rdbSaveType(rdb,RDB_OPCODE_EOF) == -1) goto werr;/* CRC64 checksum. It will be zero if checksum computation is disabled, the* loading code skips the check in this case. */
cksum = rdb->cksum;
memrev64ifbe(&cksum);
if (rioWrite(rdb,&cksum,8) == 0) goto werr;
AOF
AOF 用于對數據庫的增量備份,如果需要開啟,需要將配置文件中的appendonly
設置為 yes。同時,根據需要可以,設置appenddirname
對應保存的文件夾,設置appendfilename
用于配置文件名,設置appendfsync
用于配置頻率。開啟后,可以在指定的文件夾下看到類似以下的文件結構:
其中 rdb 結尾的代表是 AOF 備份的基準文件,aof 文件是增量備份的執行命令信息,manifest 文件是記錄 aof 文件的元數據信息。
0x00 dump.aof.1.base.rdb
通過十六進制編輯器打開該文件,可以發現內容和 RDB 中的格式一致(創建數據前備份的,所以沒有數據部分):
而由于是 AOF 的基準文件,這里aof-base
的值是01
即代表是基準文件。
0x01 dump.aof.1.incr.aof
文本文件,內容如下(*
開頭代表命令包含的參數個數,$
開頭代表命令的長度):
*2 // 兩個參數
$6 // 第一個參數長度為 6, 對應 SELECT 的長度
SELECT
$1 // 第二個參數長度為 1, 對應 0, 即 0 號數據庫
0
*3 // 三個參數
$3 // 第一個參數長度為 3, 對應 set 的長度
set
$5 // 第二個參數長度為 5, 對應 hello 的長度
hello
$0 // 第三個參數長度為 0*3 // 三個參數
$3 // 第一個參數長度為 3, 對應 set 的長度
set
$5 // 第二個參數長度為 5, 對應 hello 的長度
hello
$5
world // 第三個參數長度為 5, 對應 world 的長度
0x02 dump.aof.manifest
文本文件,內容如下:
file dump.aof.1.base.rdb seq 1 type b
file dump.aof.1.incr.aof seq 1 type i
其中seq 1
代表文件序號為 1,type b
代表type base
即基準文件,type i
代表type increment
即增量文件。
總結
本文根據一個簡單的 RDB 文件講解了 RDB 文件的存儲格式,同時也簡單介紹了 AOF 的文件格式。關于 RDB 中的 LZF 壓縮算法和更復雜數據的存儲方式(包含過期時間,數據類型為 Set,Map)等未作介紹,將留到下次。