Redis的SDS與C字串的比較
C的字串底層實現是一個字元陣列,並以空格作為結束標誌。而redis的sds(簡單動態字串)是自己定義的。其實質是一個自定義的結構體,但是在結構體的buf[]陣列中傳承了C字串以空格結束的規範,這樣做的好處是便於直接使用C語言字串函式庫的部分函式,而不必再自定重新定義。
對於自定義結構體來說,其字串的長度=len。 儲存空字元的 1
位元組空間不計算在 SDS 的 len
屬性裡面, 並且為空字元分配額外的 1
位元組空間, 以及新增空字元到字串末尾等操作都是由 SDS 函式自動完成的, 所以這個空字元對於 SDS 的使用者來說是完全透明的。
相對於C的字串來說,sds的優勢主要體現在以下幾個方面(實質都是自定義結構體的屬性在起作用)
1:將獲取字串長度操作的複雜度從O(n)降到O(1), 這確保了獲取字串長度的工作不會成為 Redis 的效能瓶頸。
因為 C 字串並不記錄自身的長度資訊, 所以為了獲取一個 C 字串的長度, 程式必須遍歷整個字串, 對遇到的每個字元進行計數, 直到遇到代表字串結尾的空字元為止, 這個操作的複雜度為 O(N) 。
和 C 字串不同, 因為 SDS 在 len
屬性中記錄了 SDS 本身的長度, 程式只要訪問 SDS 的 len
2:採用空間預分配策略杜絕緩衝區溢位(字串增長操作),同時可以減少連續執行字串增長操作所需的記憶體重分配次數。
因為 C 字串不記錄自身的長度, 所以 假定使用者在執行拼接函式時, 若為原有的字串陣列分配了足夠多的記憶體, 可以容納 新字串中的所有內容,則可以拼成功。 但一旦這個假定不成立時, 就會產生緩衝區溢位。
舉個例子, 假設程式裡有兩個在記憶體中緊鄰著的 C 字串 s1
和 s2
, 其中 s1
儲存了字串 "Redis"
s2
則儲存了字串 "MongoDB"
, 如圖 2-7 所示。
如果一個程式設計師決定通過執行:
strcat(s1, " Cluster");
將 s1
的內容修改為 "Redis Cluster"
, 但粗心的他卻忘了在執行 strcat
之前為 s1
分配足夠的空間, 那麼在 strcat
函式執行之後, s1
的資料將溢位到 s2
所在的空間中, 導致 s2
儲存的內容被意外地修改, 如圖 2-8 所示。
與 C 字串不同, SDS 的空間分配策略完全杜絕了發生緩衝區溢位的可能性: 當 SDS API 需要對 SDS 進行修改時, API 會先檢查 SDS 的空間是否滿足修改所需的要求, 如果不滿足的話, API 會自動將 SDS 的空間擴充套件至執行修改所需的大小, 然後才執行實際的修改操作, 所以使用 SDS 既不需要手動修改 SDS 的空間大小, 也不會出現前面所說的緩衝區溢位問題。
上面粗體斜線API 會自動將 SDS 的空間擴充套件至執行修改所需的大小,採用的就是空間預分配策略實現。
空間預分配:
空間預分配用於優化 SDS 的字串增長操作: 當 SDS 的 API 對一個 SDS 進行修改, 並且需要對 SDS 進行空間擴充套件的時候, 程式不僅會為 SDS 分配修改所必須要的空間, 還會為 SDS 分配額外的未使用空間。
其中, 額外分配的未使用空間數量由以下公式決定:
- 如果對 SDS 進行修改之後, SDS 的長度(也即是
len
屬性的值)將小於1 MB
, 那麼程式分配和len
屬性同樣大小的未使用空間, 這時 SDSlen
屬性的值將和free
屬性的值相同。 舉個例子, 如果進行修改之後, SDS 的len
將變成13
位元組, 那麼程式也會分配13
位元組的未使用空間, SDS 的buf
陣列的實際長度將變成13 + 13 + 1 = 27
位元組(額外的一位元組用於儲存空字元)。 - 如果對 SDS 進行修改之後, SDS 的長度將大於等於
1 MB
, 那麼程式會分配1 MB
的未使用空間。 舉個例子, 如果進行修改之後, SDS 的len
將變成30 MB
, 那麼程式會分配1 MB
的未使用空間, SDS 的buf
陣列的實際長度將為30 MB + 1 MB + 1 byte
。
此時如果再對該字串進行拼接操作, SDS API 會先檢查未使用空間是否足夠, 如果足夠的話, API 就會直接使用未使用空間, 而無須執行記憶體重分配。
3:採用惰性空間釋放策略杜絕記憶體洩漏(字串刪除操作),同時可以減少刪除後又對同一字串增加所需的記憶體重分配次數。
如果程式執行的是縮短字串的操作, 比如截斷操作(trim), 那麼在執行這個操作之後, 程式需要通過記憶體重分配來釋放字串不再使用的那部分空間 —— 如果忘了這一步就會產生記憶體洩漏。
sds採用惰性空間釋放用於優化 SDS 的字串縮短操作: 當 SDS 的 API 需要縮短 SDS 儲存的字串時, 程式並不立即使用記憶體重分配來回收縮短後多出來的位元組, 而是使用 free
屬性將這些位元組的數量記錄起來, 並等待將來使用。
舉個例子, sdstrim
函式接受一個 SDS 和一個 C 字串作為引數, 從 SDS 左右兩端分別移除所有在 C 字串中出現過的字元。
比如對於圖 2-14 所示的 SDS 值 s
來說, 執行:
sdstrim(s, "XY"); // 移除 SDS 字串中的所有 'X' 和 'Y'
注意執行 sdstrim
之後的 SDS 並沒有釋放多出來的 8
位元組空間, 而是將這 8
位元組空間作為未使用空間保留在了 SDS 裡面,此時如果再對該字串進行拼接操作, SDS API 會先檢查未使用空間是否足夠, 如果足夠的話, API 就會直接使用未使用空間, 而無須執行記憶體重分配。
通過惰性空間釋放策略, SDS 避免了縮短字串時所需的記憶體重分配操作, 併為將來可能有的增長操作提供了優化。與此同時, SDS 也提供了相應的 API , 讓我們可以在有需要時, 真正地釋放 SDS 裡面的未使用空間, 所以不用擔心惰性空間釋放策略會造成記憶體浪費。
4:二進位制安全,確保 Redis 可以適用於各種不同的使用場景。C 字串只能儲存文字資料, 而不能儲存像圖片、音訊、視訊、壓縮檔案這樣的二進位制資料。
C 字串中的字元必須符合某種編碼(比如 ASCII), 並且除了字串的末尾之外, 字串裡面不能包含空字元, 否則最先被程式讀入的空字元將被誤認為是字串結尾 —— 這些限制使得 C 字串只能儲存文字資料。
SDS 的 API 都是二進位制安全的(binary-safe): 所有 SDS API 都會以處理二進位制的方式來處理 SDS 存放在 buf
數組裡的資料, 程式不會對其中的資料做任何限制、過濾、或者假設 —— 資料在寫入時是什麼樣的, 它被讀取時就是什麼樣。這也是我們將 SDS 的 buf
屬性稱為位元組陣列的原因 —— Redis 不是用這個陣列來儲存字元, 而是用它來儲存一系列二進位制資料。比如說, 使用 SDS 來儲存之前提到的特殊資料格式就沒有任何問題, 因為 SDS 使用 len
屬性的值而不是空字元來判斷字串是否結束。