Redis源碼剖析之robj(redisObject)

我們?cè)?a target="_blank">之前的文章中已經(jīng)了解過(guò)一部分Redis的數(shù)據(jù)結(jié)構(gòu)了吉嫩,尤其是dict 中講到,可以把redis看做一個(gè)hashtable,存儲(chǔ)了一堆的key-value糕非,今天就來(lái)看下key-value中value的主要存儲(chǔ)結(jié)構(gòu)redisObject(后文統(tǒng)稱robj)。
robj的詳細(xì)代碼見object.c

字段詳解

相對(duì)與其他幾個(gè)數(shù)據(jù)結(jié)構(gòu)邀窃,robj相對(duì)簡(jiǎn)單氨菇,因?yàn)橹话藥讉€(gè)字段,含義都很明確响牛。

typedef struct redisObject {
    unsigned type:4;       // 數(shù)據(jù)類型  integer  string  list  set
    unsigned encoding:4;
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). 
                            * redis用24個(gè)位來(lái)保存LRU和LFU的信息玷禽,當(dāng)使用LRU時(shí)保存上次
                            * 讀寫的時(shí)間戳(秒),使用LFU時(shí)保存上次時(shí)間戳(16位 min級(jí)) 保存近似統(tǒng)計(jì)數(shù)8位 */
    int refcount;          // 引用計(jì)數(shù) 
    void *ptr;              // 指針指向具體存儲(chǔ)的值赫段,類型用type區(qū)分
} robj;

核心就五個(gè)字段,我們分別來(lái)介紹下矢赁。

type(4位)

type是表示當(dāng)然robj里所存儲(chǔ)的數(shù)據(jù)類型糯笙,目前redis中包含以下幾種類型。

標(biāo)識(shí)符 含義
OBJ_STRING 0 字符串(string)
OBJ_LIST 1 列表(list)
OBJ_SET 2 集合(set)
OBJ_ZSET 3 有序集(zset)
OBJ_HASH 4 哈希表(hash)
OBJ_MODULE 5 模塊(module)
OBJ_STREAM 6 流(stream)

encoding(4位)

編碼方式撩银,如果說(shuō)每個(gè)類型只有一種方式给涕,那么其實(shí)type和encoding兩個(gè)字段只需要保留一個(gè)即可,但redis為了在各種情況下盡可能介紹內(nèi)存额获,對(duì)每種類型的數(shù)據(jù)在不同情況下有不同的編碼格式够庙,所以這里需要用額外的字段標(biāo)識(shí)出來(lái)。目前有以下幾種編碼(redis 6.2)抄邀。

標(biāo)識(shí)符 含義
OBJ_ENCODING_RAW 0 最原始的標(biāo)識(shí)方式耘眨,只有string才會(huì)用到
OBJ_ENCODING_INT 1 整數(shù)
OBJ_ENCODING_HT 2 dict
OBJ_ENCODING_ZIPMAP 3 zipmap 目前已經(jīng)不再使用
OBJ_ENCODING_LINKEDLIST 4 就的鏈表,現(xiàn)在已經(jīng)不再使用了
OBJ_ENCODING_ZIPLIST 5 ziplist
OBJ_ENCODING_INTSET 6 intset
OBJ_ENCODING_SKIPLIST 7 跳表 skiplist
OBJ_ENCODING_EMBSTR 8 嵌入式的sds
OBJ_ENCODING_QUICKLIST 9 快表 quicklist
OBJ_ENCODING_STREAM 10 流 stream

這里有個(gè)OBJ_ENCODING_EMBSTR境肾,這里著重介紹下剔难。

