1. 程式人生 > >Redis底層資料結構--SDS

Redis底層資料結構--SDS

這是一種用於儲存二進位制資料的一種結構, 具有動態擴容的特點. 其實現位於src/sds.hsrc/sds.c中, 其關鍵定義如下:

typedef char *sds;

/* Note: sdshdr5 is never used, we just access the flags byte directly.
 * However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    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__)) sdshdr16 {
    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__)) sdshdr32 {
    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__)) sdshdr64 {
    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[];
};

SDS的總體概覽如下圖:

sds

其中sdshdr是頭部, buf是真實儲存使用者資料的地方. 另外注意, 從命名上能看出來, 這個資料結構除了能儲存二進位制資料, 顯然是用於設計作為字串使用的, 所以在buf中, 使用者資料後總跟著一個\0. 即圖中 "資料" + "\0" 是為所謂的buf

SDS有五種不同的頭部. 其中sdshdr5實際並未使用到. 所以實際上有四種不同的頭部, 分別如下:

sdshdr

  1. len分別以uint8uint16uint32uint64表示使用者資料的長度(不包括末尾的\0)
  2. alloc分別以uint8
    uint16uint32uint64表示整個SDS, 除過頭部與末尾的\0, 剩餘的位元組數.
  3. flag始終為一位元組, 以低三位標示著頭部的型別, 高5位未使用.

當在程式中持有一個SDS例項時, 直接持有的是資料區的頭指標, 這樣做的用意是: 通過這個指標, 向前偏一個位元組, 就能取到flag, 通過判斷flag低三位的值, 能迅速判斷: 頭部的型別, 已用位元組數, 總位元組數, 剩餘位元組數. 這也是為什麼sds型別即是char *指標類型別名的原因.

建立一個SDS例項有三個介面, 分別是:

// 建立一個不含資料的sds: 
//  頭部    3位元組 sdshdr8
//  資料區  0位元組
//  末尾    \0 佔一位元組
sds sdsempty(void);
// 帶資料建立一個sds:
//  頭部    按initlen的值, 選擇最小的頭部型別
//  資料區  從入參指標init處開始, 拷貝initlen個位元組
//  末尾    \0 佔一位元組
sds sdsnewlen(const void *init, size_t initlen);
// 帶資料建立一個sds:
//  頭部    按strlen(init)的值, 選擇最小的頭部型別
//  資料區  入參指向的字串中的所有字元, 不包括末尾 \0
//  末尾    \0 佔一位元組
sds sdsnew(const char *init);
  1. 所有建立sds例項的介面, 都不會額外分配預留記憶體空間
  2. sdsnewlen用於帶二進位制資料建立sds例項, sdsnew用於帶字串建立sds例項. 介面返回的sds可以直接傳入libc中的字串輸出函式中進行操作, 由於無論其中儲存的是使用者的二進位制資料, 還是字串, 其末尾都帶一個\0, 所以至少呼叫libc中的字串輸出函式是安全的.

在對SDS中的資料進行修改時, 若剩餘空間不足, 會呼叫sdsMakeRoomFor函式用於擴容空間, 這是一個很低階的API, 通常情況下不應當由SDS的使用者直接呼叫. 其實現中核心的幾行如下:

sds sdsMakeRoomFor(sds s, size_t addlen) {
    ...
    /* Return ASAP if there is enough space left. */
    if (avail >= addlen) return s;

    len = sdslen(s);
    sh = (char*)s-sdsHdrSize(oldtype);
    newlen = (len+addlen);
    if (newlen < SDS_MAX_PREALLOC)
        newlen *= 2;
    else
        newlen += SDS_MAX_PREALLOC;
    ...
}

可以看到, 在擴充空間時

  1. 先保證至少有addlen可用
  2. 然後再進一步擴充, 在總體佔用空間不超過閾值SDS_MAC_PREALLOC時, 申請空間再翻一倍. 若總體空間已經超過了閾值, 則步進增長SDS_MAC_PREALLOC. 這個閾值的預設值為 1024 * 1024

SDS也提供了介面用於移除所有未使用的記憶體空間. sdsRemoveFreeSpace, 該介面沒有間接的被任何SDS其它介面呼叫, 即預設情況下, SDS不會自動回收預留空間. 在SDS的使用者需要節省記憶體時, 由使用者自行呼叫:

sds sdsRemoveFreeSpace(sds s);

總結:

  1. SDS除了是某些Value Type的底層實現, 也被大量使用在Redis內部, 用於替代C-Style字串. 所以預設的建立SDS例項介面, 不分配額外的預留空間. 因為多數字符串在程式執行期間是不變的. 而對於變更資料區的API, 其內部則是呼叫了 sdsMakeRoomFor, 每一次擴充空間, 都會預留大量的空間. 這樣做的考量是: 如果一個SDS例項中的資料被變更了, 那麼很有可能會在後續發生多次變更.
  2. SDS的API內部不負責清除未使用的閒置記憶體空間, 因為內部API無法判斷這樣做的合適時機. 即便是在操作資料區的時候導致資料區佔用記憶體減少時, 內部API也不會清除閒置內在空間. 清除閒置記憶體空間責任應當由SDS的使用者自行擔當.
  3. 用SDS替代C-Style字串時, 由於其頭部額外儲存了資料區的長度資訊, 所以字串的求長操作時間複雜度為O(1)