成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

Redis源碼分析-壓縮列表ziplist

RyanQ / 2736人閱讀

摘要:用不用作為分界是因?yàn)槭堑闹担糜谂袛嗍欠竦竭_(dá)尾部。這種類型的節(jié)點(diǎn)的大小介于與之間,但是為了表示整數(shù),取出低四位之后會(huì)將其作為實(shí)際的值。當(dāng)小于字節(jié)時(shí),節(jié)點(diǎn)存為上圖的第二種類型,高位為,后續(xù)位表示的長(zhǎng)度。

// 文中引用的代碼來(lái)源于Redis3.2

前言

Redis是基于內(nèi)存的nosql,有些場(chǎng)景下為了節(jié)省內(nèi)存redis會(huì)用“時(shí)間”換“空間”。
ziplist就是很典型的例子。

介紹

ziplist是list鍵、hash鍵以及zset鍵的底層實(shí)現(xiàn)之一(3.0之后list鍵已經(jīng)不直接用ziplist和linkedlist作為底層實(shí)現(xiàn)了,取而代之的是quicklist
這些鍵的常規(guī)底層實(shí)現(xiàn)如下:

list鍵:雙向鏈表

hash鍵:字典dict

zset鍵:跳躍表zskiplist

但是當(dāng)list鍵里包含的元素較少、并且每個(gè)元素要么是小整數(shù)要么是長(zhǎng)度較小的字符串時(shí),redis將會(huì)用ziplist作為list鍵的底層實(shí)現(xiàn)。同理hash和zset在這種場(chǎng)景下也會(huì)使用ziplist。

既然已有底層結(jié)構(gòu)可以實(shí)現(xiàn)list、hash、zset鍵,為什么還要用ziplist呢?
當(dāng)然是為了節(jié)省內(nèi)存空間
我們先來(lái)看看ziplist是如何壓縮的

原理 整體布局

ziplist是由一系列特殊編碼的連續(xù)內(nèi)存塊組成的順序存儲(chǔ)結(jié)構(gòu),類似于數(shù)組,ziplist在內(nèi)存中是連續(xù)存儲(chǔ)的,但是不同于數(shù)組,為了節(jié)省內(nèi)存 ziplist的每個(gè)元素所占的內(nèi)存大小可以不同(數(shù)組中叫元素,ziplist叫節(jié)點(diǎn)entry,下文都用“節(jié)點(diǎn)”),每個(gè)節(jié)點(diǎn)可以用來(lái)存儲(chǔ)一個(gè)整數(shù)或者一個(gè)字符串。
下圖是ziplist在內(nèi)存中的布局

zlbytes: ziplist的長(zhǎng)度(單位: 字節(jié)),是一個(gè)32位無(wú)符號(hào)整數(shù)

zltail: ziplist最后一個(gè)節(jié)點(diǎn)的偏移量,反向遍歷ziplist或者pop尾部節(jié)點(diǎn)的時(shí)候有用。

zllen: ziplist的節(jié)點(diǎn)(entry)個(gè)數(shù)

entry: 節(jié)點(diǎn)

zlend: 值為0xFF,用于標(biāo)記ziplist的結(jié)尾

普通數(shù)組的遍歷是根據(jù)數(shù)組里存儲(chǔ)的數(shù)據(jù)類型 找到下一個(gè)元素的,例如int類型的數(shù)組訪問(wèn)下一個(gè)元素時(shí)每次只需要移動(dòng)一個(gè)sizeof(int)就行(實(shí)際上開(kāi)發(fā)者只需讓指針p+1就行,在這里引入sizeof(int)只是為了說(shuō)明區(qū)別)。
上文說(shuō)了,ziplist的每個(gè)節(jié)點(diǎn)的長(zhǎng)度是可以不一樣的,而我們面對(duì)不同長(zhǎng)度的節(jié)點(diǎn)又不可能直接sizeof(entry),那么它是怎么訪問(wèn)下一個(gè)節(jié)點(diǎn)呢?
ziplist將一些必要的偏移量信息記錄在了每一個(gè)節(jié)點(diǎn)里,使之能跳到上一個(gè)節(jié)點(diǎn)或下一個(gè)節(jié)點(diǎn)。
接下來(lái)我們看看節(jié)點(diǎn)的布局

節(jié)點(diǎn)的布局(entry)

每個(gè)節(jié)點(diǎn)由三部分組成:prevlength、encoding、data

prevlengh: 記錄上一個(gè)節(jié)點(diǎn)的長(zhǎng)度,為了方便反向遍歷ziplist

encoding: 當(dāng)前節(jié)點(diǎn)的編碼規(guī)則,下文會(huì)詳細(xì)說(shuō)

data: 當(dāng)前節(jié)點(diǎn)的值,可以是數(shù)字或字符串

為了節(jié)省內(nèi)存,根據(jù)上一個(gè)節(jié)點(diǎn)的長(zhǎng)度prevlength 可以將ziplist節(jié)點(diǎn)分為兩類:

entry的前8位小于254,則這8位就表示上一個(gè)節(jié)點(diǎn)的長(zhǎng)度

entry的前8位等于254,則意味著上一個(gè)節(jié)點(diǎn)的長(zhǎng)度無(wú)法用8位表示,后面32位才是真實(shí)的prevlength。用254 不用255(11111111)作為分界是因?yàn)?55是zlend的值,它用于判斷ziplist是否到達(dá)尾部。