robj *createEmbeddedStringObject(const char *ptr, size_t len) {
    robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr8)+len+1);
    struct sdshdr8 *sh = (void*)(o+1);

    o->type = OBJ_STRING;
    o->encoding = OBJ_ENCODING_EMBSTR;
    o->ptr = sh+1;
    o->refcount = 1;
    if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
        o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL;
    } else {
        o->lru = LRU_CLOCK();
    }

    sh->len = len;
    sh->alloc = len;
    sh->flags = SDS_TYPE_8;
    if (ptr == SDS_NOINIT)
        sh->buf[len] = '\0';
    else if (ptr) {
        memcpy(sh->buf,ptr,len);
        sh->buf[len] = '\0';
    } else {
        memset(sh->buf,0,len+1);
    }
    return o;
}

從上面代碼就可以看出,它是robj和sds的一個(gè)結(jié)合奥喻,將sds直接放在robj里偶宫,這里限制最多可以存放44字節(jié)長(zhǎng)度的字符串。因?yàn)閞obj占16字節(jié)衫嵌,sdshdr8頭占3字節(jié)读宙,'\0'一個(gè)字節(jié),限制字符串最長(zhǎng)為44就可以保證在64個(gè)字節(jié)里存放下所有內(nèi)容 (16+3+1+44==64)楔绞。

lru(24位)

眾所周知结闸,redis提供了過(guò)期數(shù)據(jù)自動(dòng)淘汰的策略,如何知道數(shù)據(jù)是否已經(jīng)過(guò)期酒朵?按照什么樣的策略淘汰數(shù)據(jù)桦锄?這倆問(wèn)題的答案都和 lru 這個(gè)字段有關(guān)。redis給了lru這個(gè)字段24位蔫耽,但千萬(wàn)別以為字段名叫l(wèi)ru就認(rèn)為它只是LRU淘汰策略中才會(huì)使用的结耀,其實(shí)LFU用的也是這個(gè)字段。 我估計(jì)是redis作者先寫了lru策略匙铡,所以直接就叫l(wèi)ru了图甜,后來(lái)再加lfu策略的時(shí)候直接復(fù)用這個(gè)字段了。
lru字段在不同淘汰策略時(shí)有不同的含義鳖眼。當(dāng)使用LRU時(shí)黑毅,它就是一個(gè)24位的秒級(jí)unix時(shí)間戳,代表這個(gè)數(shù)據(jù)在第多少秒被更新過(guò)钦讳。 但使用LFU策略時(shí)矿瘦,24位會(huì)被分為兩部分枕面,16位的分鐘級(jí)時(shí)間戳和8位的特殊計(jì)數(shù)器,這里就不再詳解了缚去,更具體可以關(guān)注我后續(xù)的博文潮秘。

refcount

引用計(jì)數(shù),表示這個(gè)robj目前被多少個(gè)地方應(yīng)用易结,refcount的出現(xiàn)為對(duì)象復(fù)用提供了基礎(chǔ)枕荞。了解過(guò)垃圾回收的同學(xué)都知道有中回收策略就是采用計(jì)數(shù)器的方式,當(dāng)refcount為0時(shí)衬衬,說(shuō)明該對(duì)象已經(jīng)沒用了买猖,就可以被回收掉了,redis的作者也實(shí)現(xiàn)了這種引用回收的策略滋尉。

*ptr

這個(gè)就很簡(jiǎn)單了玉控,前面幾個(gè)字段是為當(dāng)然robj提供meta信息,那這個(gè)字段就是數(shù)據(jù)具體所在地址狮惜。

robj的編解碼

redis向來(lái)將內(nèi)存空間節(jié)省做到了極致高诺,這里redis的作者又對(duì)字符串類型的robj做了特殊的編碼處理,以達(dá)到節(jié)省內(nèi)存的目的碾篡,編碼過(guò)程的代碼及注釋如下:

