Redis中的動態字串學習教程_Redis

來源:互聯網
上載者:User

sds 的用途
Sds 在 Redis 中的主要作用有以下兩個:

實現字串對象(StringObject);
在 Redis 程式內部用作 char* 類型的替代品;
以下兩個小節分別對這兩種用途進行介紹。

實現字串對象

Redis 是一個索引值對資料庫(key-value DB), 資料庫的值可以是字串、集合、列表等多種類型的對象, 而資料庫的鍵則總是字串對象。

對於那些包含字串值的字串對象來說, 每個字串對象都包含一個 sds 值。

“包含字串值的字串對象”,這種說法初聽上去可能會有點奇怪, 但是在 Redis 中, 一個字串對象除了可以儲存字串值之外, 還可以儲存 long 類型的值, 所以為了嚴謹起見, 這裡需要強調一下: 當字串對象儲存的是字串時, 它包含的才是 sds 值, 否則的話, 它就是一個 long 類型的值。
舉個例子, 以下命令建立了一個新的資料庫索引值對, 這個索引值對的鍵和值都是字串對象, 它們都包含一個 sds 值:

redis> SET book "Mastering C++ in 21 days"OKredis> GET book"Mastering C++ in 21 days"

以下命令建立了另一個索引值對, 它的鍵是字串對象, 而值則是一個集合對象:

redis> SADD nosql "Redis" "MongoDB" "Neo4j"(integer) 3redis> SMEMBERS nosql1) "Neo4j"2) "Redis"3) "MongoDB"

用 sds 取代 C 預設的 char* 類型

因為 char* 類型的功能單一, 抽象層次低, 並且不能高效地支援一些 Redis 常用的操作(比如追加操作和長度計算操作), 所以在 Redis 程式內部, 絕大部分情況下都會使用 sds 而不是 char* 來表示字串。

效能問題在稍後介紹 sds 定義的時候就會說到, 因為我們還沒有瞭解過 Redis 的其他功能模組, 所以也沒辦法詳細地舉例說那裡用到了 sds , 不過在後面的章節中, 我們會經常看到其他模組(幾乎每一個)都用到了 sds 類型值。

目前來說, 只要記住這個事實即可: 在 Redis 中, 用戶端傳入伺服器的協議內容、 aof 緩衝、 返回給用戶端的回複, 等等, 這些重要的內容都是由 sds 類型來儲存的。

redis 中的字串
在 C 語言中,字串可以用一個 \0 結尾的 char 數組來表示。

比如說, hello world 在 C 語言中就可以表示為 "hello world\0" 。

這種簡單的字串表示,在大多數情況下都能滿足要求,但是,它並不能高效地支援長度計算和追加(append)這兩種操作:

每次計算字串長度(strlen(s))的複雜度為 θ(N) 。
對字串進行 N 次追加,必定需要對字串進行 N 次記憶體重分配(realloc)。
在 Redis 內部, 字串的追加和長度計算很常見, 而 APPEND 和 STRLEN 更是這兩種操作,在 Redis 命令中的直接映射, 這兩個簡單的操作不應該成為效能的瓶頸。

另外, Redis 除了處理 C 字串之外, 還需要處理單純的位元組數組, 以及伺服器協議等內容, 所以為了方便起見, Redis 的字串表示還應該是二進位安全的: 程式不應對字串裡面儲存的資料做任何假設, 資料可以是以 \0 結尾的 C 字串, 也可以是單純的位元組數組, 或者其他格式的資料。

考慮到這兩個原因, Redis 使用 sds 類型替換了 C 語言的預設字串表示: sds 既可高效地實現追加和長度計算, 同時是二進位安全的。

sds 的實現

在前面的內容中, 我們一直將 sds 作為一種抽象資料結構來說明, 實際上, 它的實現由以下兩部分組成:

