Redis中的簡單動態字串

2021-07-11 02:45:12 字數 3901 閱讀 2350

redis沒有直接使用c語言傳統的字串表示(以空字元結尾的字元陣列,以下簡稱c字串),而是自己構建了一種名為簡單動態字串(****** dynamic string,sds)的抽象型別,並將sds用作redis的預設字串表示。

在redis裡面,c字串只會作為字串字面量(string literal)用在一些無須對字串值進行修改的地方,比如列印日誌:

redislog(redis_warning, "redis is now ready to exit, bye bye...");
當redis需要的不僅僅是乙個字串字面量,而是乙個可以被修改的字串值時,redis就會使用sds來表示字串值,比如在redis的資料庫裡面,包含字串值的鍵值對在底層都是由sds實現的。

舉個例子,如果客戶端執行命令:

127.0

.0.1:6379> set msg "hello world"

ok

那麼redis將在資料庫中建立乙個新的鍵值對,其中:

又比如,如果客戶端執行命令:

127.0

.0(integer) 3

那麼redis將在資料庫中建立乙個新的鍵值對,其中:

除了用來儲存資料庫中的字串值之外,sds還被用作緩衝區(buffer):aof模組中的aof緩衝區,以及客戶端狀態中的輸入緩衝區,都是由sds實現的。

每個sds.h/sdshdr結構表示乙個sds值:

struct sdshdr ;
sds遵循c字串以空字串結尾的慣例,儲存空字串的1位元組空間不計算在sds的len屬性裡面,並且為空字元分配額外的1位元組空間,以及新增空字元到字串末尾等操作,都是由sds函式自動完成的,所以這個空字元對於sds的使用者來說是完全透明的。遵循空字元結尾這一慣例的好處是,sds可以直接重用一部分c字串函式庫裡面的函式。

舉個例子,如果我們有乙個指向上圖所示sds的指標s,那我們就可以直接使用/printf函式,通過執行以下語句:

printf("%s", s->buf);
來列印出sds儲存的字串值redis,而無須為sds編寫專門的列印函式。

繼續觀察上圖,sds為buf陣列分配了五位元組未使用空間,所以它的free屬性的值為5(圖中用五個空格來表示五位元組的未使用空間)。

根據傳統,c語言使用長度為n+1的字元陣列來表示長度為n的字串,並且字元陣列的最後乙個元素總是空字元』\0』。

c語言使用的這種簡單的字串表示方式,並不能滿足redis對字串在安全性、效率以及功能方面的要求,下面將詳細對比sds與c字串之間的區別,並說明c字串更適用於redis的原因。

常數複雜度獲取字串長度

因為c字串並不記錄自身的長度資訊,所以為了獲取乙個c字串的長度,程式必須遍歷整個字串,對遇到的每個字元進行計數,直到遇到代表字串結尾的空字元為止,這個操作的複雜度為o(n)。

和c字串不同,因為sds在len屬性中記錄了sds本身的長度,所以獲取乙個sds長度的複雜度即為o(1)。

設定和更新sds長度的工作是由sds的api在執行時自動完成的,使用sds無須進行任何手動修改長度的工作。

通過使用sds而不是c字串,redis將獲取字串長度所需的複雜度從o(n)降低到o(1),這確保了獲取字串長度的工作不會成為redis的效能瓶頸。

杜絕緩衝區溢位

除了獲取字串長度的複雜度高之外,c字串不記錄自身長度帶來的另乙個問題是容易造成緩衝區溢位(buffer overflow)。舉個例子,/strcat函式可以將src字串中的內容拼接到dest字串的末尾:

char *strcat(char *dest, const

char *src);

因為c字串不記錄自身的長度,所以strcat假定使用者在執行這個函式時,已經為dest分配了足夠多的記憶體,可以容納src字串中的所有內容,而一旦這個假定不成立時,就會產生緩衝區溢位。

與c字串不同,sds的空間分配策略完全杜絕了發生緩衝區溢位的可能性:當sds api需要對sds進行修改時,api會先檢查sds的空間是否滿足修改所需的要求,如果不滿足的話,api會自動將sds的空間擴充套件到執行修改所需的大小,然後才執行實際的修改操作,所以使用sds既不需要手動修改sds的空間大小,也不會出現前面所說的緩衝區溢位問題。

減少修改字串時帶來的記憶體重分配次數

