[redis]SDS和連結串列

Biningo發表於2020-04-30

一、SDS

1、SDS結構體

redis3.2之前:不管buf的位元組數有多少,都用 4位元組的len來儲存長度,對於只存短字串那麼優點浪費空間,比如只存 name,則len=4 則只需要一個位元組8位即可表示

struct sdshdr {
    unsigned int len; // buf中已佔位元組數
    unsigned int free; // buf中剩餘位元組數
    char buf[]; // 資料空間
};

redis3.2之後:

struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; //已分配位元組數
    uint8_t alloc; //剩餘位元組數
    unsigned char flags; //標識屬於那種型別的SDS  低3存型別,高5不使用
    char buf[]; 
};
//........16、32、64

_attribute_ ((_packed_)) 關鍵字是為了取消位元組對齊

struct test1 {
 char c;
 int i;
};

struct __attribute__ ((__packed__)) test2 {
 char c;
 int i;
};

int main()
{
 cout << "size of test1:" << sizeof(struct test1) << endl;
 cout << "size of test2:" << sizeof(struct test2) << endl;
}

注意,這些結構都存在一個 char[]內,通過偏移來訪問

graph TB subgraph header-->buf end



2、重要函式解析

sdsReqType

確定型別:sdsReqType根據傳入的 char[] 長度來缺點應該用哪種型別的 SDS結構體來描述

static inline char sdsReqType(size_t string_size) {
    if (string_size < 1<<5)
        return SDS_TYPE_5;
    if (string_size < 1<<8) //8位 表示長度範圍 0-256
        return SDS_TYPE_8;
    if (string_size < 1<<16) //16位 
        return SDS_TYPE_16;
#if (LONG_MAX == LLONG_MAX)
    if (string_size < 1ll<<32)
        return SDS_TYPE_32;
    return SDS_TYPE_64;
#else
    return SDS_TYPE_32;
#endif
}

sdsnewlen

根據長度結構化 char陣列,建立一個 長度為 str本身長度+head長度的陣列, sdsnew就是呼叫這個來建立sds位元組陣列的

sds sdsnewlen(const void *init, size_t initlen) {
    void *sh; //存放sds header資料的頭指標
    sds s; //char* s
    char type = sdsReqType(initlen); //根據str長度 確定SDS header型別
    if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
    int hdrlen = sdsHdrSize(type); //header 長度
    unsigned char *fp; //型別指標

    sh = s_malloc(hdrlen+initlen+1); //分配 str長度+header長度的記憶體空間
    ...
    memset(sh, 0, hdrlen+initlen+1); //初始化空間
    s = (char*)sh+hdrlen; //移動到header之後的buf首地址位置
    fp = ((unsigned char*)s)-1; //移動到header的尾部 標識sds header型別
    switch(type) {
       ....
        case SDS_TYPE_8: {
//#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));  
  //sh指向header空間頭部位置 s代表buf首地址  下面將sh移動到header的首地址
        SDS_HDR_VAR(8,s); //struct sdshdr8* sh = (void*)(s-sizeof(header))
        sh->len = initlen; //填充資料
        sh->alloc = initlen; 
        *fp = type;//型別資料填充
        break;
       }
       ......
    }
    if (initlen && init)
        memcpy(s, init, initlen); //將str資料複製到buf中
    s[initlen] = '\0';
    return s;
}

sdslen、sdsavail

返回使用和未使用的空間。 **根據頭部型別轉化指標,然後直接 sh->len 和 sh->alloc-sh->len **即可求出


sdscat、sdscatlen、sdsMakeRoomFor

t拼接到 s 中,

sds sdscatsds(sds s, const sds t) {
    return sdscatlen(s, t, sdslen(t));
}
sds sdscatlen(sds s, const void *t, size_t len) {
    size_t curlen = sdslen(s);

    s = sdsMakeRoomFor(s,len); //保證空間充足
    if (s == NULL) return NULL;
    memcpy(s+curlen, t, len); //直接copy
    sdssetlen(s, curlen+len); //設定新的長度
    s[curlen+len] = '\0';
    return s;
}

sdsMakeRoomFor是為了保證空間充足,如果不充足進行擴容,下面就是newlen的核心程式碼,會擴容大於需要的長度,防止多次擴容。體現了 預先分配

擴容是一個耗時的操作

    if (avail >= addlen) return s;

    len = sdslen(s);
    sh = (char*)s-sdsHdrSize(oldtype);
    newlen = (len+addlen);
    if (newlen < SDS_MAX_PREALLOC) //#define SDS_MAX_PREALLOC (1024*1024)
        newlen *= 2;
    else
        newlen += SDS_MAX_PREALLOC;

sdstrim

將cset中在s出現的刪除,這個函式就體現了 惰性釋放 ,不會縮減空間,僅僅改變 len,同時也體現了 和 c的相容性,可以用 系統strings函式來操作 sds

sds sdstrim(sds s, const char *cset) {
    char *start, *end, *sp, *ep;
    size_t len;

    sp = start = s;
    ep = end = s+sdslen(s)-1;
    while(sp <= end && strchr(cset, *sp)) sp++;
    while(ep > sp && strchr(cset, *ep)) ep--;
    len = (sp > ep) ? 0 : ((ep-sp)+1);
    if (s != sp) memmove(s, sp, len);
    s[len] = '\0';
    sdssetlen(s,len);
    return s;
}




3、優點

A.獲取長度方便

c字串獲取長度需要便利char陣列,O(n),而SDS結構體記錄了長度,不需要char陣列即可知道長度。


B.防止溢位

char陣列不知道還有多少空間空餘,可能會在兩個字串拼接的時候溢位,而SDS記錄了未使用的空間,可以有效的分配擴容,防止溢位。


C.記憶體分配方便和使用高效

傳統c的char陣列,如果空間不足,需要手動擴容,然後複製原資料,截斷時,也需要縮減空間,來防止記憶體洩漏。但是SDS可以進行 空間預分配、惰性釋放 等策略來搞效的使用記憶體。

  • 空間預分配:

    預先分配足夠的空間,減少擴容次數

  • 惰性釋放

    因為SDS記錄了 free未分配空間欄位,所以截斷字串的時候不需要立即複製元素進行縮減,直接增加 free 數值,減少 len即可,後面要增加字串只增加len,減少free ,覆蓋寫入即可。(free = alloc-len)


D.相容C

SDS只是增加了兩個欄位,其實資料還是存在 char[] buf裡面的,所以可以使用 c內建的字串處理函式來處理 SDS底層位元組陣列。

typedef char *sds;

所以在處理 字串的API裡只是傳入了 char* 來處理字串。空間是否充足都有額外的資訊來描述。




二、連結串列

連結串列的話可以參考我的 https://www.cnblogs.com/biningooginind/p/12553163.html

基本參照了redis的連結串列操作。

1、結構體

typedef struct listNode {
    struct listNode *prev;
    struct listNode *next;
    void *value; //void* 指標 可以存放任意型別的資料
} listNode;


2、特點

連結串列的特點:

  • 刪除、插入 O(1)
  • 遍歷訪問 O(n)
  • 有head和tail指標,將訪問最後一個元素複雜度降低到O(1)
  • 帶有 len長度,方便知道連結串列的長度
  • 雙連結串列結構,前後遍歷都方便
  • 無環
  • 多型:資料用 void 來指向,可以存放任意型別資料,不用為每個型別都寫一個連結串列*
  • 迭代器模式連結串列有一個迭代器,方便遍歷節點
typedef struct listIter {
    listNode *next; //下一個節點
    int direction; //遍歷方向 forward or backward
} listIter;

相關文章