redis沒有直接使用c語言傳統(tǒng)的字符串表示(以空字符結(jié)尾的字符數(shù)組),而是自己構(gòu)建了一種“簡單動態(tài)字符串”(simple dynamic string 谍珊,SDS)用作Redis的默認字符串表示捅暴。
作用
- 保存數(shù)據(jù)庫中字符串值
- 用作緩沖區(qū):AOF模塊中的AOF緩沖區(qū)盲赊,以及客戶端狀態(tài)中的輸入緩沖區(qū)
2.1 SDS的定義
數(shù)據(jù)結(jié)構(gòu)
struct sdshdr{
//記錄buf數(shù)組已使用字節(jié)的數(shù)量碟渺,等于SDS所保存的字符串長度
int len;
//記錄buf數(shù)組中未使用的字節(jié)數(shù)量
int free;
//字節(jié)數(shù)組叔扼,用于保存字符串
char buf[]
}
實例圖
- free屬性值為0览徒,表示sds沒有分配任何未使用空間
- len屬性值為5狈定,表示sds保存了一個5字節(jié)長的字符串
- buf屬性是一個char類型的數(shù)組,分別保存了'R','e',...'s' 五個字符习蓬,最后一個字節(jié)保存空字符 '\0';
SDS遵循C字符串以空字符結(jié)尾的慣例纽什,保存空字符的1字節(jié)空間不計算在SDS的len屬性里面;為空字符分配額外的1字節(jié)空間躲叼,以及添加空字符到字符串末尾操作芦缰,都是由SDS函數(shù)自動完成的;遵循空字符串結(jié)尾好處是枫慷,可以直接重用一部分c字符串函數(shù)庫里的函數(shù)让蕾。例:printf() 函數(shù)等
2.2 SDS 與 C 字符串的區(qū)別
C語言使用長度為N+1的字符數(shù)組,表示長度為N的字符串流礁,并且最后一位元素總是空字符'\0'涕俗,這種表達方式不能滿足Redis對字符串在安全性,效率神帅,以及功能上面的要求
C字符串 | SDS |
---|---|
獲取字符串長度的復雜度為O(n) | 獲取字符串長度的復雜度為O(1) |
API是不安全的再姑,會造成緩沖區(qū)溢出 | API是安全的,不會造成緩沖區(qū)溢出 |
修改字符串長度N次必然需要執(zhí)行N次內(nèi)存重分配 | 修改字符串長度N次最多修改執(zhí)行N次內(nèi)存重分配 |
只能保存文本數(shù)據(jù) | 可以保存文本或者二進制數(shù)據(jù) |
可以使用所有<string.h>庫中的函數(shù) | 可以使用一部分<string.h>庫中的函數(shù)? |
2.2.1 常數(shù)復雜度獲取字符串長度
c字符串不記錄自身的長度信息找御,獲取一個c字符串的長度必須遍歷整個字符串元镀,遇到結(jié)尾的空字符串為止绍填,復雜度為O(N)。
SDS在len屬性中記錄了SDS本身的長度栖疑,復雜度為O(1)讨永。(?設置和更新SDS長度是由SDS的API執(zhí)行時自動完成的)
2.2.2 杜絕緩沖區(qū)溢出
假設有兩個在內(nèi)存中相鄰的兩個c字符串s1和s2,其中s1 保存了“Redis” 遇革,s2保存了“MongoDB”卿闹,通過執(zhí)行strcat(s1, "Cluster")(拼接函數(shù)) ,若忘記在執(zhí)行strcat之前為s1分配足夠的內(nèi)存空間萝快,在執(zhí)行完strcat函數(shù)后锻霎,s1的數(shù)據(jù)將溢出到s2所在的空間中,導致s2保存的內(nèi)容被意外的修改
SDS的內(nèi)存空間完全的杜絕了緩沖區(qū)溢出的可能性:當SDS api 需要對SDS進行操作的時候揪漩,會先檢查SDS的空間是否滿足修改所需的要求旋恼;如果不滿足的話,API會自動將SDS的空間擴大至所需要的大小奄容,然后才執(zhí)行修改操作冰更。所有,SDS不需要手動的修改SDS空間大小昂勒,也不會出現(xiàn)c語言的緩沖區(qū)溢出問題蜀细。
修改前
修改后
2.2.3 減少修改字符串時帶來的內(nèi)存重分配次數(shù)
c字符串的底層實現(xiàn)總是一個N+1個字符長的數(shù)組,所以每次增長或縮短一個c字符串,程序都總要對保存這個c字符串的數(shù)組進行一次內(nèi)存重分配操作:
- 增長字符串操作,比如拼接祝闻,執(zhí)行這個操作之前,需要通過內(nèi)存重新分配來擴展底層數(shù)組的空間大小涣觉,如果忘了會產(chǎn)生緩沖區(qū)溢出。
- 縮短字符串操作血柳,比如截斷官册,執(zhí)行這個操作之后,需要通過內(nèi)存重分配釋放不再使用的那部分空間难捌,如果忘了就會產(chǎn)生內(nèi)存泄漏膝宁。
c字符串內(nèi)存重新分配涉及復雜的算法,需要執(zhí)行系統(tǒng)的調(diào)用根吁,通常是一個比較耗時的操作:
- 一般程序中员淫,修改字符串長度不太常出現(xiàn),每次修改都執(zhí)行一次內(nèi)存重分配是可以接受的击敌。
- Redis作為數(shù)據(jù)庫介返,用于速度要求嚴格,數(shù)據(jù)頻繁修改的場合,每次修改字符串的長度都需要執(zhí)行一次內(nèi)存分配的話圣蝎,光是執(zhí)行內(nèi)存重分配的時間就會占去修改字符串所用時間的一大部分刃宵,修改頻繁的話,還會對性能造成影響徘公、
sds為了避免c字符串的這種缺陷牲证,通過未使用空間解除了字符串長度和底層數(shù)組長度的之間的關(guān)聯(lián);在sds中buf數(shù)組的長度不一定就是字符數(shù)量加一关面,數(shù)組里面包含了未使用的字節(jié)坦袍,由free屬性記錄;
通過未使用空間缭裆,實現(xiàn)了 空間預分配 和 惰性空間釋放 兩種優(yōu)化政策:
2.2.3.1 空間預分配
用于優(yōu)化sds字符串增長操作键闺,當進行空間擴展的時候,不僅會為sds分配修改所需要的空間澈驼,還會分配額外的未使用空間。
其中筛武,額外分配未使用空間數(shù)量缝其,由一下公式?jīng)Q定:
- sds len屬性 < 1MB,程序分配和len屬性同樣大小的未使用空間;
- sds len屬性 >= 1MB徘六,程序分配1MB的未使用空間内边;
通過空間預分配策略,Redis可以減少連續(xù)執(zhí)行字符串增長操作所需的內(nèi)存重分配次數(shù)待锈。
在擴展sds空間之前漠其,api會先檢查未使用的空間是否足夠,足夠的話竿音,直接使用未使用空間和屎,無須執(zhí)行內(nèi)存重分配。
通過這種預分配策略春瞬,將內(nèi)存分配次數(shù)從必定N次降低為最多N次
2.2.3.2 惰性空間釋放
用于優(yōu)化sds字符串縮短操作:通過api縮短sds保存的字符串時柴信,并不立即使用內(nèi)存重分配來回收縮短后多出來的字節(jié),而是通過free屬性將這些字節(jié)的數(shù)量記錄下來宽气,等待將來使用随常;
執(zhí)行 sdstrim(s, "XY"); //移除sds字符串中所有'X' 和 'Y'
執(zhí)行sdstrim之后的sds并沒有釋放多出來的8個字節(jié)空間,而是將這8個字節(jié)空間作為未使用空間保留在了sds里面萄涯,將來要執(zhí)行增長操作的話绪氛,未使用空間就能派上用場。
通過惰性空間釋放策略涝影,sds避免了縮短字符串所需的內(nèi)存重分配操作枣察,并為將來可能有的增長操作提供了優(yōu)化。
sds也提供了相應的api袄琳,在我們需要的時候询件,真正的釋放sds未使用空間燃乍,不用擔心惰性空間釋放策略會造成內(nèi)存的浪費
2.2.4 二進制安全
c字符串中的字符必須符合某種編碼(ASCII) ,并且除了字符串末尾之外宛琅,其他地方不能包含空字符刻蟹,否則最先被程序讀入的空字符將被誤以為是字符串結(jié)尾,也限制了c字符串只能保存文本數(shù)據(jù)嘿辟,不能保存圖片舆瘪,音頻,視頻红伦,壓縮文件這樣的二進制數(shù)據(jù)英古。
為了確保Redis適用于不同的使用場景,SDS api 都是二進制安全的昙读,所有api都會以處理二進制方式來處理sds存放在buf數(shù)組里的數(shù)據(jù)召调,并且不會對其中的數(shù)據(jù)做任何限制,過濾蛮浑;sds使用的len屬性的值唠叛,而不是空字符來判斷字符串是否結(jié)束。
2.2.5兼容部分C字符串函數(shù)
雖然sds的api都是二進制安全的沮稚,但一樣遵循c字符串以空字符結(jié)尾的慣例艺沼,為保證保存文本數(shù)據(jù)可以重用一部分<string.h>定義的函數(shù)
2.3 SDS API
函數(shù):作用 | 時間復雜度 |
---|---|
sdsnew: 創(chuàng)建一個包含給定c字符串的SDS | O(n) |
sdsempty: 創(chuàng)建一個不包含任何內(nèi)容的空SDS | O(1) |
sdsfree: 釋放給定的sds | O(n) |
sdslen: 返回sds的已使用空間字節(jié)數(shù) | 通過讀取len屬性 O(1) |
sdsavail: 返回sds未使用的空間字節(jié)數(shù) | 通過讀取free屬性 O(1) |
sdsdup: 創(chuàng)建一個給定sds的副本 | O(n) |
sdsclear: 清空sds保存的字符串內(nèi)容 | 惰性空間釋放策略 O(1) |
sdscat: 將給定的c字符串拼接到sds字符串末尾 | O(n) |
sdscatsds: 將給定的sds字符串拼接到另一個sds字符串的末尾 | O(n) |
sdscpy: 給定的c字符串復制到sds里面,覆蓋sds原有的字符串 | O(n) |
sdsgrowzero: 用空字符串將sds擴展至給定長度 | O(n) |
sdsrange: 保留給定區(qū)間內(nèi)的數(shù)據(jù)蕴掏,不在區(qū)間內(nèi)數(shù)據(jù)會被覆蓋或清除 | O(n) |
sdstrim: 接受一個sds 和一個c字符串作為參數(shù)障般,從sds左右兩端分別移除所有在c字符串中出現(xiàn)過的字符 | O(m*n) m為sds的長度,n為給定c字符串長度 |
sdscmp: 對比兩個字符串是否相同 | O(n) |