redis資料庫之rdb持久化
redis是一種記憶體資料庫,也就是redis的資料在正常工作的情況下都是儲存在記憶體中。但並不是說redis只能把資料儲存在記憶體中,redis提供了兩種資料持久化機制:rdb和aof。rdb持久化有三種方式被啟動:使用者向redis傳送save或者bgsave命令。save和bgsave的不同就在於save會阻塞redis伺服器,而bgsave不會。這樣bgsave就在不影響redis伺服器正常工作的情況進行資料持久化,bgsave主要是通過子程序來進行資料持久化。無論是save還是bgsave最後都會通過呼叫rdbsave來進行儲存操作。
在講解rdbsave這個函式之前,先來介紹下rdb檔案格式。一個rdb檔案分為以下幾個部分:
REDIS:檔案的最開頭儲存著REDIS 五個字元,標識著一個RDB 檔案的開始。
RDB-VERSION:一個四位元組長的以字元表示的整數,記錄了該檔案所使用的RDB 版本號。目前的RDB 檔案版本為0006 。
SELECT-DB:這域儲存著跟在後面的鍵值對所屬的資料庫號碼。在讀入RDB 檔案時,程式會根據這個域的值來切換資料庫,確保資料被還原到正確的資料庫上。
KEY-VALUE-PAIRS:每個鍵值對的資料使用以下結構來儲存:
OPTIONAL-EXPIRE-TIME:,如果鍵沒有設定過期時間,那麼這個域就不會出現;反之,如果這個域出現的話,那麼它記錄著鍵的過期時間
KEY:儲存著鍵,格式和REDIS_ENCODING_RAW 編碼的字串物件一樣
TYPE-OF-VALUE:記錄著VALUE 域的值所使用的編碼,根據這個域的指示,程式會使用不同的方式來儲存和讀取VALUE 的值。
VALUE:儲存著真實的值,但是這個被儲存的值會被進行各種編碼。(可以檢視redis設計與實現)
下面來正式看下rdbsave函式:
rdbsave的整體邏輯還是比較簡單的。我們具體還是來看rdb是怎麼儲存redis各種型別的。int rdbSave(char *filename) { ...... // 以 "temp-<pid>.rdb" 格式建立臨時檔名 snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid()); fp = fopen(tmpfile,"w"); ...... snprintf(magic,sizeof(magic),"REDIS%04d",REDIS_RDB_VERSION); if (rdbWriteRaw(&rdb,magic,9) == -1) goto werr; ...... for (j = 0; j < server.dbnum; j++) { // 指向資料庫 redisDb *db = server.db+j; // 指向資料庫 key space dict *d = db->dict; // 資料庫為空, pass ,處理下個數據庫 if (dictSize(d) == 0) continue; // 建立迭代器 di = dictGetSafeIterator(d); if (!di) { fclose(fp); return REDIS_ERR; } /* Write the SELECT DB opcode */ // 記錄正在使用的資料庫的號碼 if (rdbSaveType(&rdb,REDIS_RDB_OPCODE_SELECTDB) == -1) goto werr; if (rdbSaveLen(&rdb,j) == -1) goto werr; /* Iterate this DB writing every entry */ // 將資料庫中的所有節點儲存到 RDB 檔案 while((de = dictNext(di)) != NULL) { // 取出鍵 sds keystr = dictGetKey(de); // 取出值 robj key, *o = dictGetVal(de); long long expire; initStaticStringObject(key,keystr); // 取出過期時間 expire = getExpire(db,&key); if (rdbSaveKeyValuePair(&rdb,&key,o,expire,now) == -1) goto werr; } dictReleaseIterator(di); } ...... if (rdbSaveType(&rdb,REDIS_RDB_OPCODE_EOF) == -1) goto werr; ...... }
儲存type的函式rdbSaveType的邏輯很簡單,就不介紹了。儲存len的方式在講壓縮列表的時候介紹過,這裡也不介紹了。
來看一個比較重要的函式:rdbSaveKeyValuePair
int rdbSaveKeyValuePair(rio *rdb, robj *key, robj *val,
long long expiretime, long long now)
{
if (expiretime != -1) {
.......
}
......
// 儲存值型別
if (rdbSaveObjectType(rdb,val) == -1) return -1;
// 儲存 key
if (rdbSaveStringObject(rdb,key) == -1) return -1;
// 儲存 value
if (rdbSaveObject(rdb,val) == -1) return -1;
......
}
這個函式就是儲存db-data的。主要就是儲存過期時間,以及值的型別,key以及值。rdbSaveObjectType也比較簡單,就不做介紹了。
int rdbSaveStringObject(rio *rdb, robj *obj) {
/* Avoid to decode the object, then encode it again, if the
* object is alrady integer encoded. */
if (obj->encoding == REDIS_ENCODING_INT) {
// 整數在嘗試編碼之後寫入
return rdbSaveLongLongAsStringObject(rdb,(long)obj->ptr);
} else {
// 如果是字串,直接寫入 rdb
redisAssertWithInfo(NULL,obj,obj->encoding == REDIS_ENCODING_RAW);
return rdbSaveRawString(rdb,obj->ptr,sdslen(obj->ptr));
}
}
如果是整數會進行編碼後再寫入,而字串直接呼叫rdbSaveRawString寫入到rdb中。先來看下rdbSaveLongLongAsStringObject:
/* Save a long long value as either an encoded string or a string. */
/*
* 將一個 long long 值儲存為字串,或者編碼字串
*/
int rdbSaveLongLongAsStringObject(rio *rdb, long long value) {
unsigned char buf[32];
int 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);
redisAssert(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;
}
首先會進行編碼,如果編碼失敗,會再以字串的方式寫入rdb檔案中。主要來看rdbEncodeInteger:
int rdbEncodeInteger(long long value, unsigned char *enc) {
if (value >= -(1<<7) && value <= (1<<7)-1) {
enc[0] = (REDIS_RDB_ENCVAL<<6)|REDIS_RDB_ENC_INT8;
enc[1] = value&0xFF;
return 2;
} else if (value >= -(1<<15) && value <= (1<<15)-1) {
enc[0] = (REDIS_RDB_ENCVAL<<6)|REDIS_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] = (REDIS_RDB_ENCVAL<<6)|REDIS_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的31次方減一而且大於負的2的31次方都會進行整數編碼, 如果不在這個範圍就會按字串寫入。而這個範圍又分為幾個小範圍都是按char,short,int的最大值和最小值來判斷的。
如果vale在char範圍內,字元陣列的第一個元素為:C0也就是11000000
如果value在short範圍內,字元陣列的第一個元素為:C1也就是11000001
如果value在int範圍內,字元陣列的第一個元素為:C2也就是11000010
字元陣列後面都是真實值。
繼續來看另外一個比較重要的函式:
int rdbSaveRawString(rio *rdb, unsigned char *s, size_t len)
{
......
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;
}
}
......
if (server.rdb_compression && len > 20) {
}
......
if ((n = rdbSaveLen(rdb,len)) == -1) return -1;
nwritten += n;
if (len > 0) {
if (rdbWriteRaw(rdb,s,len) == -1) return -1;
nwritten += len;
}
......
}
字串的編碼會分一下幾種情況:
1、如果字串的長度小於11,會把字串轉化為整形,在寫入rdb中。
2、如果需要lzf壓縮,會進行壓縮後,把壓縮後的資料寫入rdb中,壓縮後是按什麼方式寫入的呢?格式如下:
首先寫入lzf的標識,也就是繼續上面value編碼的值繼續即C3(11000011).
然後是資料壓縮後的長度
再是壓縮之前的長度
最後是壓縮後的資料。
3、如果上面兩個條件都不滿足,就會按原始方式寫入。
最後來看下另外一個重要的函式,也就是真正把資料庫的值寫入rdb檔案的函式:
int rdbSaveObjectType(rio *rdb, robj *o) {
switch (o->type) {
// 字串
case REDIS_STRING:
return rdbSaveType(rdb,REDIS_RDB_TYPE_STRING);
// 列表
case REDIS_LIST:
// ziplist 編碼
if (o->encoding == REDIS_ENCODING_ZIPLIST)
return rdbSaveType(rdb,REDIS_RDB_TYPE_LIST_ZIPLIST);
// 雙端連結串列
else if (o->encoding == REDIS_ENCODING_LINKEDLIST)
return rdbSaveType(rdb,REDIS_RDB_TYPE_LIST);
else
redisPanic("Unknown list encoding");
// 集合
case REDIS_SET:
// intset
if (o->encoding == REDIS_ENCODING_INTSET)
return rdbSaveType(rdb,REDIS_RDB_TYPE_SET_INTSET);
// 字典
else if (o->encoding == REDIS_ENCODING_HT)
return rdbSaveType(rdb,REDIS_RDB_TYPE_SET);
else
redisPanic("Unknown set encoding");
// 有序集
case REDIS_ZSET:
// ziplist
if (o->encoding == REDIS_ENCODING_ZIPLIST)
return rdbSaveType(rdb,REDIS_RDB_TYPE_ZSET_ZIPLIST);
// 跳躍表
else if (o->encoding == REDIS_ENCODING_SKIPLIST)
return rdbSaveType(rdb,REDIS_RDB_TYPE_ZSET);
else
redisPanic("Unknown sorted set encoding");
// 雜湊
case REDIS_HASH:
// ziplist
if (o->encoding == REDIS_ENCODING_ZIPLIST)
return rdbSaveType(rdb,REDIS_RDB_TYPE_HASH_ZIPLIST);
// 字典
else if (o->encoding == REDIS_ENCODING_HT)
return rdbSaveType(rdb,REDIS_RDB_TYPE_HASH);
else
redisPanic("Unknown hash encoding");
default:
redisPanic("Unknown object type");
}
return -1; /* avoid warning */
}
這個函式的switch-case結構跟我們在講述redis的object物件的時候那張圖剛好形成對比。不論是string型別還是skiplist又或者是hash都是通過rdbSaveRawString又或者是rdbSaveStringObject來寫入rdb中。具體什麼型別用什麼方式寫入,通過上面這個函式一眼就能看出來。
redis定時rdb持久化的方式通過serverCron函式實現,redis所有的定時任務都是通過serverCron來實現的。具體實現看serverCron,邏輯也是比較簡單,最後還是通過呼叫rdbSave寫入rdb檔案的。