1. 程式人生 > 實用技巧 >redis的資料結構——簡單動態字串

redis的資料結構——簡單動態字串

一、前言

redis採用C語言來實現,但並沒有使用C語言傳統字串的表示,而是自己構建了一種名為簡單動態字串(simple dynamic string,SDS)的資料結構,並將SDS作為Redis的預設字串表示。在Redis裡C字串只會作為字面量用在一些無需對字串修改的地方,入列印日誌。

二、一個簡單的例子

執行下圖的命令,那麼redis將建立一個鍵值對。其中:

  • 鍵值對的鍵是一個字串物件,物件的底層實現是一個儲存著"msg"字串的SDS。
  • 鍵值對的值是一個字串物件,物件的底層實現是一個儲存著"hellow world"字串的SDS。

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

那麼Redis將在資料庫中建立一個新的鍵值對。其中:

  • 鍵值對的鍵是一個字串物件,物件的底層實現是一個儲存著”fruit“字串的SDS。
  • 鍵值對的值是一個列表物件,該列表物件包含三個字串物件,這三個字串物件都是由SDS實現的:第一個SDS儲存著字串”apple“,第二個SDS儲存著字串”banana“,第三個SDS儲存著字串”cherry“。

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

三、SDS的定義

每一個SDS都是由如下結構表示的:

struct sdshdr {
      
      
//記錄buf陣列中已經使用的位元組數 //等於SDS所儲存字串的長度 int len; //記錄buf陣列中未使用位元組的數量 int free; //位元組陣列,用於儲存字串 char buf[]; };

上圖展示了一個Redis的SDS示例:

  • free屬性的值為0,表示這個SDS沒有分配任何未使用的空間。
  • len屬性的值為5,表示這個SDS儲存了一個5位元組長的字串。
  • buf屬性是一個char型別的陣列,陣列的前五個位元組分別儲存了‘R’,‘e’、‘d’、‘i’、‘s’五個字元。而最後一個位元組儲存了空字元‘\0’。

需要注意的是,SDS遵循C語言字串以空字元結尾的規範,儲存的空字元的一個位元組不計算到len屬性中,並且為空字元分配額外的空間,以及新增空字元到字串末尾都是由SDS的函式自動完成的,所以這個空字串在SDS中是完全透明的。遵循C語言字串的規範帶來的好處是可以複用C語言提供的一些字串操作函式。

四、SDS與C字串的區別

1、常數複雜度獲取字串長度

因為C語言字串並不記錄自身長度資訊,所以想要得到C語言字元長度必須遍歷字串,直到遇到結尾的空字串,這個操作時間複雜度為O(n)。而SDS通過len屬性記錄了字串的長度,只要訪問len屬性就可以得到字串的長度,這個操作的時間複雜度為O(1)。設定和更新SDS長度的工作是由SDS的API在執行時自動完成的,使用SDS無需進行任何手動修改長度的工作。

2、杜絕緩衝區溢位

除了獲取字串長度時間複雜度高之外,C字串不記錄自身長度帶來的另一個問題是容易造成緩衝區溢位(buffer overflow)。例如對C字串進行拼接操作時使用char *strcat (char *dest, char *src) 函式。如果沒有為dest分配足夠多的記憶體,那麼拼接時就會產生緩衝區溢位。

與C語言不同的是SDS的空間分配策略完全杜絕了發生緩衝區溢位的可能性:當SDS的API需要修改SDS時,API會先檢查SDS的空間是否滿足修改需求,如果不滿足,API自動將SDS的空間擴充套件至執行修改所需要的大小,然後再進行修改,所以SDS既不需要手動修改SDS空間的大小,也不會出現緩衝區溢位的問題。

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

對與一個包含N個字元的字串,C語言的底層實現總是一個N+1長度的字元陣列。因為C字串的長度和底層陣列的長度存在這種關聯性,所以每次增長或縮短一個C字串時,程式都總要對儲存這個C字串的陣列進行一次記憶體重分配。

