Redis學習筆記-字符串SDS

  1. 引言
    redis在我日常工作中使用的頻率相當高,每個項目基本都會用到。在redis的使用過程中,字符串使用的頻率也是相當的高,所以有必要學習redis的字符串的相關知識。文中涉及的代碼爲redis3.0版本

  2. 簡單瞭解SDS(Simple Dynamic String 簡單動態字符串)

    1. redis使用sds進行字符串的表示,沒有使用C默認的char*類型,char*的功能單一,抽象層次低,不能高效的支持一些redis的常用操作,如追加和長度計算
    2. SDS數據結構
      struct sdshdr {
          
          // 記錄buf已使用的字節數量
          // 即已保存的字符串的長度
          int len;
      
          // buf 中剩餘可用的字節數量
          int free;
      
          // 字節數組,用於保存字符串
          char buf[];
      };
      
    3. SDS示例
      SDS示例
    	free:0,表示sds沒有未使用空間
    	len:5,表示保存的字符串長度是5
    	buf:char數據,保存了R、e、d、i、s,buf其實保存了5個內容字符,還有一個\0結束字符,
    			但是\0不會被記錄長度到len當中	
    

    4.結束符疑問
    爲什麼還要浪費1個字節空間去記錄一個\0結束字符呢?
    因爲C字符串是以空字符\0結尾的,sds遵循C 字符串以空字符結尾的慣例,並且不會把\0記錄到len的長度當中,在分配空間時會額爲分配1個字節去記錄該字符,整個的操作都是sds的函數自動完成的,使用者無需關心。
    使用空字符\0結尾的好處是,可以使用C字符串函數庫裏面的函數

  3. SDS和C字符串
    3.1.C字符串
    C字符串“Redis”
    C字符串的結構如上所示,C字符串不能滿足Redis對字符串的安全性、效率以及功能方面的需求,所以redis纔會使用sds
    3.2. C字符串的問題

    1. 獲取字符串長度,獲取一個C字符串的長度,程序必須遍歷整個字符串,直到遇到結尾的空字符\0爲止,時間複雜度爲O(n)
    2. 緩衝區溢出,如果存在兩個字符串s1和s2,兩個字符串是緊鄰的如下圖所示,其中s1內容爲Redis,s2內容爲MongoDB,內存結構圖如下所示:
      s1和s2相鄰的字符串
      此時對s1進行append操作,使用如下函數:
      char *strcat(char *dest, const char *src);
      執行:
      strcat(s1, " Cluster");
      

    但是append前並沒有對s1分配足夠的內存空間就會導致s1的數據溢出到s2,導致s2的內容被修改。如下圖所示:
    s1內容溢出到s2

    1. 內存頻繁分配和回收內存,當對字符串頻繁的修改時,C字符串需要頻繁的分配和回收內存。
    2. 二進制安全 ,C 字符串中的字符必須符合某種編碼(比如 ASCII), 並且除了字符串的末尾之外, 字符串裏面不能包含空字符, 否則最先被程序讀入的空字符將被誤認爲是字符串結尾。舉個栗子:
      Redis Cluster
      上圖的字符串使用了空字符分隔,此時C字符串所用的函數只能識別其中的 “Reids”,會忽略後面的"Cluster"

    3.3 SDS如何解決這些問題

    1. 獲取字符串的長度,在SDS的結構體當中使用了len屬性記錄字符串內容的長度,sds獲取長度的代碼如下:

      /*
       * 返回 sds 實際保存的字符串的長度
       *
       * T = O(1)
       */
      static inline size_t sdslen(const sds s) {
          struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
          return sh->len;
      }
      
    2. 杜絕內存溢出,當 sds api 需要對 sds 進行修改時, api會先檢查 sds的空間是否滿足修改所需的要求, 如果不滿足的話, api會自動將 sds 的空間擴展至執行修改所需的大小, 然後才執行實際的修改操作。

    3. 減少修改字符串時帶來的內存分配次數,C字符串修改字符串需要重新分配內存,否則會導致內存溢出,SDS採取空間預分配惰性空間釋放方法從而較少內存分配次數
      空間預分配:

      1. sds api對sds修改時,如果此時需要對sds的空間進行擴展,sds不僅會分配當前操作所需要的空間,還會爲sds分配額外的未使用空間,這樣redis在後續執行字符串增長操作到時候可以減少內存重新分配的次數。
      2. 對sds修改後,如果len小於1M,則程序分配的和len相同的大小未使用空間,也就說此時len和free值是相等的。舉個栗子:對sds的修改,sds的len變成了13字節,程序會分配13字節的未使用空間,則sds的buf數組的實際長度爲13+13+1 = 27字節,即len+free+空字符=27字節
      3. 對sds修改後,如果len大於1MB,則程序會分配1MB的未使用空間,舉個栗子:sds修改後爲3MB,那麼程序會分配1MB的未使用空間,則buf數組的實際長度爲3MB+1MB+1byte
      4. 通過這種預分配策略, sds 將連續增長 N 次字符串所需的內存重分配次數必定 N 次降低爲最多 N 次。(因爲free的空間有可能不滿足當前修改操作的所需空,所以是降低爲最多N次)

      惰性空間釋放:

      1. sds的api對sds的縮短操作時,程序不會立即回收內存,而是使用free屬性將這些需要回收字節的數量記錄起來,等待將來使用。字符串”XYXXYabcXYY”移除所有的“X”和“Y”,過程如下圖所示。
        字符串”XYXXYabcXYY”
        字符串”XYXXYabcXYY”移除所有的“X”和“Y”
        如上圖所示,sds並沒有釋放多出來的 8 字節空間, 而是將這 8 字節空間作爲未使用空間保留在了 sds裏面, 如果將來要對 sds進行增長操作的話, 這些未使用空間就可能會派上用場
    4. 二進制安全,sds的api都是二進制安全的,所有 SDS API 都會以處理二進制的方式來處理 SDS 存放在 buf 數組裏的數據, 程序不會對其中的數據做任何限制、過濾、或者假設 —— 數據在寫入時是什麼樣的, 它被讀取時就是什麼樣。

  4. 總結
    sds比起c字符串有以下優點:

    1. 常數複雜度獲取字符串長度
    2. 杜絕緩衝區溢出
    3. 減少修改字符串長度時所需的內存重分配次數
    4. 二進制安全
    5. 兼容部分 C 字符串函數
  5. 參考資料
    <<Redis設計與實現>>

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章