深入瞭解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 可以直接重用一部分 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> 庫中的函式。 |