根據(jù)當(dāng)前節(jié)點(diǎn)存儲(chǔ)的數(shù)據(jù)類型及長(zhǎng)度,可以將ziplist節(jié)點(diǎn)分為9類
其中整數(shù)節(jié)點(diǎn)分為6類:

整數(shù)節(jié)點(diǎn)的encoding的長(zhǎng)度為8位,其中高2位用來(lái)區(qū)分整數(shù)節(jié)點(diǎn)和字符串節(jié)點(diǎn)(高2位為11時(shí)是整數(shù)節(jié)點(diǎn)),低6位用來(lái)區(qū)分整數(shù)節(jié)點(diǎn)的類型,定義如下:

#define ZIP_INT_16B (0xc0 | 0<<4)//整數(shù)data,占16位(2字節(jié))
#define ZIP_INT_32B (0xc0 | 1<<4)//整數(shù)data,占32位(4字節(jié))
#define ZIP_INT_64B (0xc0 | 2<<4)//整數(shù)data,占64位(8字節(jié))
#define ZIP_INT_24B (0xc0 | 3<<4)//整數(shù)data,占24位(3字節(jié))
#define ZIP_INT_8B 0xfe //整數(shù)data,占8位(1字節(jié))
/* 4 bit integer immediate encoding */
//整數(shù)值1~13的節(jié)點(diǎn)沒(méi)有data,encoding的低四位用來(lái)表示data
#define ZIP_INT_IMM_MASK 0x0f
#define ZIP_INT_IMM_MIN 0xf1    /* 11110001 */
#define ZIP_INT_IMM_MAX 0xfd    /* 11111101 */

值得注意的是 最后一種encoding是存儲(chǔ)整數(shù)0~12的節(jié)點(diǎn)的encoding,它沒(méi)有額外的data部分,encoding的高4位表示這個(gè)類型,低4位就是它的data。這種類型的節(jié)點(diǎn)的encoding大小介于ZIP_INT_24B與ZIP_INT_8B之間(1~13),但是為了表示整數(shù)0,取出低四位xxxx之后會(huì)將其-1作為實(shí)際的data值(0~12)。在函數(shù)zipLoadInteger中,我們可以看到這種類型節(jié)點(diǎn)的取值方法:

...
 } else if (encoding >= ZIP_INT_IMM_MIN && encoding <= ZIP_INT_IMM_MAX) {
        ret = (encoding & ZIP_INT_IMM_MASK)-1;
 }
...

字符串節(jié)點(diǎn)分為3類:

當(dāng)data小于63字節(jié)時(shí)(2^6),節(jié)點(diǎn)存為上圖的第一種類型,高2位為00,低6位表示data的長(zhǎng)度。

