-
引言
redis在我日常工作中使用的頻率相當高,每個項目基本都會用到。在redis的使用過程中,字符串使用的頻率也是相當的高,所以有必要學習redis的字符串的相關知識。文中涉及的代碼爲redis3.0版本 -
簡單瞭解SDS(Simple Dynamic String 簡單動態字符串)
- redis使用sds進行字符串的表示,沒有使用C默認的
char*
類型,char*
的功能單一,抽象層次低,不能高效的支持一些redis的常用操作,如追加和長度計算。 - SDS數據結構
struct sdshdr { // 記錄buf已使用的字節數量 // 即已保存的字符串的長度 int len; // buf 中剩餘可用的字節數量 int free; // 字節數組,用於保存字符串 char buf[]; };
- 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字符串函數庫裏面的函數 - redis使用sds進行字符串的表示,沒有使用C默認的
-
SDS和C字符串
3.1.C字符串
C字符串的結構如上所示,C字符串不能滿足Redis對字符串的安全性、效率以及功能方面的需求,所以redis纔會使用sds
3.2. C字符串的問題- 獲取字符串長度,獲取一個C字符串的長度,程序必須遍歷整個字符串,直到遇到結尾的空字符\0爲止,時間複雜度爲O(n)
- 緩衝區溢出,如果存在兩個字符串s1和s2,兩個字符串是緊鄰的如下圖所示,其中s1內容爲Redis,s2內容爲MongoDB,內存結構圖如下所示:
此時對s1進行append操作,使用如下函數:char *strcat(char *dest, const char *src); 執行: strcat(s1, " Cluster");
但是append前並沒有對s1分配足夠的內存空間,就會導致s1的數據溢出到s2,導致s2的內容被修改。如下圖所示:
- 內存頻繁分配和回收內存,當對字符串頻繁的修改時,C字符串需要頻繁的分配和回收內存。
- 二進制安全 ,C 字符串中的字符必須符合某種編碼(比如 ASCII), 並且除了字符串的末尾之外, 字符串裏面不能包含空字符, 否則最先被程序讀入的空字符將被誤認爲是字符串結尾。舉個栗子:
上圖的字符串使用了空字符分隔,此時C字符串所用的函數只能識別其中的 “Reids”,會忽略後面的"Cluster"
3.3 SDS如何解決這些問題
-
獲取字符串的長度,在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; }
-
杜絕內存溢出,當 sds api 需要對 sds 進行修改時, api會先檢查 sds的空間是否滿足修改所需的要求, 如果不滿足的話, api會自動將 sds 的空間擴展至執行修改所需的大小, 然後才執行實際的修改操作。
-
減少修改字符串時帶來的內存分配次數,C字符串修改字符串需要重新分配內存,否則會導致內存溢出,SDS採取空間預分配和惰性空間釋放方法從而較少內存分配次數
空間預分配:- sds api對sds修改時,如果此時需要對sds的空間進行擴展,sds不僅會分配當前操作所需要的空間,還會爲sds分配額外的未使用空間,這樣redis在後續執行字符串增長操作到時候可以減少內存重新分配的次數。
- 對sds修改後,如果len小於1M,則程序分配的和len相同的大小未使用空間,也就說此時len和free值是相等的。舉個栗子:對sds的修改,sds的len變成了13字節,程序會分配13字節的未使用空間,則sds的buf數組的實際長度爲13+13+1 = 27字節,即len+free+空字符=27字節
- 對sds修改後,如果len大於1MB,則程序會分配1MB的未使用空間,舉個栗子:sds修改後爲3MB,那麼程序會分配1MB的未使用空間,則buf數組的實際長度爲3MB+1MB+1byte
- 通過這種預分配策略, sds 將連續增長 N 次字符串所需的內存重分配次數從必定 N 次降低爲最多 N 次。(因爲free的空間有可能不滿足當前修改操作的所需空,所以是降低爲最多N次)
惰性空間釋放:
- sds的api對sds的縮短操作時,程序不會立即回收內存,而是使用free屬性將這些需要回收字節的數量記錄起來,等待將來使用。字符串”XYXXYabcXYY”移除所有的“X”和“Y”,過程如下圖所示。
如上圖所示,sds並沒有釋放多出來的 8 字節空間, 而是將這 8 字節空間作爲未使用空間保留在了 sds裏面, 如果將來要對 sds進行增長操作的話, 這些未使用空間就可能會派上用場
-
二進制安全,sds的api都是二進制安全的,所有 SDS API 都會以處理二進制的方式來處理 SDS 存放在 buf 數組裏的數據, 程序不會對其中的數據做任何限制、過濾、或者假設 —— 數據在寫入時是什麼樣的, 它被讀取時就是什麼樣。
-
總結
sds比起c字符串有以下優點:- 常數複雜度獲取字符串長度
- 杜絕緩衝區溢出
- 減少修改字符串長度時所需的內存重分配次數
- 二進制安全
- 兼容部分 C 字符串函數
-
參考資料
<<Redis設計與實現>>
Redis學習筆記-簡單動態字符串SDS
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.