Redis設計與實現 -- 動態字符串對象(SDS)

1. 動態字符串( simple dynamic string, SDS)

  在 Redis 中,當需要可以被重複修改的字符串時,會使用 SDS 類型 ,而不是 C 語言中默認的 C 字符串類型 。舉個例子:

SET msg "Hello World"

  在這個語句中,Redis 會新建一個鍵值對,其中

  • key 爲一個 字符串,對象的底層實現是一個保存着字符串 “msg” 的 SDS 對象。
  • value 爲一個字符串,對象的底層實現是一個保存着字符串 “Hello World” 的 SDS 對象。

  如果是 key 對多個 value 的情況下, 如

RPUSH fruits "apple" "banana" "cherry"

  Redis 同樣會新建一個鍵值對,其中

  • key 爲一個 字符串,對象的底層實現是一個保存着字符串 “fruits” 的 SDS 對象。
  • value 爲一個列表對象,包含着三個字符串對象,分別由三個 SDS 實現:第一個 SDS 保存着字符串 “apple” ,第二個 SDS 保存着字符串 “banana”,第三個 SDS 保存着字符串 “cherry”。

  SDS 對象除了作爲保存數據庫值的字符串之外,還用作緩衝區,比如 AOF 模塊中的 AOF 緩衝區,客戶端的輸入緩衝區等等。

2. SDS 的定義

  

  SDS 的結構如上圖所示,len 表示字符串長度,需要注意的是,SDS 的 buf 數組也和 C 字符串一樣保留着最後的空字符,而它的 len 計算時不會加上這個空字符,所以 "Redis\0" 在這裏 len 值爲 5。由於封裝的原因,這裏的 ‘\0’ 實際上雖然存在,但是使用者是完全不知道這裏還有一個空字符的存在的,而這裏加上空字符的原因也是爲了可以讓 SDS 沿用 一些 C 字符串函數庫的函數。比如可以直接用 printf("%s", s->buf); 來打印 SDS 的字符串。

 3. SDS 和 C 字符串的區別

  • 傳統的 C 字符串中,我們如果想要知道它的長度,需要遍歷其一遍,直到遇到空字符爲止,時間複雜度爲 O(N)。而 SDS 可以直接取 len 的值,時間複雜度爲 O(1)。
  • 由於沒有字符串的長度,C 字符串很容易導致緩衝區溢出,訪問越界問題。
  • C 字符串在使用的時候遇到空字符 ‘\0’ 便會認爲字符串已經結束,一旦字符串中含有該字符則會導致字符串的異常截斷,而 SDS 利用 len 字段可以防止這個情況。所以通過使用二進制安全的 SDS 可以存取任意數據。
  • 拼接字符串時,C 字符串需要內存重分配拓展數組的大小,防止緩衝區溢出,截斷字符串時,C 字符串需要內存重分配釋放截斷部分,防止內存泄漏。而 Redis 爲了提高性能,增加了一個未使用空間的字段,通過該字段實現了空間預分配和惰性空間釋放兩種優化策略。

空間預分配策略:每次 SDS 進行空間擴展的時候,程序除了修改當前已有的空間,還會爲 SDS 分配額外的空間,這也是 free 字段的作用,記錄額外空間的長度,額外空間的長度分配有兩種策略:根據修改後 SDS 的長度,即 len 屬性的值,len 小於 1 MB 時,分配和 len 一樣大小的額外空間, len 大於 1MB 時,則 分配 1 MB 的額外空間。舉個例子, SDS 爲 13 個字節,擴展 2 個字節後,len 變成 15 個字節,這時會分配額外的 15 個字節,free = 15;這時實際的字符串長度爲 15 + 15 + 1 = 31。

惰性空間釋放策略:當 SDS 進行縮短字符串的操作時,不馬上進行內存重分配,而是利用 free 字段將這些截斷的字符串長度記錄下來,舉個例子,如下圖所示的 SDS 結構

如果我們試圖刪除所有 “X” 和 “Y” 的字符, SDS 的結構會變成如下圖所示。

可以看到所有 “X” 和 “Y” 的字符長度總計爲 8 ,這裏 free 的值也被修改爲 8。這時如果再對字符串進行增長的操作,則可以防止進行內存重分配的操作,而直接使用前面 “節約” 下來的內存。

4. 總結

 之前遇到過一個二進制數據存取的問題 ,將 protobuf 序列化成二進制存到 Redis 數據庫,然後取出來的時候並沒有正常取出整個字符串,導致數據取出來之後反序列化異常,對比了存取時字符串的長度,發現就是因爲 '\0' 字符導致的,後面通過加上字符串的長度去取字符串解決了這個問題。

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