Redis 動態字符串

SDS是Redis底層使用的字符串的表示形式。

SDS用途

SDS主要用兩方面作用:

1.實現字符串對像

Redis是鍵值對數據庫,數據庫的值(value)可以是字符串、集合、列表等類型對象,但是數據庫鍵(key)總是字符串對象。
舉例如下:

redis> SET name "cai"  
OK    
redis> GET name  
"cai"  

這里鍵值對的鍵和值都是字符串對象,他們都包含一個SDS值

2.在Redis內部作為char*的替代品

因為 char* 類型的功能單一, 抽象層次低, 并且不能高效地支持一些 Redis 常用的操作(比如追加操作和長度計算操作), 所以在 Redis 程序內部, 絕大部分情況下都會使用 SDS 而不是 char 來表示字符串*。
在 C 語言中,字符串可以用一個\0結尾的char數組來表示。比如說, hello world 在 C 語言中就可以表示為 "hello world\0" 。這種簡單的字符串表示,在大多數情況下都能滿足要求,但是,它并不能高效地支持長度計算和追加(append)這兩種操作:

  • 每次計算字符串長度(strlen(s))的復雜度為 θ(N) 。
  • 對字符串進行N次追加,必定需要對字符串進行N次內存重分配(realloc)。

SDS的實現

在源代碼sds.h中定義了sds以及sdshdr結構體。

// sds 類型  
typedef char *sds;    
// sdshdr 結構  
struct sdshdr {  
    // buf 已占用長度  
    int len;  
    // buf 剩余可用長度  
    int free;  
    // 實際保存字符串數據的地方  
    char buf[];  
};  

從這個定義中無法看出sds與sdshdr之間的關系。
通過查看sds.c中的代碼,皆能迎刃而解了。

/* 
* 創建一個指定長度的 sds  
* 如果給定了初始化值 init 的話,那么將 init 復制到 sds 的 buf 當中 
* 
* T = O(N) 
*/  
sds sdsnewlen(const void *init, size_t initlen) {  
   struct sdshdr *sh;  
   // 有 init ?  
   // O(N)  
   if (init) {  
       sh = zmalloc(sizeof(struct sdshdr)+initlen+1);  
   } else {  
       sh = zcalloc(sizeof(struct sdshdr)+initlen+1);  
   }  
 
   // 內存不足,分配失敗  
   if (sh == NULL) return NULL;  
 
   sh->len = initlen;  
   sh->free = 0;  
 
   // 如果給定了 init 且 initlen 不為 0 的話  
   // 那么將 init 的內容復制至 sds buf  
   // O(N)  
   if (initlen && init)  
       memcpy(sh->buf, init, initlen);  
   // 加上終結符  
   sh->buf[initlen] = '\0';  
 
   // 返回 buf 而不是整個 sdshdr  
   return (char*)sh->buf;  
}  

通過創建函數可以看到,函數返回值是sds,在函數中返回的是sdshdr結構體中數據指向部分。
這就可以知道在創建sds對象的時候,其實是創建了一個sdshdr結構體對象,但是通過巧妙的指針指向,實現了sds

追加指令APPEND

利用 sdshdr 結構,可以用 θ(1) 復雜度獲取字符串的長度,還可以減少追加(append)操作所需的內存重分配次數。

redis> SET msg "hello world"  
OK  
redis> APPEND msg " again!"  
(integer) 18  
redis> GET msg  
"hello world again!"  

首先, SET 命令創建并保存 hello world 到一個 sdshdr 中,這個 sdshdr 的值如下:

struct sdshdr {  
    len = 11;  
    free = 0;  
    buf = "hello world\0";  
}  

當執行 APPEND 命令時,相應的 sdshdr 被更新,字符串 " again!" 會被追加到原來的 "hello world" 之后:

struct sdshdr {  
    len = 18;  
    free = 18;  
    buf = "hello world again!\0                  ";     // 空白的地方為預分配空間,共 18 + 18 + 1 個字節  
}  

