redis 系列3 簡單動態字串 SDS
一. SDS概述
Redis 沒有直接使用C語言傳統的字串表示,而是自己構建了一種名為簡單動態字串(simple dynamic string, SDS)的抽象型別,並將SDS用作Redis的預設字串表示。Redis只會使用C字串作為字面量。在Redis裡,使用SDS來表示字串值,是一個可以被修改的字串,字串“鍵值對”底層都是由SDS實現的。
-- 例1:客戶端執行如下命令: 127.0.0.1:6379> set msg "hello world" OK 127.0.0.1:6379> get msg "hello world"
上面例1中就在資料庫裡建立一個新的鍵值對。 其中“鍵”是一個字串物件,物件的底層實現是一個儲存著字串"msg" 的SDS。 "鍵值" 也是一個字串物件,物件的底層實現是一個儲存著字串" hello world " 的SDS。
-- 例2: 客戶端執行如下命令: 127.0.0.1:6379> rpush fruits "apple" "banana" "cherry" (integer) 3 127.0.0.1:6379> lrange fruits 0 -1 1) "apple" 2) "banana" 3) "cherry"
上面例2中也在資料庫裡建立一個新的鍵值對。其中“鍵”是一個字串物件,物件的底層實現是一個儲存著字串" fruits " 的SDS。"鍵值"是一個列表物件,列表包含了三個字串物件,分別由三個SDS實現。
SDS除了用來儲存資料庫中的字串值之外,還用作緩衝區(buffer): AOF模組中的AOF緩衝區,以及客戶端狀態中的輸入緩衝區。
二. SDS 定義
每個SDS.h檔案下的sdshdr結構表示一個SDS值, 下面是Redis原始碼,在github的地址是https://github.com/antirez/sds
struct sdshdr{ //記錄buf陣列中已使用位元組的數量,也就是字串的長度 int len ; // 記錄buf陣列中未使用位元組的數量 int free; //位元組陣列,用於儲存字串 char buf[]; }
在C語言中使用長度為N+1的字元陣列來表示長度為N的字串,並且字元陣列最後一個元素總是空字元'\0'。 假設SDS的值為 "Redis",那麼free屬性值為0, len 屬性值為5, buf陣列為R,e,d,i,s五個字元,最後一個位元組則儲存空字元'\0' 。
三. SDS與C字串的區別
C語言使用簡單的字串表示方式,並不能滿足Redis對字串在安全性,效率以及功能方面的要求,從幾個方面說明:
1. 常數複雜度獲取字串長度
因為c字串並不記錄自身的長度資訊,所以為了獲取一個c字串的長度,程式必須遍歷整個字串。與C 字串不同,因為SDS在len屬性中記錄了SDS本身的長度,對於SDS的值為"Redis"的位元組長度就是5。
2. 杜絕緩衝區溢位
在c中, 假設緊鄰的字串s1 和 s2, s1儲存為"redis", s2儲存為"mongodb", 如果修改s1的值為 redis cluster, 但修改之前沒了空間,那麼s1的資料將溢位到s2所在空間中。
在SDS中,會先檢查給定的SDS空間是否足夠,會自動擴充套件修改所需的大小空間。然後在執行實際的修改操作。
3. 減少修改字串時帶來的記憶體重分配次數
在c 中,字串的底層實現總是一個N+1個字元長的陣列,因為字串的長度和底層陣列的長度之間存在著這種關聯,所以每次增長或者縮短一個c字串,程式都要對儲存這個C字串的陣列進行一次記憶體重分配操作。
在SDS中通過未使用空間解除了字串長度和底層陣列長度之間的關聯,buf陣列的長度不一定就是字元數量加1, 數組裡機可以包含未使用的位元組,這些由free屬性記錄。
3.1 空間預分配
當SDS的API對一個SDS進行操作,並且需要對SDS進行空間擴充套件的時候,程式不僅會為SDS 分配修改所必須要的空間,還會為SDS分配額外的未使用空間。額外分配的未使用空間數量由以下公式決定:
如果對SDS進行修改之後,SDS的長度(也即是len屬性的值) 將小於1MB,那麼程式分配和len屬性同樣大小的未使用空間。這時SDS len屬性的值將和fee屬性的值相同,例如:修改之後,SDS的len將變成13位元組, 那麼程式也會分配13位元組的未使用空間,SDS的buf陣列的實際長度將變成13+13+1=27位元組。
如果對SDS進行修改之後,SDS的len大於等於1MB, 那麼程式會分配1MB的未使用空間,如果對SDS進行修改之後, SDS的len變成30MB,那麼程式會分配1MB的未使用空間,SDS的buf陣列的實際長度為30MB + 1MB +1byte。
通過空間預分配策略,Redis可以減少連續執行字串增長操作所需的記憶體重分配次數。
3.2 惰性空間釋放
惰性空間釋放用於優化SDS的字串縮短操作。當SDS的API需要縮短SDS儲存的字串時,程式並不立即使用記憶體重分配來回收縮短多出來的位元組,而是使用free屬性將這些位元組的資料記錄起來,並等待將來使用(縮短後未使用的空間不會釋放,而是將來增長操作時,再使用這些未使用空間)。
4. 二進位制安全
在c字串的字元必須符合某種編碼(如ASCII), 並且除了字串的末尾之處,字串裡面不能包含空字元,否則最先被程式讀入的空字元將被誤以為是字串的結尾,這使得c字串只能儲存文字資料,不能儲存圖片,音訊,視訊,壓縮檔案之類的二進位制資料。
為了保證Redis 可以適用各種不同的使用場景,SDS的API 都是二進位制安全的,程式不會對其中的資料做任何限制,過濾,資料寫入是什麼,讀取時就是什麼。
5. 相容部分C字串函式
在SDS中會遵循C字串以空字元結尾的慣例,總會為buf陣列分配空間時多分配一個位元組來容納這個空字元,這是為了讓SDS的字串可以重用一部分(string.h>庫定入的函式。
四 總結
4.1 C字串與SDS之間的區別總結
C字串 |
SDS |
獲取字串長度的複雜度為0(N) |
獲取字串長度的複雜度為0(1) |
API是不安全的,可能會造成緩衝區溢位 |
API是安全的,不會造成緩衝區溢位 |
修改字串長度N次必然需要執行N次記憶體重分配 |
修改字串長度N次最多需要執行N次記憶體重分配 |
只能儲存文字資料 |
可以儲存文字或者二進位制資料 |
可以使用所有<string.h>庫中函式 |
可以使用一部分<string.h>庫中函式 |
4.2 SDS API(主要的一些API)
函式 |
作用 |
sdsnew |
建立一個SDS字串 |
sdsempty |
建立一個不包含內容的空SDS 字串 |
sdsfree |
釋放給定的SDS字串未使用空間 |
sdslen |
返回SDS字串已使用空間位元組數 |
sdsavail |
返回SDS字串未使用空間位元組數 |
sdsdup |
建立一個給定SDS的副本 |
sdsclear |
清空SDS字串內容 |
sdscat |
將給定c字元拼接到SDS字串的末尾 |
sdscatsds |
將給定的SDS字串拼接到另一個SDS字串的末尾 |
sdscpy |
將給定的c字元拼複製到SDS裡機,覆蓋SDS原有字串 |
sdsgrowzero |
用空字元將SDS擴充套件至指定長度 |
sdsrange |
保留SDS指定區間內的資料,不在區間內的資料會被覆蓋或清除 |
sdstrim |
接受一個SDS和一個C字串作為引數,從SDS中移除所有在C字串中出現過的字元 |
sdscmp |
對比兩個SDS字串是否相同 |