當(dāng)data小于16383字節(jié)時(shí)(2^14),節(jié)點(diǎn)存為上圖的第二種類型,高2位為01,后續(xù)14位表示data的長(zhǎng)度。

當(dāng)data小于4294967296字節(jié)時(shí)(2^32),節(jié)點(diǎn)存為上圖的第二種類型,高2位為10,下一字節(jié)起連續(xù)32位表示data的長(zhǎng)度。

上圖可以看出:
不同于整數(shù)節(jié)點(diǎn)encoding永遠(yuǎn)是8位,字符串節(jié)點(diǎn)的encoding可以有8位、16位、40位三種長(zhǎng)度
相同encoding類型的整數(shù)節(jié)點(diǎn) data長(zhǎng)度是固定的,但是相同encoding類型的字符串節(jié)點(diǎn),data長(zhǎng)度取決于encoding后半部分的值。

#define ZIP_STR_06B (0 << 6)//字符串data,最多有2^6字節(jié)(encoding后半部分的length有6位,length決定data有多少字節(jié))
#define ZIP_STR_14B (1 << 6)//字符串data,最多有2^14字節(jié)
#define ZIP_STR_32B (2 << 6)//字符串data,最多有2^32字節(jié)

上文介紹了ziplist節(jié)點(diǎn)(entry)的分類,知道了節(jié)點(diǎn)可以細(xì)分為9種類型,那么當(dāng)遍歷一個(gè)ziplist時(shí),指針到達(dá)某個(gè)節(jié)點(diǎn)時(shí) 如何判斷出節(jié)點(diǎn)的類型從而找到data呢?

已知節(jié)點(diǎn)的位置,求data的值

根據(jù)圖2 entry布局 可以看出,若要算出data的偏移量,得先計(jì)算出prevlength所占內(nèi)存大?。?字節(jié)和5字節(jié)):

//根據(jù)ptr指向的entry,返回這個(gè)entry的prevlensize
#define ZIP_DECODE_PREVLENSIZE(ptr, prevlensize) do {                          
    if ((ptr)[0] < ZIP_BIGLEN) {                                               
        (prevlensize) = 1;                                                     
    } else {                                                                   
        (prevlensize) = 5;                                                     
    }                                                                          
} while(0);

接著再用ZIP_DECODE_LENGTH(ptr + prevlensize, encoding, lensize, len)算出encoding所占的字節(jié),返回給lensize;data所占的字節(jié)返回給len

//根據(jù)ptr指向的entry求出該entry的len(encoding里存的 data所占字節(jié))和lensize(encoding所占的字節(jié))
#define ZIP_DECODE_LENGTH(ptr, encoding, lensize, len) do {                    
    ZIP_ENTRY_ENCODING((ptr), (encoding));                                     
    if ((encoding) < ZIP_STR_MASK) {                                           
        if ((encoding) == ZIP_STR_06B) {                                       
            (lensize) = 1;                                                     
            (len) = (ptr)[0] & 0x3f;                                           
        } else if ((encoding) == ZIP_STR_14B) {                                
            (lensize) = 2;                                                     
            (len) = (((ptr)[0] & 0x3f) << 8) | (ptr)[1];                       
        } else if (encoding == ZIP_STR_32B) {                                  
            (lensize) = 5;                                                     
            (len) = ((ptr)[1] << 24) |                                         
                    ((ptr)[2] << 16) |                                         
                    ((ptr)[3] <<  8) |                                         
                    ((ptr)[4]);                                                
        } else {                                                               
            assert(NULL);                                                      
        }                                                                      
    } else {                                                                   
        (lensize) = 1;                                                         
        (len) = zipIntSize(encoding);                                          
    }                                                                          
} while(0);

//將ptr的encoding解析成1個(gè)字節(jié):00000000、01000000、10000000(字符串類型)和11??????(整數(shù)類型)
//如果是整數(shù)類型,encoding直接照抄ptr的;如果是字符串類型,encoding被截?cái)喑梢粋€(gè)字節(jié)并清零后6位
#define ZIP_ENTRY_ENCODING(ptr, encoding) do {  
    (encoding) = (ptr[0]); 
    if ((encoding) < ZIP_STR_MASK) (encoding) &= ZIP_STR_MASK; 
} while(0)

