1. 程式人生 > 實用技巧 >深入瞭解Redis(1)-字串底層實現

深入瞭解Redis(1)-字串底層實現

一.簡單動態字串(SDS)

  Redis中字串實現有兩種方式,C語言傳統字串(以空字元結尾的字元陣列)和簡單動態字串(SDS),並將SDS作為預設字串表示.

  C字串只會作為字串字面量,用在一些無需對字串值進行修改的地方,比如列印日誌:

redisLog(REDIS_WARNING,"Redis is now ready to exit, bye bye...");

二.SDS的實現

每個sds.h/sdshdr結構表示一個SDS值:

struct sdshdr {

    // 記錄 buf 陣列中已使用位元組的數量
    // 等於 SDS 所儲存字串的長度
    int len;

    
// 記錄 buf 陣列中未使用位元組的數量 int free; // 位元組陣列,用於儲存字串 char buf[]; };

圖 2-1 展示了一個 SDS 示例:

  • free屬性的值為0, 表示這個 SDS 沒有分配任何未使用空間。
  • len屬性的值為5, 表示這個 SDS 儲存了一個五位元組長的字串。
  • buf屬性是一個char型別的陣列, 陣列的前五個位元組分別儲存了'R''e''d''i''s'五個字元, 而最後一個位元組則儲存了空字元'\0'

  SDS 遵循 C 字串以空字元結尾的慣例, 儲存空字元的1位元組空間不計算在 SDS 的len屬性裡面, 並且為空字元分配額外的1

位元組空間, 以及新增空字元到字串末尾等操作都是由 SDS 函式自動完成的, 所以這個空字元對於 SDS 的使用者來說是完全透明的。

  遵循空字元結尾這一慣例的好處是, SDS 可以直接重用一部分 C 字串函式庫裡面的函式。

三.SDS與C字串的區別

  c字串是由長度為n+1的字串陣列實現的,並且陣列的最後一個值總是為空字元'\0'.

比如說, 圖 2-3 就展示了一個值為"Redis"的 C 字串:

1.SDS可以更快的獲取字串長度

  因為c字串的資料結構為陣列,所以也繼承了陣列的基本特性,比如獲取該字串的長度,需要去遍歷整個陣列,該操作的複雜度為O(N).

  而SDS本身就維護了len屬性記錄了字串的長度,所以獲取SDS字串長度的操作複雜度為O(1).

2.杜絕緩衝區溢位

  c字串容易造成緩衝區溢位,因為c字串本身不記錄自身長度,比如<string.h>/strcat函式拼接字串時,可能導致記憶體空間不足.

  SDS的空間分配策略完全杜絕了發生緩衝區溢位的可能性,當 SDS API 需要對 SDS 進行修改時,API 會先檢查 SDS 的空間是否滿足修改所需的要求,如果不滿足的話,API 會自動將 SDS 的空間擴充套件至執行修改所需的大, 然後才執行實際的修改操作,所以使用 SDS 既不需要手動修改 SDS 的空間大小,也不會出現前面所說的緩衝區溢位問題.

3.減少修改字串時帶來的記憶體重分配次數

  因為 C 字串並不記錄自身的長, 所以對於一個包含了N個字元的 C 字串來說,這個 C 字串的底層實現總是一個N+1個字元長的陣列.在對字串的增長或者縮短操作中,很容易造出記憶體溢位和記憶體洩漏.

  為了避免 C 字串的這種缺陷,SDS 通過未使用空間解除了字串長度和底層陣列長度之間的關聯:在 SDS 中,buf陣列的長度不一定就是字元數量加一,數組裡面可以包含未使用的位元組,而這些位元組的數量就由 SDS 的free屬性記錄.通過未使用空間,SDS 實現了空間預分配和惰性空間釋放兩種優化策.

4.空間預分配

  空間預分配用於優化 SDS 的字串增長操作:當 SDS 的 API 對一個 SDS 進行修改,並且需要對 SDS 進行空間擴充套件的時候,程式不僅會為 SDS 分配修改所必須要的空間,還會為 SDS 分配額外的未使用空間.

5.惰性空間釋放

  惰性空間釋放用於優化 SDS 的字串縮短操作:當 SDS 的 API 需要縮短 SDS 儲存的字串, 程式並不立即使用記憶體重分配來回收縮短後多出來的位元組,而是使用free屬性將這些位元組的數量記錄起, 並等待將來使用.

6.二進位制安全

  C 字串中的字元必須符合某種編碼(比如 ASCII),並且除了字串的末尾之外,字串裡面不能包含空字元,否則最先被程式讀入的空字元將被誤認為是字串結尾 —— 這些限制使得 C 字串只能儲存文字資料,而不能儲存像圖片、音訊、視訊、壓縮檔案這樣的二進位制資料.

  為了確保 Redis 可以適用於各種不同的使用場景,SDS 的 API 都是二進位制安全的(binary-safe): 所有 SDS API 都會以處理二進位制的方式來處理 SDS 存放在buf數組裡的資料,程式不會對其中的資料做任何限制、過濾、或者假設 —— 資料在寫入時是什麼樣的,它被讀取時就是什麼.這也是我們將 SDS 的buf屬性稱為位元組陣列的原因 —— Redis 不是用這個陣列來儲存字元, 而是用它來儲存一系列二進位制資料.比如, 使用 SDS 來儲存之前提到的特殊資料格式就沒有任何問題,因為 SDS 使用len屬性的值而不是空字元來判斷字串是否結束.

7.相容部分c字串函式

  雖然 SDS 的 API 都是二進位制安全的,但它們一樣遵循 C 字串以空字元結尾的慣例:這些 API 總會將 SDS 儲存的資料的末尾設定為空字元,並且總會在為buf陣列分配空間時多分配一個位元組來容納這個空字元,這是為了讓那些儲存文字資料的 SDS 可以重用一部分<string.h>庫定義的函式.

總結區別:

C 字串SDS
獲取字串長度的複雜度為。 獲取字串長度的複雜度為。
API 是不安全的,可能會造成緩衝區溢位。 API 是安全的,不會造成緩衝區溢位。
修改字串長度N次必然需要執行N次記憶體重分配。 修改字串長度N次最多需要執行N次記憶體重分配。
只能儲存文字資料。 可以儲存文字或者二進位制資料。
可以使用所有<string.h>庫中的函式。 可以使用一部分<string.h>庫中的函式。