/* 將string類型的robj做特殊編碼虱而,以節(jié)省存儲(chǔ)空間  */
robj *tryObjectEncoding(robj *o) {
    long value;
    sds s = o->ptr;
    size_t len;

    /* Make sure this is a string object, the only type we encode
     * in this function. Other types use encoded memory efficient
     * representations but are handled by the commands implementing
     * the type. 
     * 這里只編碼string對(duì)象,其他類型的的編碼都由其對(duì)應(yīng)的實(shí)現(xiàn)處理 */
    serverAssertWithInfo(NULL,o,o->type == OBJ_STRING);

    /* We try some specialized encoding only for objects that are
     * RAW or EMBSTR encoded, in other words objects that are still
     * in represented by an actually array of chars.
     * 非sds string直接返回原數(shù)據(jù) */
    if (!sdsEncodedObject(o)) return o;

    /* It's not safe to encode shared objects: shared objects can be shared
     * everywhere in the "object space" of Redis and may end in places where
     * they are not handled. We handle them only as values in the keyspace. 
     * 如果是共享的對(duì)象开泽,不能編碼牡拇,因?yàn)榭赡軙?huì)影響到其他地方的使用*/
     if (o->refcount > 1) return o;

    /* Check if we can represent this string as a long integer.
     * Note that we are sure that a string larger than 20 chars is not
     * representable as a 32 nor 64 bit integer. 
     * 檢查是否可以把字符串表示為一個(gè)長(zhǎng)整型數(shù)。注意如果長(zhǎng)度大于20個(gè)字符的字符串是
     * 不能被表示為32或者64位的整數(shù)的*/
    len = sdslen(s);
    if (len <= 20 && string2l(s,len,&value)) {
        /* This object is encodable as a long. Try to use a shared object.
         * Note that we avoid using shared integers when maxmemory is used
         * because every object needs to have a private LRU field for the LRU
         * algorithm to work well. 
         * 如果可以被編碼為long型穆律,且編碼后的值小于OBJ_SHARED_INTEGERS(10000)惠呼,且未配
         * 置LRU替換淘汰策略, 就使用這個(gè)數(shù)的共享對(duì)象,相當(dāng)于所有小于10000的數(shù)都是用的同一個(gè)robj*/
        if ((server.maxmemory == 0 ||
            !(server.maxmemory_policy & MAXMEMORY_FLAG_NO_SHARED_INTEGERS)) &&
            value >= 0 &&
            value < OBJ_SHARED_INTEGERS)
        {
            decrRefCount(o);
            incrRefCount(shared.integers[value]);
            return shared.integers[value];
        } else {
            /* 否則原來(lái)如果是RAW類型峦耘,直接轉(zhuǎn)為OBJ_ENCODING_INT類型剔蹋,然后用long來(lái)直接存儲(chǔ)字符串 */    
            if (o->encoding == OBJ_ENCODING_RAW) {
                sdsfree(o->ptr);
                o->encoding = OBJ_ENCODING_INT;
                o->ptr = (void*) value;
                return o;
            /*如果是OBJ_ENCODING_EMBSTR,也會(huì)轉(zhuǎn)化為OBJ_ENCODING_INT辅髓,并用long存儲(chǔ)字符串*/
            } else if (o->encoding == OBJ_ENCODING_EMBSTR) {
                decrRefCount(o);
                return createStringObjectFromLongLongForValue(value);
            }
        }
    }
    // 對(duì)于那些無(wú)法轉(zhuǎn)為long的字符串泣崩,做如下處理

    /* If the string is small and is still RAW encoded,
     * try the EMBSTR encoding which is more efficient.
     * In this representation the object and the SDS string are allocated
     * in the same chunk of memory to save space and cache misses. 
     * 如果字符串太小,長(zhǎng)度小于等于44洛口,直接轉(zhuǎn)為OBJ_ENCODING_EMBSTR*/
    if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT) {
        robj *emb;

        if (o->encoding == OBJ_ENCODING_EMBSTR) return o;
        emb = createEmbeddedStringObject(s,sdslen(s));
        decrRefCount(o);
        return emb;
    }

    /* We can't encode the object...
     *
     * Do the last try, and at least optimize the SDS string inside
     * the string object to require little space, in case there
     * is more than 10% of free space at the end of the SDS string.
     *
     * We do that only for relatively large strings as this branch
     * is only entered if the length of the string is greater than
     * OBJ_ENCODING_EMBSTR_SIZE_LIMIT. 
     * 
     * 如果前面沒有編碼成功矫付,這里做最后一次嘗試,如果sds有超過(guò)10%的可用空閑空間第焰,
     * 且字符長(zhǎng)度大于OBJ_ENCODING_EMBSTR_SIZE_LIMIT(44)那嘗試釋放sds中多余
     * 的空間以節(jié)省內(nèi)存技即。
     **/
    trimStringObjectIfNeeded(o);

    /* 直接返回原始對(duì)象. */
    return o;
}
  1. 檢查是否是字符串,如果不是直接返回樟遣。
  2. 檢查是否是共享對(duì)象(refcount > 1)而叼,被共享的對(duì)象不做編碼。
  3. 如果字符串長(zhǎng)度小于等于20豹悬,直接可以編碼為一個(gè)long型的整數(shù)葵陵,這里小于10000的long對(duì)象都是共享的。
  4. 如果字符串長(zhǎng)度小于等于44瞻佛,直接用OBJ_ENCODING_EMBSTR存儲(chǔ)脱篙。
  5. 如果沒有被編碼,且字符串長(zhǎng)度超過(guò)44伤柄,且sds中的空閑空間超過(guò)10%绊困,則清除空閑空間,以節(jié)省內(nèi)存适刀。