//根據(jù)encoding返回?cái)?shù)據(jù)(整數(shù))所占字節(jié)數(shù)
unsigned int zipIntSize(unsigned char encoding) {
    switch(encoding) {
    case ZIP_INT_8B:  return 1;
    case ZIP_INT_16B: return 2;
    case ZIP_INT_24B: return 3;
    case ZIP_INT_32B: return 4;
    case ZIP_INT_64B: return 8;
    default: return 0; /* 4 bit immediate */
    }
    assert(NULL);
    return 0;
}

完成以上步驟之后,即可算出data的位置:ptr+prevlensize+lensize,以及data的長(zhǎng)度len

ziplist接口

上文已經(jīng)闡述了ziplist的底層內(nèi)存布局,接下來(lái)看看一些基本的增刪改查操作在ziplist中是如何執(zhí)行的。

ziplistNew 創(chuàng)建一個(gè)ziplist O(1)
/* Create a new empty ziplist. */
unsigned char *ziplistNew(void) {
    unsigned int bytes = ZIPLIST_HEADER_SIZE+1;//4字節(jié)4字節(jié)2字節(jié)1字節(jié),沒(méi)有entry節(jié)點(diǎn)
    unsigned char *zl = zmalloc(bytes);
    ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);//賦值
    ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);//
    ZIPLIST_LENGTH(zl) = 0;//
    zl[bytes-1] = ZIP_END;//
    return zl;
}
#define ZIPLIST_HEADER_SIZE     (sizeof(uint32_t)*2+sizeof(uint16_t))//空ziplist除了的大小
#define ZIPLIST_BYTES(zl)       (*((uint32_t*)(zl)))//的指針的值,可讀可寫
#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))//的指針的值
#define ZIPLIST_HEADER_SIZE     (sizeof(uint32_t)*2+sizeof(uint16_t))//空ziplist除了的大小
#define ZIPLIST_LENGTH(zl)      (*((uint16_t*)((zl)+sizeof(uint32_t)*2)))//的指針的值

參照著圖1理解會(huì)直觀些,分配了一塊內(nèi)存并初始化,沒(méi)有entry。

ziplistFind 從ziplist里找出一個(gè)entry O(n)
//返回p節(jié)點(diǎn)之后data與vstr(長(zhǎng)度是vlen)相等的節(jié)點(diǎn),只找p節(jié)點(diǎn)之后每隔skip的節(jié)點(diǎn)
//時(shí)間復(fù)雜度 O(n)
unsigned char *ziplistFind(unsigned char *p, unsigned char *vstr, unsigned int vlen, unsigned int skip) {
    int skipcnt = 0;
    unsigned char vencoding = 0;
    long long vll = 0;

    while (p[0] != ZIP_END) {
        unsigned int prevlensize, encoding, lensize, len;
        unsigned char *q;

        ZIP_DECODE_PREVLENSIZE(p, prevlensize);
        ZIP_DECODE_LENGTH(p + prevlensize, encoding, lensize, len);
        q = p + prevlensize + lensize;//當(dāng)前節(jié)點(diǎn)的data

        if (skipcnt == 0) {
            /* Compare current entry with specified entry */
            if (ZIP_IS_STR(encoding)) {//判斷當(dāng)前節(jié)點(diǎn)是不是字符串節(jié)點(diǎn)
                if (len == vlen && memcmp(q, vstr, vlen) == 0) {
                    return p;
                }
            } else {
                /* Find out if the searched field can be encoded. Note that
                 * we do it only the first time, once done vencoding is set
                 * to non-zero and vll is set to the integer value. */
                if (vencoding == 0) {//這個(gè)代碼塊只會(huì)執(zhí)行一次,計(jì)算vstr的整數(shù)表示
                    if (!zipTryEncoding(vstr, vlen, &vll, &vencoding)) {
                        //將參數(shù)給的節(jié)點(diǎn)vstr當(dāng)做整數(shù)節(jié)點(diǎn)轉(zhuǎn)換;將data值返回給vll,節(jié)點(diǎn)編碼返回給vencoding
                        //進(jìn)入這個(gè)代碼塊說(shuō)明將vstr轉(zhuǎn)換成整數(shù)失敗,vencoding不變,下次判斷當(dāng)前節(jié)點(diǎn)是整數(shù)節(jié)點(diǎn)之后可以跳過(guò)這個(gè)節(jié)點(diǎn)
                        /* If the entry can"t be encoded we set it to
                         * UCHAR_MAX so that we don"t retry again the next
                         * time. */
                        vencoding = UCHAR_MAX;//當(dāng)前節(jié)點(diǎn)是整數(shù)節(jié)點(diǎn),但是vstr是字符串節(jié)點(diǎn),跳過(guò)不用比較了
                    }
                    /* Must be non-zero by now */
                    assert(vencoding);
                }

                /* Compare current entry with specified entry, do it only
                 * if vencoding != UCHAR_MAX because if there is no encoding
                 * possible for the field it can"t be a valid integer. */
                if (vencoding != UCHAR_MAX) {
                    long long ll = zipLoadInteger(q, encoding);//算出當(dāng)前節(jié)點(diǎn)的data
                    if (ll == vll) {
                        return p;
                    }
                }
            }

            /* Reset skip count */
            skipcnt = skip;
        } else {
            /* Skip entry */
            skipcnt--;
        }

        /* Move to next entry */
        p = q + len;
    }

    return NULL;
}

