Redis內存佔用計算

SDS

Redis對字符串進行了封裝,定義以下:redis

typedef char *sds;

struct sdshdr {
    unsigned int len;
    unsigned int free;
    char buf[];
};

其實就是給字符串前面多加了兩個unsigned int來保存字符串信息,len是總長度,free是當前可用長度。
假設當前有一個字符串"aaa",那麼經過sds來保存它最少須要多少個字節呢,計算方式以下:數據結構

4(len)+4(free)+3(「aaa」)+1(‘\\0’) = 12

也就是說經過sds來保存一個字符串,會在字符串實際佔用之上多佔用9個字節來存放額外的信息。函數

Object

sds僅僅是對字符串的封裝,Redis還對其封裝了一層RedisObject,定義以下:ui

typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
    int refcount;
    void *ptr;
} robj;

其中type佔用4bit,encoding佔用4bit,lru佔用24bit,refcount佔用4Byte,ptr佔用8Byte,總共佔用16字節。以"aaa"爲例,封裝成sds佔用3+9=12字節,再封裝成robj,實際佔用12+16=28字節,其中ptr指向sds,因此對字符串的robj的內存佔用公式能夠總結爲:指針

N + 9 + 16

其中N爲字符串長度;code

注:robj的ptr並不老是保存實際內容,假設字符串爲"123",type爲 REDIS_STRING,Redis會在tryObjectEncoding函數中判斷obj的ptr指向的sds能不能轉化成整數,若是能夠,那麼直接將ptr的值賦爲123,並釋放以前的sds。而此時encoding爲REDIS_ENCODING_INT代表這個ptr保存的是一個整數,因此實際佔用僅爲一個robj的大小,即16字節。

計算

前面已經介紹了Redis封裝的數據結構,如今能夠開始計算了,咱們以Redis接收到客戶端"set aaa bbb"命令以後,
第一步就是參數解析,直接看關鍵代碼:
首先是協議解析,調用棧是readQueryFromClient -> processInputBuffer -> processMultibulkBuffer,在processMultibulkBuffer中會對每一個參數調用createStringObject生成robj,先來看createStringObject的實現:server

robj *createObject(int type, void *ptr) {
    robj *o = zmalloc(sizeof(*o));
    o->type = type;
    o->encoding = REDIS_ENCODING_RAW;
    o->ptr = ptr;
    o->refcount = 1;

    /* Set the LRU to the current lruclock (minutes resolution). */
    o->lru = server.lruclock;
    return o;
}

那麼對於」set」,」aaa」,」bbb」會生成3個robj,各佔28個字節,不過對於set這個robj僅僅是用來查找命令lookupCommand使用,後來會釋放,對」aaa」這個robj也是會用新的」aaa」的sds來存儲,robj也會釋放,前兩個不計算在內,因此目前僅有」bbb」總共佔56字節。索引

第二步,命令的執行,調用棧是processCommand -> call -> proc(此處爲setcommand) -> setGenericCommand -> setKey,最終數據是在setKey中被存在db中,這裏有一點特殊說明一下,在setcommand調用setGenericCommand以前會調用內存

c->argv[2] = tryObjectEncoding(c->argv[2]);

這裏會對命令中的value進行上面說的tryObjectEncoding,此處argv[2]是包含」bbb」的robj,因此這裏tryObjectEncoding後,這個robj不會變小,但若是此處是包含」123」的robj,那麼通過tryObjectEncoding後,大小會從28變爲16(具體緣由參考Object一節__部分)element

接着往下看,setKey的定義以下:

void setKey(redisDb *db, robj *key, robj *val) {
    if (lookupKeyWrite(db,key) == NULL) {
        dbAdd(db,key,val);
    } else {
        dbOverwrite(db,key,val);
    }
    incrRefCount(val);
    removeExpire(db,key);
    signalModifiedKey(db,key);
}

能夠看到咱們會將」aaa」和」bbb」的robj指針做爲第2、三參數傳給它,這裏假設這個key以前不存在,那麼會調用dbAdd把他插入到db中(db實際是個hash表)

void dbAdd(redisDb *db, robj *key, robj *val) {
    sds copy = sdsdup(key->ptr); // 將key的robj轉換爲對應的sds,在dict中的key用
    //sds的形式存
    int retval = dictAdd(db->dict, copy, val);

    redisAssertWithInfo(NULL,key,retval == REDIS_OK);
    if (val->type == REDIS_LIST) signalListAsReady(db, key);
}

int dictAdd(dict *d, void *key, void *val)
{
    dictEntry *entry = dictAddRaw(d,key); //生成新的dictEntry並賦值key字段

    if (!entry) return DICT_ERR;
    dictSetVal(d, entry, val); //給dictEntry的v字段賦值,指向包含"bbb"的robj
    return DICT_OK;
}

dictEntry *dictAddRaw(dict *d, void *key)
{
    int index;
    dictEntry *entry;
    dictht *ht;

    if (dictIsRehashing(d)) _dictRehashStep(d);

    /* Get the index of the new element, or -1 if
     * the element already exists. */
    if ((index = _dictKeyIndex(d, key)) == -1)
        return NULL;

    /* Allocate the memory and store the new entry */
    ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
    entry = zmalloc(sizeof(*entry)); //生成新的dictEntry
    entry->next = ht->table[index]; //插入到index位置的桶中
    ht->table[index] = entry;
    ht->used++;

    /* Set the hash entry fields. */
    dictSetKey(d, entry, key); //給dictEntry的key字段賦值,指向"aaa"的sds
    return entry;
}

dbAdd調用dictAdd,最終由dictAdd將一個sds和一個object插入到db中,說是插入,其實就是對這組鍵值對調用dictAddRaw生成一個dictEntry,並把他插入到按key求hash值索引到的桶中,說到這裏已經明確了,這組鍵值對最終實際保存的位置就是在dictEntry中,它的大小就是最終實際大小,來看定義

typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;

一個dictEntry的大小是8(key)+8(v)+8(next) = 24字節,key是一個」aaa」的sds指針,v是一個指向包含」bbb」的robj的指針,next是指向對應桶中第二個dictEntry的指針。

結論

  • 在執行"set aaa bbb"命令以後,Redis會用24(dictEntry)+12(sds(「aaa」))+28(robj(「bbb」)) = 64字節來存儲;
  • 在執行」set aaa 10000」命令後,redis會用24(dictEntry)+12(sds(「aaa」))+16(robj(「10000」)) = 52字節來存儲(redis對整數10000以內robj建立了shared object,也就是說若是這裏不是10000而是123的話,不會爲123新建立robj而是直接增長shared object中已有123的robj的計數,這樣空間佔用更小);
  • 上面說的64和52只是redis申請的字節數,實際佔用還要根據具體allocator來看;
相關文章
相關標籤/搜索