redis就是用c語言寫,但redis的string并沒有直接用c語言的string,而是自己搞了一個 SDS 結構體來表示字符串。
SDS 的全稱是 Simple Dynamic String,中文叫做“簡單動態字符串”。
想知道為什么這么做,我們先看看c語言的string是什么樣的。
C語言的string
- 本質:是 char 類型的一維數組。
- 結尾:必須以 \0 結束,表示字符串終止。
- 長度判斷:靠遍歷字符直到 \0 來判斷長度。

c語言的string存在著以下缺點,并且也是redis不使用其的原因:
- 判斷長度時不方便:
- 通過遍歷到末尾的空格進行判斷,復雜度為O(n)
- 擴容時不方便:
- 因為沒有預分配的內存,所以每次追加數據時就得重新申請一塊內存空間,十分消耗資源。并且在C語言中需要程序員手動分配內存進行擴容,若操作不當可能發生內存溢出。
- 特殊數據無法處理(二進制安全):
- 因為末尾時以結束符結尾,那么我實際要存儲的數據如果末尾也是結束符,兩個空格末尾就會發生沖突。而二進制數據中會很經常出現結束符,所以叫作二進制安全。
這些缺點不符合redis的高性能,為了避免這些缺點,redis自己搞了一個 SDS 結構體來表示字符串。
redis中的string
當你 set abc abcdefg 時,這個 set 命令會創建出兩個 sds,一個存 key:abc,一個存 value:abcdefg。
key 和 value 在 redisDb 的 dict 中通過鍵值對哈希表進行映射。
sds結構如下:
struct attribute ((packed)) sdshdr8 {uint8_t len; // 字符串長度,不包含結束標示uint8_t alloc; // 分配空間unsigned char flags; // SDS類型char buf[]; // 字符數組(實際數據)
};

len字段的作用:
維護buf[]數組的長度,用于快速O(1)獲取字符串的長度(因為字符串內容實際上存儲在數組里,字符串長度等價于數組長度)。
若沒有len字段維護的長度,當我們每次要獲取字符串長度時,都需要從頭到尾遍歷得到O(n)。
alloc字段的作用:
alloc 表示預分配的內存,也就是為了容納新增元素而預留的空間。
想象一下,若沒有預分配的內存,每次新增元素時,數組會因為空間不足,去重新申請一塊內存空間,十分消耗資源。
有了預分配內存后,若當前剩余的預分配內存足夠容納新增元素時,我們就不需要再去分配內存空間,這樣可以大幅度減少內存分配次數,提高性能。
flags字段的作用:
表示?type(4位)?和 encoding(4位) 兩個字段,加起來 8 位,用位域實現。
其中 type 表示對象的邏輯類型,例如 string、list、set 等。這里的 type 是 string;encoding 表示對象的底層編碼方式,比如 int、embstr、raw 等。
buf[]字段的作用:
用于存儲字符串實際內容。
擴容策略
當你要“寫入數據”導致 len + 新增數據長度 > alloc 時,就會觸發擴容機制。

惰性空間釋放
當sds的字符串縮短了,sds的buf內會多出來一些空間,這個空間并不會馬上被回收,而是暫時留著以防再用的時候進行多余的內存分配。這個是惰性空間釋放的策略
SDS的優勢:
優化獲取字符串長度:
C語言要想獲取字符串長度必須遍歷整個字符串的每一個字符,然后自增做累加,時間復雜度為O(n);sds直接維護了一個len變量,時間復雜度為O(1)。
減少內存分配:
當我們對一個字符串類型進行追加的時候,可能會發生兩種情況:
- 當前剩余空間(剩余空間 = alloc - len)足夠容納追加內容時,我們就不需要再去分配內存空間,這樣可以減少內存分配次數。
- 當前剩余空間不足以容納追加內容,我們需要重新為其申請內存空間。
惰性釋放空間
當你對一個 Redis 字符串執行縮短操作(比如刪掉部分字符)時,Redis 只更新 len 字段,而不會立刻縮小 alloc 所占的內存,多余的空間會被保留,等待將來復用,這樣就避免了頻繁的內存申請。
(如果你依舊想釋放多余空間,Redis 提供了手動釋放函數供調用。)
上面的SDS只是字符串類型中存儲字符串內容的結構,Redis中的字符串分為兩種存儲方式,分別是embstr和raw。
String的三種編碼格式(encoding)
String 在 Redis 中有三種編碼方式: int、embstr、raw 。
其中 raw 和 embstr 類型,都是基于動態字符串(SDS)實現的。

embstr
果存儲在 SDS 中的數據小于等于 44 字節,則會采用 EMBSTR 編碼,此時 RedisObject 與 SDS 是一段連續空間。而不是像 RAW 的編碼方式一樣,由 ptr 指向另外一片空間,申請內存時只需要調用一次內存分配函數,效率更高。

raw
raw 是 string 的基本編碼方式,基于簡單動態字符串(SDS)實現,存儲上限為512mb。當一個字符串采用 raw 的編碼方式的時候,它的結構如圖所示。

embstr的存儲方式是將RedisObject對象頭和SDS結構放在內存中連續的空間位置,也就是使用malloc方法一次分配,而raw需要兩次malloc,分別分配對象頭和SDS的空間。釋放空間也一樣,embstr釋放一次,raw釋放兩次,所以embstr是一種優化,
(malloc函數用于申請內存空間)
int
如果存儲的字符串是整數值,并且大小在 LONG MAX 范圍內,則會采用 INT 編碼。將字符串內容轉為 long,redisObject的對象 ptr 指向該long,并將 encoding 設置為 int,這樣就不需要重新開辟空間,算是長整形的一個優化。直接將數據保存在 RedisObject 的 ptr 指針位置(剛好8字節),不再需要SDS了。

為什么是44字節?
原因:對象頭占16字節,空的sdshdr占用4字節,也就是一個數據至少占用16+4=20字節。
其次操作系統使用jmalloc和tmalloc進行內存的分配,而內存分配的單位都是2的N次方,所以是 2,4,8,16,32,64 等字節,但是redis如果采取32的話,那么32-25=7,也太他媽少了,所以Redis采取的是64字節,所以:64-20=44。
盡量使用embstr和int編碼
在使用 string 類型時,盡可能讓其長度小于 44 字節,或者使用整數表示,使其使用 EMBSTR 和 INT 編碼。