redis代碼結構之三類型庫-list

來源:互聯網
上載者:User

redis代碼結構之三類型庫-list 

1. REDIS_LIST(t_list.c)

該類型的命令包括:lpush,rpush,lpop,rpop等等。這裡我們只介紹lpush命令,它相應的命令回呼函數為void lpushCommand(redisClient *c) { pushGenericCommand(c,REDIS_HEAD);}我們直接看pushGenericCommand

void pushGenericCommand(redisClient *c, int where) {    int j, addlen = 0, pushed = 0;    robj *lobj = lookupKeyWrite(c->db,c->argv[1]); //尋找key是否存在,存在的話返回的是它的subobject    int may_have_waiting_clients = (lobj == NULL); //key不存在    if (lobj && lobj->type != REDIS_LIST) {//如果key存在,則它的type必定是REDIS_LIST        addReply(c,shared.wrongtypeerr);        return;    }    for (j = 2; j < c->argc; j++) {        c->argv[j] = tryObjectEncoding(c->argv[j]);        if (may_have_waiting_clients) {            if (handleClientsWaitingListPush(c,c->argv[1],c->argv[j])) { //判斷是否有block的pop key,這種情況是由於blpop引起的                addlen++;                continue;            } else {                may_have_waiting_clients = 0;            }        }        if (!lobj) { //如果該key現在沒有value,只調用一次            lobj = createZiplistObject(); //一開始總是使用ziplist,並賦值,跟進去可以看到其實ziplist是字串數組            dbAdd(c->db,c->argv[1],lobj); //將key,robj value加入dict        }        listTypePush(lobj,c->argv[j],where); //將每個value加入到value list,並且該函數會判斷是否需要轉換儲存類型        pushed++;    }    addReplyLongLong(c,addlen + (lobj ? listTypeLength(lobj) : 0));    if (pushed) signalModifiedKey(c->db,c->argv[1]);    server.dirty += pushed;}

上面的幾個value有點彆扭,其實通過key得到的都是一個robj value,而這個裡面又包含了一個list或ziplist,這個list又是由多個robj value對象組成。下面我們看一下listTypePush函數:

void listTypePush(robj *subject, robj *value, int where) {    /* Check if we need to convert the ziplist */    listTypeTryConversion(subject,value); //查看是否需要將ziplist轉換為普通的LINKEDLIST    //如果當前是ziplist類型,並且長度已經大於配置的list_max_ziplist_entries時,則將ziplist轉換為LINKEDLIST    if (subject->encoding == REDIS_ENCODING_ZIPLIST &&        ziplistLen(subject->ptr) >= server.list_max_ziplist_entries)             listTypeConvert(subject,REDIS_ENCODING_LINKEDLIST);    if (subject->encoding == REDIS_ENCODING_ZIPLIST) {//仍然可以使用ziplist        int pos = (where == REDIS_HEAD) ? ZIPLIST_HEAD : ZIPLIST_TAIL;        value = getDecodedObject(value); //獲得string類型        subject->ptr = ziplistPush(subject->ptr,value->ptr,sdslen(value->ptr),pos);//ziplist的插入        decrRefCount(value);    } else if (subject->encoding == REDIS_ENCODING_LINKEDLIST) {//list類型,就是我們常見的雙向鏈表插入,這裡我們不再解釋        if (where == REDIS_HEAD) {            listAddNodeHead(subject->ptr,value);        } else {            listAddNodeTail(subject->ptr,value);        }        incrRefCount(value);    } else {        redisPanic("Unknown list encoding");    }}    //轉換類型,如果要儲存的value的長度大於配置的list_max_ziplist_value時也必須把原來的ziplist轉換為listvoid listTypeTryConversion(robj *subject, robj *value) {    if (subject->encoding != REDIS_ENCODING_ZIPLIST) return;    if (value->encoding == REDIS_ENCODING_RAW &&        sdslen(value->ptr) > server.list_max_ziplist_value)            listTypeConvert(subject,REDIS_ENCODING_LINKEDLIST);}

通過上面的代碼我們可以發現當要插入的value的長度(實際位元組長度)大於配置的list_max_ziplist_value時或者ziplist的長度(元素大小)大於server.list_max_ziplist_entries時把ziplist轉換為list類型。下面我們主要介紹一下ziplist的相關操作。

1.1   ziplist

/* Create a new empty ziplist. */unsigned char *ziplistNew(void) {    unsigned int bytes = ZIPLIST_HEADER_SIZE+1;    unsigned char *zl = zmalloc(bytes);    ZIPLIST_BYTES(zl) = bytes;    ZIPLIST_TAIL_OFFSET(zl) = ZIPLIST_HEADER_SIZE;    ZIPLIST_LENGTH(zl) = 0;    zl[bytes-1] = ZIP_END;    return zl;}//往ziplist中增加entry,zl就是ziplist字串數組;s為要插入的value的實際內容;slen為value長度,where是首或尾unsigned char *ziplistPush(unsigned char *zl, unsigned char *s, unsigned int slen, int where) {    unsigned char *p;//p為實際把插入的位置head的話就是從header後開始,TAIL就是從最後的一個位元組處開始    p = (where == ZIPLIST_HEAD) ? ZIPLIST_ENTRY_HEAD(zl) : ZIPLIST_ENTRY_END(zl);    return __ziplistInsert(zl,p,s,slen);}

通過上面的new可以看到ziplist的結構:ziplist是一個字串數組,它的格式為:
[header:<zlbytes><zltail><zllen>][body:<entry>…<entry>][end:<zlend>],其中前面三個又稱為ziplist header
zlbytes: uint32_t,儲存該字串數組的總長度,包括header,body,end;
zltail:uint32_t,用來儲存最後一個entry的位移,即最後一個節點的位置,允許pop操作,而不需要遍曆
zllen:uint16_t,用來儲存ziplist的entry個數,所以最多2^16-1個。
zlend:1byte結束標誌[255]
下面我們再來看一下body:entry的結構:(每個body也是由三個部分組成)
prevrawlen:儲存前一個節點的長度(這個len就是一個entry總長度),佔用1或5個位元組,當len少於254時佔1個位元組,否則前面一個位元組儲存254,後面4個位元組儲存實際的長度
lensize[curencode|curlen]:儲存當前節點使用的encode類型,如果是字串類型還必須包括長度;整型的話不需要長度,因為整型的長度是固定的。該欄位可能佔用:1,2,5,1,1,1個位元組。當內容不能轉換為long long的時候,必須使用字串來encode,此時如果串的長度小於63(2^6-1),則可以使用ZIP_STR_06B來encode,這個欄位就佔用1個位元組,其中前面兩位表示ZIP_STR_06B(00),後面6位表示實際的長度;如果串的長度小於16384(2^14-1),則可以使用ZIP_STR_14B來encode,這個欄位就佔用2個位元組,其中前面兩位表示ZIP_STR_14B(01),後面14位表示實際的長度;如果串的長度大於等於16384,則使用ZIP_STR_32B來encode,這個欄位就佔用5個位元組,其中前面一個位元組表示ZIP_STR_32B(1000,0000),後面4個位元組(32位)表示實際的長度;
如果內容能夠轉換為long long的時候,則把它轉換為相應的類型int16_t,int32_t,int64_t,這時該欄位就只需要佔用一個位元組,它們的高4位用來表示encode類型,分別(1100,1101,1110),此時不再需要內容的長度,因為每個int,它的大小是可能通過類型來獲得的。
value:儲存實際的內容,它就根據前面的encode來儲存的。佔用的長度,如果是字串,則由串的長度決定,如果的整形,則由每種類型自己決定(2,4,8)。如:

圖1 ziplist結構

下面我們看一下ziplist是怎麼進行插入:

static zlentry zipEntry(unsigned char *p) {    zlentry e;    e.prevrawlen = zipPrevDecodeLength(p,&e.prevrawlensize); //返回上一個節點的總共長度,並且把儲存這個長度值所佔用的位元組數儲存到e.prevrawlensize,即1或者5(見上面的解釋)    e.len = zipDecodeLength(p+e.prevrawlensize,&e.lensize); //返回當前節點的內容的長度,並且把lensize佔用的大小儲存到e.lensize,即1,2,5,1,1,1    e.headersize = e.prevrawlensize+e.lensize; //即每個節點的前面兩個欄位的長度,這個值加上上面的e.len就是該節點的實際長度    e.encoding = zipEntryEncoding(p+e.prevrawlensize); //返回當前節點的encode    e.p = p;    return e;}//zl為要操作的ziplist,p為當前操作的位置,該值會根據head|tail而不同,如果是head則是ziplist: header之後的位置;如果是tail則在ziplist:end:<zlend>的位置,s為實際內容,slen為實際長度static unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {    size_t curlen = ZIPLIST_BYTES(zl), reqlen, prevlen = 0;    size_t offset;    int nextdiff = 0;    unsigned char encoding = 0;    long long value;    zlentry entry, tail;    /* Find out prevlen for the entry that is inserted. */    if (p[0] != ZIP_END) {//這說明是從header insert        entry = zipEntry(p); //見上面的注釋        prevlen = entry.prevrawlen; //返回上一個節點的長度,如果從head insert的話顯然這個都是0    } else {//從tail insert        unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl); //獲得最後一個節點的起始位置        if (ptail[0] != ZIP_END) { //如果當前list非空            prevlen = zipRawEntryLength(ptail); //獲得最後一個節點的總共空間大小        }    }    /* See if the entry can be encoded 計算出儲存該內容實際應該佔用多少位元組 reqlen為該節點總共要使用的空間*/    if (zipTryEncoding(s,slen,&value,&encoding)) { //整形2,4,8        /* 'encoding' is set to the appropriate integer encoding */        reqlen = zipIntSize(encoding);    } else { //字串實際的長度        /* 'encoding' is untouched, however zipEncodeLength will use the         * string length to figure out how to encode it. */        reqlen = slen;    }    /* We need space for both the length of the previous entry and     * the length of the payload. */    reqlen += zipPrevEncodeLength(NULL,prevlen); //儲存上一個節點長度這個值需要佔用多少空間,即body第一個欄位佔有用的空間1或5    reqlen += zipEncodeLength(NULL,encoding,slen);//body第二個欄位佔用的空間1,2,5,1,1,1    /* When the insert position is not equal to the tail, we need to     * make sure that the next entry can hold this entry's length in     * its prevlen field. */    nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0; //如果是head insert的話,顯然如果此時的前端節點的prevrawlen就要改變,並且當要插入的節點的長度>245時,就需要增加4個位元組,原來的頭因為它的前一個是空所以只有一個位元組儲存0,而現在需要5個位元組來儲存這個新插入的節點的長度    /* Store offset because a realloc may change the address of zl. */    offset = p-zl;    zl = ziplistResize(zl,curlen+reqlen+nextdiff); //重新分配記憶體reqlen是當前插入節點的長度,nextdiff為需要增加的長度    p = zl+offset;    /* Apply memory move when necessary and update tail offset. */    if (p[0] != ZIP_END) { //header insert,顯然之前的內容還在前面,所以需要把它們往後移        /* Subtract one because of the ZIP_END bytes */        memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff); //這裡使用nextdiff是為了說明這nextdiff也是屬於之前的節點的內容        /* Encode this entry's raw length in the next entry. */        zipPrevEncodeLength(p+reqlen,reqlen); //更新之前head node的prevlen        /* Update offset for tail */        ZIPLIST_TAIL_OFFSET(zl) += reqlen;        /* When the tail contains more than one entry, we need to take         * "nextdiff" in account as well. Otherwise, a change in the         * size of prevlen doesn't have an effect on the *tail* offset. */        tail = zipEntry(p+reqlen);        if (p[reqlen+tail.headersize+tail.len] != ZIP_END)            ZIPLIST_TAIL_OFFSET(zl) += nextdiff;    } else { //tail insert        /* This element will be the new tail. */        ZIPLIST_TAIL_OFFSET(zl) = p-zl;    }    /* When nextdiff != 0, the raw length of the next entry has changed, so     * we need to cascade the update throughout the ziplist */    if (nextdiff != 0) { //如果新插入的節點len大於254時,即前一個節點的prevlen需要擴充到5個節點來儲存時,需要依次去遍曆整個list去修改每個node的prevlen,當前這個遍曆可能不會很長,因為從head開始也就是增加4個位元組,當某一個prevlen+4小於254時,該遍曆就結束了        offset = p-zl;        zl = __ziplistCascadeUpdate(zl,p+reqlen);        p = zl+offset;    }    /* Write the entry */    p += zipPrevEncodeLength(p,prevlen); //儲存當前插入的新節點的prevlen    p += zipEncodeLength(p,encoding,slen); //儲存當前插入新節點的lensize    if (ZIP_IS_STR(encoding)) { //儲存實際的內容        memcpy(p,s,slen);    } else {        zipSaveInteger(p,value,encoding);    }    ZIPLIST_INCR_LENGTH(zl,1);    return zl;}

總而言之,ziplist其本質是一個字串數組,它通過儲存每個節點的上一個節點的大小,以及當前結節的encode及實際長度,來達到兩個方面的遍曆。另外,從上面的操作可以看出當從head插入的時候,整個insert需要更多的過程:memmove,計算擴充值nextdiff,從而導致遍曆性的修改整個list的prevlen。但不管是從head還tail都需要一個realloc的過程,所以雖然ziplist可以節省記憶體,但是它是以cpu換mem。如果想減少使用,可以改小上面說的兩個配置。並且盡量使用rpush的操作,來得到更好的效能。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.