//嘗試將entry地址的內(nèi)容轉(zhuǎn)換成整數(shù),并根據(jù)這個(gè)整數(shù)算出一個(gè)合適的encoding返回給encoding參數(shù)。
//若無(wú)法轉(zhuǎn)換成整數(shù),則encoding不變,返回0,等到下次調(diào)用zipEncodeLength時(shí)再計(jì)算一個(gè)該字符串的encoding
int zipTryEncoding(unsigned char *entry, unsigned int entrylen, long long *v, unsigned char *encoding) {
    long long value;

    if (entrylen >= 32 || entrylen == 0) return 0;
    if (string2ll((char*)entry,entrylen,&value)) {
        /* Great, the string can be encoded. Check what"s the smallest
         * of our encoding types that can hold this value. */
        if (value >= 0 && value <= 12) {
            *encoding = ZIP_INT_IMM_MIN+value;
        } else if (value >= INT8_MIN && value <= INT8_MAX) {
            *encoding = ZIP_INT_8B;
        } else if (value >= INT16_MIN && value <= INT16_MAX) {
            *encoding = ZIP_INT_16B;
        } else if (value >= INT24_MIN && value <= INT24_MAX) {
            *encoding = ZIP_INT_24B;
        } else if (value >= INT32_MIN && value <= INT32_MAX) {
            *encoding = ZIP_INT_32B;
        } else {
            *encoding = ZIP_INT_64B;
        }
        *v = value;
        return 1;
    }
    return 0;
}

/* Read integer encoded as "encoding" from "p" */
int64_t zipLoadInteger(unsigned char *p, unsigned char encoding) {
    int16_t i16;
    int32_t i32;
    int64_t i64, ret = 0;
    if (encoding == ZIP_INT_8B) {
        ret = ((int8_t*)p)[0];
    } else if (encoding == ZIP_INT_16B) {
        memcpy(&i16,p,sizeof(i16));
        memrev16ifbe(&i16);
        ret = i16;
    } else if (encoding == ZIP_INT_32B) {
        memcpy(&i32,p,sizeof(i32));
        memrev32ifbe(&i32);
        ret = i32;
    } else if (encoding == ZIP_INT_24B) {
        i32 = 0;
        memcpy(((uint8_t*)&i32)+1,p,sizeof(i32)-sizeof(uint8_t));
        memrev32ifbe(&i32);
        ret = i32>>8;
    } else if (encoding == ZIP_INT_64B) {
        memcpy(&i64,p,sizeof(i64));
        memrev64ifbe(&i64);
        ret = i64;
    } else if (encoding >= ZIP_INT_IMM_MIN && encoding <= ZIP_INT_IMM_MAX) {
        ret = (encoding & ZIP_INT_IMM_MASK)-1;
    } else {
        assert(NULL);
    }
    return ret;
}
其他接口

