【Redis 系列】redis 學習十五,redis sds資料結構和底層設計原理

小魔童哪吒發表於2022-05-10

redis 是 C 語言寫的,那麼我們思考一下 redis 是如何表示一個字串的?redis 的資料結構和 C 語言的資料結構是一樣的嗎?

我們可以看到 redis 原始碼中的 sds 庫函式,和 sds 的具體實現,分別有如下 2 個檔案:

  • sds.h
  • sds.c

具體路徑是:deps/hiredis/sds.h , deps/hiredis/sds.c

sds.h 中涉及如下資料結構:

SDS

redis 中 SDS simple Dynamic string

簡單動態字串

C 語言中表示字串的方式是字元陣列,例如:

char data[]="xiaomotong"

如果 C 語言需要擴容的話需要重新分配一個再大一點的記憶體,存放新的字串,若每次都要重新分配字串,對於效率和效能必然會大大降低,並且若某一個字串是 “xiaomo\0tong”

這個時候,實際上 C 中 遇到 ‘\0’ 就結束了,因此實際 “xiaomo\0tong” 只會讀取到xiaomo ,字串長度就是 6

因此 redis 中的 sds 資料結構是這樣設計的,是通過一個成員來標誌字串的長度:

SDS:
    free:0
    len:6
    char buf[]="xiaomo"
        
若這個時候,我們需要在字串後面追加字串, sds 就會進行擴容,例如在後面加上 “tong” , 那麼 sds 的資料結構中的值會變成如下:
    free:10
    len:10
    char buf[]="xiaomotong"      

最後的 "xiaomotong" 也是帶有 \0 的,這也保持了 C 語言的標準,redis 中對於 sds 資料結構擴容是成倍增加的,但是到了一定的級別,例如 1M 的時候,就不會翻倍的擴容,而是做加法 例如 1M 變成 2M , 2M 變成 3M 等等

SDS 的優勢:

  • 二進位制安全的資料結構
  • 記憶體預分配機制,避免了頻繁的記憶體分配
  • 相容 C 語言的庫函式

redis 原始碼 sds 資料結構

現在我們看到的是 reids-6.2.5 sds 的資料結構,將以前的表示一個長度使用了 int 型別,是 32 位元組的,能表示的長度可以達到 42 億,其實遠遠沒有必要使用 int32 ,太浪費資源了

下面的資料結構,可以根據不同的需求,選取不同的資料結構進行使用

struct __attribute__ ((__packed__)) hisdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) hisdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) hisdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) hisdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) hisdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
  • hisdshdr5

用於長度在 0 -- 2^5 - 1 範圍內

  • hisdshdr8

用於長度在 2^5-- 2^8 - 1 範圍內

  • hisdshdr16

用於長度在 2^8 -- 2^16 - 1 範圍內

  • hisdshdr32

用於長度在 2^16 -- 2^32 - 1 範圍內

  • hisdshdr64

用於長度在 2^32 -- 2^64 - 1 範圍內

上述的 unsigned char flags 佔用 1 個位元組,8個 bit 位:

  • 其中 3 位 用於表示型別
  • 其中 5 位 用於表示字串的長度

前面 3 個 bit 位,能表示的數字範圍是 0 - 7 ,對於應到如下巨集

#define HI_SDS_TYPE_5  0
#define HI_SDS_TYPE_8  1
#define HI_SDS_TYPE_16 2
#define HI_SDS_TYPE_32 3
#define HI_SDS_TYPE_64 4
#define HI_SDS_TYPE_MASK 7

原始碼實現是通過與操作來獲取到具體的資料結構型別的:

我們們以 hisdshdr8 資料結構為例子,unsigned char flags 是這樣的

  • len

表示已經使用的長度

  • alloc

預分配的空間大小

  • flag

表示使用哪一種資料結構(前 3 個 bit)

  • buf

實際儲存的字串

那麼,我們就能夠計算出來,該資料結構的空間剩餘 free = alloc - len

原始碼中 sds.h 下的函式 hisds hi_sdsnewlen(const void *init, size_t initlen)

使用 一個 init 指標和 initlen 長度,來建立一個字串