typedef char *sds;struct sdshdr {  // buf 已佔用長度  int len;  // buf 剩餘可用長度  int free;  // 實際儲存字串資料的地方  char buf[];};

其中,類型 sds 是 char * 的別名(alias),而結構 sdshdr 則儲存了 len 、 free 和 buf 三個屬性。

作為例子,以下是新建立的,同樣儲存 hello world 字串的 sdshdr 結構:

struct sdshdr {  len = 11;  free = 0;  buf = "hello world\0"; // buf 的實際長度為 len + 1};

通過 len 屬性, sdshdr 可以實現複雜度為 θ(1) 的長度計算操作。

另一方面, 通過對 buf 分配一些額外的空間, 並使用 free 記錄未使用空間的大小, sdshdr 可以讓執行追加操作所需的記憶體重分配次數大大減少, 下一節我們就會來詳細討論這一點。

當然, sds 也對操作的正確實現提出了要求 —— 所有處理 sdshdr 的函數,都必須正確地更新 len 和 free 屬性,否則就會造成 bug 。

資料類型定義
與sds實現有關的資料類型有兩個,一個是 sds:

  // 字串類型的別名   typedef char *sds; 


另一個是 sdshdr:

  // 持有sds的結構   struct sdshdr {     // buf中已經被使用的字串空間數量     int len;     // buf中預留字串的空間數量     int free;     // 實際儲存字串的地方     char buf[];   }; 


其中,sds只是字串數群組類型char*的別名,而sdshdr用於持有和儲存sds的資訊

比如,sdshdr.len可以用於在O(1)的複雜度下擷取sdshdr.buf中儲存的字串的實際長度,而sdshdr.free則用於儲存sdshdr.buf中還有多少預留空間

(這裡sdshdr應該是sds handler的縮寫)

將sdshdr用作sds
sds模組對sdshdr結構使用了一點小技巧:通過指標運算,它使得sdshdr結構可以像sds類型一樣被傳值和處理,並在需要的時候恢複成sdshdr類型

通過下面的函數定義來理解這個技巧

sdsnewlen 函數返回一個新的sds值,實際上,它建立的卻是一個sdshdr結構:

  sds sdsnewlen(const void *init, size_t initlen)   {     struct sdshdr *sh;        if (init) {       // 建立       sh = malloc(sizeof(struct sdshdr) + initlen + 1);     } else {       // 重分配       sh = calloc(1, sizeof(struct sdshdr) + initlen + 1);     }        if (sh == NULL) return NULL;        sh->len = initlen;     sh->free = 0;  // 剛開始free為0        if (initlen && init) {       memcpy(sh->buf, init, initlen);     }     sh->buf[initlen] = '\0';        // 只返回sh->buf這個字串部分     return (char *)sh->buf;   } 


通過使用變數持有一個sds的值,在遇到那些只處理sds值本身的函數時,可以直接將sds傳給它們。比如說,sdstoupper 函數就是其中的一個例子:

 

  static inline size_t sdslen(const sds s)   {     // 從sds中計算出相應的sdshdr結構     struct sdshdr *sh = (void *)(s - (sizeof(struct sdshdr)));        return sh->len;   }         void sdstoupper(sds s)   {     int len = sdslen(s), j;        for (j = 0; j < len; j ++)       s[j] = toupper(s[j]);   } 


這裡有一個技巧,通過指標運算,可以從sds值中計算出相應的sdshdr結構:

sds雖然是指向char *的buf(ps:並且空數組不佔用記憶體空間,數組名即為記憶體位址),但是分配的時候是分配sizeof(struct sdshdr) + initlen + 1的,通過sds - sizeof(struct sdshdr)可以計算出struct sdshdr的首地址,從而可以得到len和free的資訊

sdsavail 函數就是使用這中技巧的一個例子:

 

  static inline size_t sdsavail(const sds s)   {     struct sdshdr *sh = (void *)(s - (sizeof(struct sdshdr)));        return sh->free;   } 


記憶體配置函數實現
和Reids 的實現決策相關的函數是 sdsMakeRoomFor :

 

  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;        len = sdslen(s);     sh = (void *)(s - (sizeof(struct sdshdr)));        // 設定新sds的字串長度     // 這個長度比完成本次拼接實際所需的長度要大     // 通過預留空間最佳化下次拼接操作     newlen = (len + addlen);     if (newlen < 1024 * 1024)       newlen *= 2;     else       newlen += 1024;        // 重新分配sdshdr     newsh = realloc(sh, sizeof(struct sdshdr) + newlen + 1);     if (newsh == NULL) return NULL;        newsh->free = newlen - len;        // 只返回字串部分     return newsh->buf;   } 


這種記憶體配置策略表明,在對sds 值進行擴充(expand)時,總會預留額外的空間,通過花費更多的記憶體,減少了對記憶體進行重分配(reallocate)的次數,並最佳化下次擴充操作的處理速度

再把redis的如果實現對sds字串擴充的方法貼一下,很不錯的思路:

  /**    * 按長度len擴充sds,並將t拼接到sds的末尾    */   sds sdscatlen(sds s, const void *t, size_t len)   {     struct sdshdr *sh;        size_t curlen = sdslen(s);        // O(N)     s = sdsMakeRoomFor(s, len);     if (s == NULL) return NULL;        // 複製     memcpy(s + curlen, t, len);        // 更新len和free屬性     sh = (void *)(s - (sizeof(struct sdshdr)));     sh->len = curlen + len;     sh->free = sh->free - len;        // 終結符     s[curlen + len] = '\0';        return s;   }      /**    * 將一個char數組拼接到sds 末尾    */   sds sdscat(sds s, const char *t)   {     return sdscatlen(s, t, strlen(t));   } 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.