redis的字符串底層結構
struct sdshdr {
// 記錄 buf 數組中已使用字節的數量
// 等於 SDS 所保存字符串的長度
int len;
// 記錄 buf 數組中未使用字節的數量
int free;
// 字節數組,用於保存字符串
char buf[];
};
遵循空字符結尾這一慣例的好處是, SDS 可以直接重用一部分 C 字符串函數庫裏面的函數。C保存的原因我認爲是判斷字符串長度的一個臨界值
那麼它與c字符串的區別?
-
讀取字符串效率
c字符串通過一個個字節的長度讀取,時間是O(n),而SDS則直接獲取len屬性就可以了==,時間是O(1);比如說, 因爲字符串鍵在底層使用 SDS 來實現, 所以即使我們對一個非常長的字符串鍵反覆執行 STRLEN 命令, 也不會對系統性能造成任何影響, 因爲 STRLEN 命令的複雜度僅爲 O(1) 。
-
杜絕緩衝區溢出
C 字符串不記錄自身長度帶來的另一個問題是容易造成緩衝區溢出,比如一個字符串Redis\0hello,在C字符串Redis後加入個hell,他會因爲直接覆蓋後面的數據,就變成Redishello\0,在執行拼接操作之前檢查
s
的長度是否足夠, 在發現s
目前的空間不足以拼接,Redis中就是自動將 SDS 的空間擴展至執行修改所需的大小,再修改,先檢查給定 SDS 的空間是否足夠 -
減少修改字符串時帶來的內存重分配次數
因爲 C 字符串的長度和底層數組的長度之間存在着這種關聯性, 所以每次增長或者縮短一個 C 字符串, 程序都總要對保存這個 C 字符串的數組進行一次內存重分配操作
比如增加字符,不重排內存分配來擴展數組大小的話,就會上面說的緩衝區溢出
比如減少字符,不重排內存分配來釋放空間,就容易內存泄漏
如果修改字符串長度的情況不太常出現, 那麼每次修改都執行一次內存重分配是可以接受的。
如果每次修改字符串的長度都需要執行一次內存重分配的話, 那麼光是執行內存重分配的時間就會佔去修改字符串所用時間的一大部分, 如果這種修改頻繁地發生的話, 可能還會對性能造成影響。
如何避免空間增長呢?
空間預分配: 在存的數據小於1M時 存3字節的字符串會多分配3字節+1字節存空字符
超過1M,比如存30M就多分配1M+1字節
通過空間預分配策略, Redis 可以減少連續執行字符串增長操作所需的內存重分配次數。
在擴展 SDS 空間之前, SDS API 會先檢查未使用空間是否足夠, 如果足夠的話, API 就會直接使用未使用空間, 而無須執行內存重分配。
如何避免空間浪費呢?
惰性空間釋放: 當 SDS 的 API 需要縮短 SDS 保存的字符串時, 程序並不立即使用內存重分配來回收縮短後多出來的字節, 而是使用 free
屬性將這些字節的數量記錄起來, 並等待將來使用。
-
二進制安全
C字符串無法存放帶有空字符的數據,不然誤以爲是結尾,到時候判斷長度會錯,而redis使用len字段獲取長度就避免了這個問題,而且redis存放的是字節,而不是字符,所以本身就是二進制安全的
C 字符串 | SDS |
---|---|
獲取字符串長度的複雜度爲 O(N) 。 | 獲取字符串長度的複雜度爲 O(1) 。 |
API 是不安全的,可能會造成緩衝區溢出。 | API 是安全的,不會造成緩衝區溢出。 |
修改字符串長度 N 次必然需要執行 N 次內存重分配。 |
修改字符串長度 N 次最多需要執行 N 次內存重分配。 |
只能保存文本數據。 | 可以保存文本或者二進制數據。 |
可以使用所有 <string.h> 庫中的函數。 |
可以使用一部分 <string.h> 庫中的函數。 |