ziplistInsert 往ziplist里插入一個(gè)entry 時(shí)間復(fù)雜度 平均:O(n), 最壞:O(n2)

ziplistDelete 從siplist里刪除一個(gè)entry 時(shí)間復(fù)雜度 平均:O(n), 最壞:O(n2)

為什么插入節(jié)點(diǎn)和刪除節(jié)點(diǎn)兩個(gè)接口的最壞時(shí)間復(fù)雜度會(huì)是O(n2)呢?這是由于ziplist的“連鎖更新”導(dǎo)致的,連鎖更新在最壞情況下需要對(duì)ziplist執(zhí)行n次空間重分配操作,而且每次空間重分配的最壞時(shí)間復(fù)雜度為O(n) ----《Redis設(shè)計(jì)與實(shí)現(xiàn)》
但是出現(xiàn)“連鎖更新”的情況并不多見(jiàn),所以這里基本不會(huì)造成性能問(wèn)題。
篇幅有限這里不能細(xì)說(shuō)連鎖更新,感興趣可以閱讀《Redis設(shè)計(jì)與實(shí)現(xiàn)》的相關(guān)章節(jié)以及ziplist.c里的__ziplistCascadeUpdate()函數(shù)。

總結(jié)

ziplist是為節(jié)省內(nèi)存空間而生的。

ziplist是一個(gè)為Redis專門提供的底層數(shù)據(jù)結(jié)構(gòu)之一,本身可以有序也可以無(wú)序。當(dāng)作為listhash的底層實(shí)現(xiàn)時(shí),節(jié)點(diǎn)之間沒(méi)有順序;當(dāng)作為zset的底層實(shí)現(xiàn)時(shí),節(jié)點(diǎn)之間會(huì)按照大小順序排列。

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/29617.html

相關(guān)文章

  • Redis5源碼學(xué)習(xí)】2019-04-17 壓縮列表ziplist

    摘要:記錄壓縮列表總共占用的字節(jié)數(shù),在對(duì)壓縮列表進(jìn)行內(nèi)存重分配時(shí)使用個(gè)字節(jié)。為十六進(jìn)制值,標(biāo)記一個(gè)壓縮列表的末尾具體的數(shù)據(jù)存放在中。占用或字節(jié)表示當(dāng)前存儲(chǔ)數(shù)據(jù)的類型與數(shù)據(jù)的長(zhǎng)度。在學(xué)習(xí)的同時(shí),如果沒(méi)有經(jīng)過(guò)自己的思考,收效甚微。 baiyan全部視頻:https://segmentfault.com/a/11... 為什么需要ziplist 乍一看標(biāo)題,我們可能還不知道ziplist是何許人也...

    church 評(píng)論0 收藏0
  • 跟著大彬讀源碼 - Redis 6 - 對(duì)象和數(shù)據(jù)類型(下)

    摘要:哈希對(duì)象哈希對(duì)象的可選編碼分別是和。編碼的哈希對(duì)象編碼的哈希對(duì)象使用壓縮列表作為底層實(shí)現(xiàn)。關(guān)于哈希編碼轉(zhuǎn)換的函數(shù),可以參考,源碼如下是原始對(duì)象,是目標(biāo)編碼。對(duì)應(yīng)源碼如下對(duì)象元素?cái)?shù)量為,或者總結(jié)哈希對(duì)象有和編碼。 繼續(xù)擼我們的對(duì)象和數(shù)據(jù)類型。 上節(jié)我們一起認(rèn)識(shí)了字符串和列表,接下來(lái)還有哈希、集合和有序集合。 1 哈希對(duì)象 哈希對(duì)象的可選編碼分別是:ziplist 和 hashtable。...

    YFan 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<