Redis深度歷險-壓縮列表 Redis深度歷險-壓縮列表

Redis深度歷險-壓縮列表

zsethash在元素個數較少時會採用壓縮列表來存儲以節省空間,主要代碼在ziplist.cziplist.h中;這是非常重要的數據結構,在zsethashlist的底層數據結構之一

設計思想

壓縮列表並沒有定義成結構體,而是以字符串的形式返回出去,必須要先明白壓縮列表的空間排布

壓縮列表

  • zlbytes:用以獲取整個壓縮列表的長度,4字節長
  • zltail_offset:用以獲取最後一個元素的地址,4字節長;這個字段是用來方便倒序查找的
  • zllength:存儲的元素個數,2字節
  • entry:元素,長度不固定
  • zlend:存儲的是特殊字符0xff,1字節長,用於標記壓縮列表末端

元素

即使是單個元素entry內部的排布也是很複雜的,主要是爲了儘可能節省空間

存儲類型

壓縮列表支持保存一個整數或者一個字符串,支持以下類型

  • ZIP_STR_06B:長度小於2^6的字符串
  • ZIP_STR_14B:長度小於2^14的字符串
  • ZIP_STR_32B:長度小於2^32的字符串
  • ZIP_INT_16B:2字節長的有符號整數
  • ZIP_INT_32B:4字節長的有符號整數
  • ZIP_INT_64B:8字節長的有符號整數
  • ZIP_INT_24B:3字節長的有符號整數
  • ZIP_INT_8B:1字節長的有符號整數
  • 0~12的極小值

不同類型的內存存儲方式是不一樣的

內存排布

  • prelen:存儲的是前一個元素的內容長度,分兩種情況
    • 前一個元素內容長度小於254字節,則使用1個字節存儲
    • 大於254字節的情況使用5個字節存儲,第1個字節設置爲標誌位oxfe,後4字節保存真正的長度
  • content:包含兩部分,數據的類型和長度encoding、數據;encoding長度不定,通過位標識類型
    • 00xxxxxx:對應於ZIP_STR_06B,後6位表示長度
    • 01xxxxxx xxxxxxxx:對應於ZIP_STR_14B,後14位表示長度
    • 10000000:對應於ZIP_STR_32B10後6位沒有使用,該字節的後4個字節共32位來表示長度
    • 1100000011010000111000001111000011111110用來表示2、4、8、3、1字節長度的數字,11111111表示結束即oxff
    • encoding除了上述的值被佔用了,後四位還有0001~1101可以用來表示極小值,即0~12

壓縮列表固然設計的很精細巧妙,但是是否有點過度設計了???代碼中的zlentry並不代表內存排布,只是便於操作而已;prelen的設計主要是方便從後往前的便利操作

集合中的壓縮列表

集合在數據個數小於128zset_max_ziplist_entries個且插入的字符串小於64zset_max_ziplist_value時會使用跳錶存儲,這兩個參數均可配

創建壓縮列表

/* Create a new empty ziplist. */
unsigned char *ziplistNew(void) {
    //空的壓縮列表只有:zlbyte、zltail_offset、zllength、zlend
    unsigned int bytes = ZIPLIST_HEADER_SIZE+ZIPLIST_END_SIZE;
    //分配空間
    unsigned char *zl = zmalloc(bytes);
    ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);
    ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);
    ZIPLIST_LENGTH(zl) = 0;
    //將最後一個字節標記爲0xff
    zl[bytes-1] = ZIP_END;
    return zl;
}

//獲取zlbyte的地址
#define ZIPLIST_BYTES(zl)       (*((uint32_t*)(zl)))
//獲取zltail_offset的地址
#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))
//獲取zllength的地址
#define ZIPLIST_LENGTH(zl)      (*((uint16_t*)((zl)+sizeof(uint32_t)*2)))

壓縮列表沒有固定的數據結構而是通過unsigned char*來表示,這裏用到了很多宏來計算空間

插入數據

zset中需要先根據score進行查找後插入到指定位置

unsigned char *zzlInsert(unsigned char *zl, sds ele, double score) {
    //定位到壓縮列表頭部
    unsigned char *eptr = ziplistIndex(zl,0), *sptr;
    double s;
        
    //元素在壓縮列表中從大到小按需存儲,這裏從前往後遍歷元素
    while (eptr != NULL) {
        //從前往後遍歷壓縮列表所有元素
        sptr = ziplistNext(zl,eptr);
        serverAssert(sptr != NULL);
        s = zzlGetScore(sptr);
                
        //優先比較分數、其次比較值
        if (s > score) {
            zl = zzlInsertAt(zl,eptr,ele,score);
            break;
        } else if (s == score) {
            if (zzlCompareElements(eptr,(unsigned char*)ele,sdslen(ele)) > 0) {
                zl = zzlInsertAt(zl,eptr,ele,score);
                break;
            }
        }

        eptr = ziplistNext(zl,sptr);
    }

    //如果插入的數據是最小的就插入在末尾
    if (eptr == NULL)
        zl = zzlInsertAt(zl,NULL,ele,score);
    return zl;
}

在有序集合中需要在業務上保證壓縮列表是有序的

哈希表中的壓縮列表

查找元素

int hashTypeGetFromZiplist(robj *o, sds field,
                           unsigned char **vstr,
                           unsigned int *vlen,
                           long long *vll)
{
    unsigned char *zl, *fptr = NULL, *vptr = NULL;
    int ret;

    serverAssert(o->encoding == OBJ_ENCODING_ZIPLIST);
        
    //調用ziplistFind查找元素
    zl = o->ptr;
    fptr = ziplistIndex(zl, ZIPLIST_HEAD);
    if (fptr != NULL) {
        fptr = ziplistFind(zl, fptr, (unsigned char*)field, sdslen(field), 1);
        if (fptr != NULL) {
            
            vptr = ziplistNext(zl, fptr);
            serverAssert(vptr != NULL);
        }
    }
        
    //獲取元素的數據
    if (vptr != NULL) {
        ret = ziplistGet(vptr, vstr, vlen, vll);
        serverAssert(ret);
        return 0;
    }

    return -1;
}

內部調用了壓縮列表的接口,本質就是遍歷對比所有的元素

總結

這裏不對代碼做詳細的分析,因爲涉及到大量的內存操作,邏輯必然非常複雜
學習一下設計思想和空間排布就可以了,壓縮列表僅在數據較少時可用,因爲涉及到大量的遍歷、數據複製移動操作

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章