當(dāng)然有編碼就有解碼秤朗,代碼及如下,相對(duì)比較簡(jiǎn)單:

/* Get a decoded version of an encoded object (returned as a new object).
 * If the object is already raw-encoded just increment the ref count.
 * 獲取解碼后的對(duì)象(返回的是有個(gè)新對(duì)象)笔喉,如果這個(gè)對(duì)象是個(gè)原始類型取视,只是把引用加一。 */
robj *getDecodedObject(robj *o) {
    robj *dec;

    if (sdsEncodedObject(o)) {
        incrRefCount(o);
        return o;
    }
    if (o->type == OBJ_STRING && o->encoding == OBJ_ENCODING_INT) {
        char buf[32];

        ll2string(buf,32,(long)o->ptr);
        dec = createStringObject(buf,strlen(buf));
        return dec;
    } else {
        serverPanic("Unknown encoding type");
    }
}

引用計(jì)數(shù)和自動(dòng)清理

上文已經(jīng)說(shuō)到了常挚,redis為了節(jié)省空間作谭,會(huì)復(fù)用一些對(duì)象,沒有引用的對(duì)象會(huì)被自動(dòng)清理奄毡。作者用了引用計(jì)數(shù)的方式來(lái)實(shí)現(xiàn)gc折欠,代碼也比較簡(jiǎn)單,如下:

void incrRefCount(robj *o) {
    if (o->refcount < OBJ_FIRST_SPECIAL_REFCOUNT) {
        o->refcount++;
    } else {
        if (o->refcount == OBJ_SHARED_REFCOUNT) {
            /* Nothing to do: this refcount is immutable. */
        } else if (o->refcount == OBJ_STATIC_REFCOUNT) {
            serverPanic("You tried to retain an object allocated in the stack");
        }
    }
}
/* 減少引用計(jì)數(shù)吼过,如果沒有引用了就釋放內(nèi)存空間 */
void decrRefCount(robj *o) {
    // 清理空間 
    if (o->refcount == 1) {
        switch(o->type) {
        case OBJ_STRING: freeStringObject(o); break;
        case OBJ_LIST: freeListObject(o); break;
        case OBJ_SET: freeSetObject(o); break;
        case OBJ_ZSET: freeZsetObject(o); break;
        case OBJ_HASH: freeHashObject(o); break;
        case OBJ_MODULE: freeModuleObject(o); break;
        case OBJ_STREAM: freeStreamObject(o); break;
        default: serverPanic("Unknown object type"); break;
        }
        zfree(o);
    } else {
        if (o->refcount <= 0) serverPanic("decrRefCount against refcount <= 0");
        if (o->refcount != OBJ_SHARED_REFCOUNT) o->refcount--;
    }
}