正如之前所說,因為c字串並不記錄自身的長度,所以對於乙個包含了n個字元的c字串來說,這個c字串的底層實現總是乙個n+1個字元長的陣列(額外的乙個字元空間用於儲存空字元)。因為c字串的長度和底層陣列的長度之間存在著這種關聯性,所以每次增長或者縮短乙個c字串,程式都總要對儲存這個c字串的陣列進行一次記憶體重分配操作:

如果程式執行的是縮短字串的操作,比如截斷操作(trim),那麼在執行這個操作之後,程式需要通過記憶體重分配來釋放字串不再使用的那部分空間——如果忘了這一步就會產生記憶體洩漏。

為了避免c字串的這種缺陷,sds通過未使用空間解除了字串長度和底層陣列長度之間的關聯:在sds中,buf陣列的長度不一定是字元數量加一,陣列裡面可以包含未使用的位元組,而這些位元組的數量就由sds的free屬性記錄。

通過未使用空間,sds實現了空間分配和惰性空間釋放兩種優化策略。

1、空間預分配

空間預分配用於優化sds的字串增長操作:當sds的api對乙個sds進行修改,並且需要對sds進行空間擴充套件的時候,程式不僅會為sds分配修改所必須要的空間,還會為sds分配額外的未使用空間。

其中,額外分配的未使用空間數量由以下公式決定:

通過空間預分配策略,redis可以減少連續執行字串增長操作所需的記憶體重分配次數。

在擴充套件sds空間之前,sds api會先檢查未使用空間是否足夠,如果足夠的話,api就會直接使用未使用空間,而無須執行記憶體重分配。

通過這種預分配策略,sds將連續增長n次字串所需的記憶體重分配次數從必定n次降低為最多n次。

2、惰性空間釋放

惰性空間釋放用於優化sds的字串縮短操作:當sds的api需要縮短sds儲存的字串時,程式並不立即使用記憶體重分配來**縮短後多出來的位元組,而是使用free屬性將這些位元組的數量記錄起來,並等待將來使用。

通過惰性空間釋放策略,sds避免了縮短字串時所需的記憶體重分配操作,並為將來可能有的增長操作提供了優化。

與此同時,sds也提供了相應的api,讓我們可以在有需要時,真正地釋放sds的未使用空間,所以不用擔心惰性空間釋放策略會造成記憶體浪費。

二進位制安全

雖然資料庫一般用於儲存文字資料,但使用資料庫來儲存二進位制資料的場景也不少見,因此,為了確保redis可以適用於各種不同的使用場景,sds的api都是二進位制安全的(binary-safe),所有sds api都會以處理二進位制的方式來處理sds存放在buf陣列裡的資料,程式不會對其中的資料做任何限制、過濾、或者假設,資料寫入時是什麼樣的,它被讀取時就是什麼樣的。

這也是我們將sds的buf屬性稱為位元組陣列的原因——redis不是用這個陣列來儲存字元,而是用它來儲存一系列二進位制資料。

通過使用二進位制安全的sds,而不是c字串,使得redis不僅可以儲存文字資料,還可以儲存任意格式的二進位制資料。

相容部分c字串函式

雖然sds的api都是二進位制安全的,但它們一樣遵循c字串以空字元結尾的慣例:這些api總會將sds儲存的資料的末尾設定為空字元,並且總會在為buf陣列分配空間時多分配乙個位元組來容納這個空字元,這是為了讓那些儲存文字資料的sds可以重用一部分庫定義的函式。

通過遵循c字串以空字元結尾的慣例,sds可以在有需要時重用函式庫,從而避免了不必要的**重複。

Redis 簡單動態字串

在c語言中,乙個結構體中最後乙個成員的位址減去第乙個成員的位址,就是該結構體的大小 例如 struct sdshdr buf的位址減去len的位址,正好等於sizeof sdshdr 在c 中 char str nihao 是不被允許的,但是c語言中可以,並且可以直接列印str 輸出nihao,和c...

redis簡單動態字串

redis內部使用sds,簡單動態字串,sds是什麼 dynamic t包含字串長度,空間使用率,已使用,未使用等資訊的乙個結構體 sds比c語言字串的優點 獲取字串長度不需要進行遍歷,時間複雜度為o 1 杜絕緩衝區溢位 對sds進行修改的時候,api會先檢查sds的空間是否足夠,如果不需要的話,a...

redis 簡單動態字串

redis沒有直接使用c語言傳統的字串表示,而是自己構建了一種名為簡單動態字串 sds 的抽象型別,並將其作為redis的預設字串表示,即redis中包含字串值的鍵值對在底層都是由sds實現的。每個sds.h sdshdr結構表示乙個sds的值,如下所示 sds遵循c字串以空字元結尾的管理,儲存空字...