當調用 SET 命令創建 sdshdr 時, sdshdr 的 free 屬性為 0 , Redis 也沒有為 buf 創建額外的空間,
而在執行 APPEND 之后, Redis 為 buf 創建了多于所需空間一倍的大小。在這個例子中, 保存 "hello world again!" 共需要 18 + 1 個字節, 但程序卻為我們分配了 18 + 18 + 1 = 37 個字節 ,
這樣一來, 如果將來再次對同一個 sdshdr 進行追加操作,只要追加內容的長度不超過 free 的值, 就不需要對 buf 進行內存重分配。
舉例如下:

redis> APPEND msg " again!"  
(integer) 25  

struct sdshdr {  
    len = 25;  
    free = 11;  
    buf = "hello world again! again!\0           ";  // 空白的地方為預
    //分配空間,共 18 + 18 + 1 個字節  
}  

理解了SET和APPEND機制,就能知道為什么使用SDS能夠降低獲取長度和追加的復雜度了。

sds.c中的sdsMakeRoomFor函數說明了這種內存預分配優化策略。

/* Enlarge the free space at the end of the sds string so that the caller 
 * is sure that after calling this function can overwrite up to addlen 
 * bytes after the end of the string, plus one more byte for nul term. 
 *  
 * Note: this does not change the *size* of the sds string as returned 
 * by sdslen(), but only the free buffer space we have. */  
/*  
 * 對 sds 的 buf 進行擴展,擴展的長度不少于 addlen 。 
 * 
 * T = O(N) 
 */  
sds sdsMakeRoomFor(  
    sds s,  
    size_t addlen   // 需要增加的空間長度  
)   
{  
    struct sdshdr *sh, *newsh;  
    size_t free = sdsavail(s);  
    size_t len, newlen;  
  
    // 剩余空間可以滿足需求,無須擴展  
    if (free >= addlen) return s;  
      
    sh = (void*) (s-(sizeof(struct sdshdr)));  
  
    // 目前 buf 長度  
    len = sdslen(s);  
    // 新 buf 長度  
    newlen = (len+addlen);  
    // 如果新 buf 長度小于 SDS_MAX_PREALLOC 長度  
    // 那么將 buf 的長度設為新 buf 長度的兩倍  
    if (newlen < SDS_MAX_PREALLOC)  
        newlen *= 2;  
    else  
        newlen += SDS_MAX_PREALLOC;  
  
    // 擴展長度  
    newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);  
  
    if (newsh == NULL) return NULL;  
  
    newsh->free = newlen - len;  
  
    return newsh->buf;  
}  

如下代碼就巧妙的利用了指針的指向,找到sds對應的sdshdr結構體。

sh = (void*) (s-(sizeof(struct sdshdr)));  

SDS_MAX_PREALLOC 的值為 1024 * 1024 , 當大小小于 1MB 的字符串執行追加操作時, sdsMakeRoomFor 就為它們分配多于所需大小一倍的空間; 當字符串的大小大于 1MB , 那么 sdsMakeRoomFor 就為它們額外多分配 1MB 的空間。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Redis使用的是自己構建的簡單動態字符串(simple dynamic string,SDS)的抽象類型, 并將...
    但莫閱讀 515評論 0 0
  • Redis的內存優化 聲明:本文內容來自《Redis開發與運維》一書第八章,如轉載請聲明。 Redis所有的數據都...
    meng_philip123閱讀 18,925評論 2 29
  • 前言 Redis的作者antirez(Salvatore Sanfilippo)曾經發表了一篇名為Redis宣言(...
    OzanShareing閱讀 1,472評論 0 20
  • 三月的光影微微的刺眼,飄散帶著櫻花初開的溫良。我并不是個擅長寫故事的人,也不是個有故事的人。 我走過很多城市,睡過...
    邵小陽閱讀 376評論 0 1
  • Compiler 監視模式(watch mode) 是普通的單次運行(normal single run)
    胡博術閱讀 148評論 0 0