  • 如果程式要增長字串,那麼在執行這個操作之前需要通過記憶體重分配擴充套件字元陣列的空間大小——如果不進行會產生緩衝區溢位。
  • 如果程式要縮短字串,那麼在執行這個操作之前需要通過記憶體重分配來釋放字串不在使用的那部分空間——如果不進行會產生記憶體洩漏。

因為記憶體重分配涉及複雜的演算法,並且可能會發生系統呼叫,所以它通常是一個比較耗時的操作:一般的程式中,如果修改字串的操作不太頻繁出現,那麼每次執行記憶體重分配是可以接受的,但redis經常被用於速度要求嚴苛,資料被頻繁修改的場合,如果每次修改字串都需要執行記憶體重分配,那麼光是分配記憶體就佔據了大量的時間,如果這種修改頻繁發生的話,可能還會對效能造成影響。

為了避免這種情況的發生,SDS實現了空間預分配惰性空間釋放兩種優化策略。

空間預分配

當API需要對SDS的空間進行擴充套件時,程式不僅會為SDS分配擴充套件所必須的空間,還會為SDS分配額外未使用的空間。

額外分配空間的策略如下:

  1. 如果對SDS進行修改後,SDS的長度(len屬性的值)小於1MB,那麼程式分配和len屬性相同大小的未使用空間,這時len屬性的值將和free屬性的值相同。例如,如果修改之後的len將變成13個位元組,那麼程式也會分配13個位元組的未使用空間,buf陣列的長度將變成13+13+1=27。
  2. 如果對SDS進行修改後,SDS的長度大於等於1MB,那麼程式會分配1MB的未使用空間。舉個例子,如果進行修改之後,SDS的len屬性變成30MB,那麼程式會分配1MB的未使用空間,SDS的buf陣列的實際長度為30MB + 1MB + 1byte。

通過空間預分配策略,Redis可以減少連續執行字串增長操作所需要的記憶體分配次數,SDS連續增長N次字串所需的記憶體分配次數從必定N次降低為最多N次。

惰性空間釋放

當進行縮短SDS儲存的字串時,程式並不立即使用記憶體重分配來回收縮短後多出來的位元組,而是使用free屬性將這些位元組的數量記錄下來,並等待將來使用。同時SDS也提供了相應的API,讓我們可以在有需要時,真正釋放SDS未使用的空間,所以不用擔心惰性空間釋放策略會造成空間浪費。

4、二進位制安全

C字串必須符號某種編碼(如ASCII編碼),並且除了字串的末尾之外,字串裡面不能包含空字元,否則會認為是字串的結尾,這些限制使得C字串只能儲存文字資料。不能儲存圖片、音訊、視訊、壓縮檔案這樣的二進位制資料。而SDS的API都是二進位制安全的,所有SDS 的API都會以處理二進位制的方式來處理SDS存放在buf數組裡的資料,程式不會對其中的資料做任何限制、過濾或假設,資料寫入時什麼樣子,讀取時就是什麼樣。這也是我們將SDS的buf屬性稱為位元組陣列的原因——Redis不是用這個陣列儲存字元,而是用它來儲存一系列二進位制資料。所以SDS不僅可以儲存文字資料,還可以儲存任意格式的二進位制資料。

5、相容部分C字串函式

雖然SDS的API是二進位制安全的,但它們一樣遵循C字串以空字元結尾的的慣例:這些API會將SDS儲存的資料的末尾設定為空字元,並且總會為buf陣列分配空間是多分配一個位元組容納這個空字元,這是為了讓那些儲存文字資料的SDS可以重用一部分C定義的函式,避免程式碼重複。

五、總結

Redis只會使用C字串作為字面量,大多數情況下,Redis使用SDS作為字串表示。

比起C字串,SDS具有如下優勢:

  1. 常數複雜度獲取字串長度。
  2. 杜絕緩衝區溢位。
  3. 減少修改字串帶來的空間重分配次數。
  4. 二進位制安全。
  5. 重用部分C語言字串函式。