hisds hi_sdsnewlen(const void *init, size_t initlen) {
    void *sh;
    hisds s;
    // 計算type,獲取需要使用的資料結構型別
    char type = hi_sdsReqType(initlen);
    // 現在預設使用 HI_SDS_TYPE_8 了
    if (type == HI_SDS_TYPE_5 && initlen == 0) type = HI_SDS_TYPE_8;
    
    int hdrlen = hi_sdsHdrSize(type);
    unsigned char *fp; /* flags pointer. */
    
    // 分配記憶體
    sh = hi_s_malloc(hdrlen+initlen+1);
    if (sh == NULL) return NULL;
    if (!init)
        memset(sh, 0, hdrlen+initlen+1);
    s = (char*)sh+hdrlen;
    fp = ((unsigned char*)s)-1;
    // 根據不同的型別對資料結構初始化
    switch(type) {
        case HI_SDS_TYPE_5: {
            *fp = type | (initlen << HI_SDS_TYPE_BITS);
            break;
        }
        case HI_SDS_TYPE_8: {
            HI_SDS_HDR_VAR(8,s);
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
        case HI_SDS_TYPE_16: ...
        case HI_SDS_TYPE_32: ...
        case HI_SDS_TYPE_64: ...
    }
    if (initlen && init)
        memcpy(s, init, initlen);
    // 相容 C 庫,字串後面加上 \0
    s[initlen] = '\0';
    return s;
}
  • hi_sdsReqType

根據字串的長度來計算所使用的資料型別

  • hi_sdsHdrSize

根據不同的型別,獲取該型別需要分配的空間大小

  • hi_s_malloc

開闢記憶體,呼叫的是 alloc.h 中的 hi_malloc,具體實現就看不到了

  • switch(type) …

根據不同的型別,來將對應的資料結構做初始化

  • s[initlen] = '\0'

相容 C 庫,字串後面加上 ’\0’

redis k-v 底層設計原理

redis 是如何儲存海量資料的?

redis 中資料是以 key-value 的方式來儲存的,key 都是字串,而 value 根據不同的資料結構表現形式也不太一樣

他們的儲存方式是以 陣列 + 連結串列的方式儲存的:

  • 陣列

陣列中存放的是連結串列的地址

  • 連結串列

連結串列中儲存的是具體的資料

舉個例子:

上面有說到 redis 裡面的 key 都是字串的方式,那麼如何與陣列和連結串列進行結合呢?

具體邏輯是使用 hash 函式,將字串 key 按照演算法計算出一個索引值,這個值就是陣列的索引,該索引對應的陣列元素是指向一個連結串列的,連結串列中存放具體的資料

  • dict[10] 作為陣列,每一個元素會指向一條連結串列
  • 現在我們要插入 k1 - v1 , k2 - v2 , k3 - v3

通過 hash 函式進行計算:

hash(k1) % 10 = 0

hash(k2) % 10 = 1

此處對 10 取模的原因是,整個陣列就只能存放 10 個元素

那麼結果是這樣的

dict[0] -> (k1,v1) -> null
dict[1] -> (k2,v2) -> null

若這個時候我們們插入的 (k3,v3) 計算出來的索引與前面已有資料的衝突了咋辦?

hash(k3) % 10 = 1

這就會出現 hash 衝突了,當 hash 衝突的時候,若 k3 與 k2 是相等了,那麼就會直接更新 k2 對應的 value 值

若 k3 與 k2 不同,則會通過鏈地址法來解決 hash 衝突,會把 (k3,v3) 通過頭插法來插入到原有的連結串列中,如:

dict[0] -> (k1,v1) -> null
dict[1] -> (k3,v3) -> (k2,v2) -> null

小結

  • 對於上述的 hash ,相同的輸入,一定會有相同的輸出
  • 不同的輸入,也有可能有相同的輸出,此時就 hash 衝突了,是需要解決的

參考資料:

歡迎點贊,關注,收藏

朋友們,你的支援和鼓勵,是我堅持分享,提高質量的動力

歡迎大家對文章中的原始碼細節進行討論和分享,不足之處還請多多指教,如果大佬們有更好的學習方法還請給予指導,謝謝

1、評論區超過 10 人互動(不含作者本人),作者可以以自己的名義抽獎送出掘金徽章 2 枚(掘金官方承擔)

好了,本次就到這裡

技術是開放的,我們的心態,更應是開放的。擁抱變化,向陽而生,努力向前行。

我是小魔童哪吒,歡迎點贊關注收藏,下次見~

相關文章