總結(jié)

總結(jié)下锐秦,可以認(rèn)為robj有這樣幾個(gè)作用。

  1. 為所有類型的value提供一個(gè)統(tǒng)一的封裝那先。
  2. 為數(shù)據(jù)淘汰保存必要的信息农猬。
  3. 實(shí)現(xiàn)數(shù)據(jù)復(fù)用,和自動(dòng)gc功能售淡。

本文是Redis源碼剖析系列博文斤葱,同時(shí)也有與之對(duì)應(yīng)的Redis中文注釋版,有想深入學(xué)習(xí)Redis的同學(xué)揖闸,歡迎star和關(guān)注揍堕。
Redis中文注解版?zhèn)}庫(kù):https://github.com/xindoo/Redis
Redis源碼剖析專欄:https://zxs.io/s/1h
如果覺得本文對(duì)你有用,歡迎一鍵三連汤纸。
本文來(lái)自https://blog.csdn.net/xindoo

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末衩茸,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子贮泞,更是在濱河造成了極大的恐慌楞慈,老刑警劉巖幔烛,帶你破解...
    沈念sama閱讀 219,270評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異囊蓝,居然都是意外死亡饿悬,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門聚霜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)狡恬,“玉大人,你說(shuō)我怎么就攤上這事蝎宇〉芫ⅲ” “怎么了?”我有些...
    開封第一講書人閱讀 165,630評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵姥芥,是天一觀的道長(zhǎng)兔乞。 經(jīng)常有香客問(wèn)我,道長(zhǎng)撇眯,這世上最難降的妖魔是什么报嵌? 我笑而不...
    開封第一講書人閱讀 58,906評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮熊榛,結(jié)果婚禮上锚国,老公的妹妹穿的比我還像新娘。我一直安慰自己玄坦,他們只是感情好血筑,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,928評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著煎楣,像睡著了一般豺总。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上择懂,一...
    開封第一講書人閱讀 51,718評(píng)論 1 305
  • 那天喻喳,我揣著相機(jī)與錄音,去河邊找鬼困曙。 笑死表伦,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的慷丽。 我是一名探鬼主播蹦哼,決...
    沈念sama閱讀 40,442評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼要糊!你這毒婦竟也來(lái)了纲熏?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,345評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎局劲,沒想到半個(gè)月后勺拣,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,802評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡容握,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,984評(píng)論 3 337
  • 正文 我和宋清朗相戀三年宣脉,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片剔氏。...
    茶點(diǎn)故事閱讀 40,117評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖竹祷,靈堂內(nèi)的尸體忽然破棺而出谈跛,到底是詐尸還是另有隱情,我是刑警寧澤塑陵,帶...
    沈念sama閱讀 35,810評(píng)論 5 346
  • 正文 年R本政府宣布感憾,位于F島的核電站,受9級(jí)特大地震影響令花,放射性物質(zhì)發(fā)生泄漏阻桅。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,462評(píng)論 3 331
  • 文/蒙蒙 一兼都、第九天 我趴在偏房一處隱蔽的房頂上張望嫂沉。 院中可真熱鬧,春花似錦扮碧、人聲如沸趟章。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)蚓土。三九已至,卻和暖如春赖淤,著一層夾襖步出監(jiān)牢的瞬間蜀漆,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工咱旱, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留确丢,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,377評(píng)論 3 373
  • 正文 我出身青樓莽龟,卻偏偏與公主長(zhǎng)得像蠕嫁,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子毯盈,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,060評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容