1. 程式人生 > >圖解Redis之資料結構篇——簡單動態字串SDS

圖解Redis之資料結構篇——簡單動態字串SDS

圖解Redis之資料結構篇——簡單動態字串SDS

前言

    相信用過Redis的人都知道,Redis提供了一個邏輯上的物件系統構建了一個鍵值對資料庫以供客戶端使用者使用。這個物件系統包括字串物件,雜湊物件,列表物件,集合物件,有序集合物件等。但是Redis面向記憶體並沒有直接使用這些物件。而是使用了簡單動態字串,連結串列,字典(散列表),跳躍表,整數集合,壓縮列表這些資料結構來操作記憶體。

Redis物件結構

一、簡單動態字串(SDS)

    Redis預設並未直接使用C字串(C字串僅僅作為字串字面量,用在一些無需對字串進行修改的地方,如列印日誌)。而是以Struct的形式構造了一個SDS的抽象型別。當Redis需要一個可以被修改的字串時,就會使用SDS來表示。在Redis資料庫裡,包含字串值的鍵值對都是由SDS實現的(Redis中所有的鍵都是由字串物件實現的即底層是由SDS實現,Redis中所有的值物件中包含的字串物件底層也是由SDS實現)。

1.1 SDS

Redis簡單動態字串

struct sdshdr{
    //int 記錄buf陣列中未使用位元組的數量 如上圖free為0代表未使用位元組的數量為0
    int free;
    //int 記錄buf陣列中已使用位元組的數量即sds的長度 如上圖len為5代表未使用位元組的數量為5
    int len;
    //位元組陣列用於儲存字串 sds遵循了c字串以空字元結尾的慣例目的是為了重用c字串函式庫裡的函式
    char buf[];
}

二、為什麼要使用SDS

SDS與C字串

    上圖表示了SDS與C字串的區別,關於為什麼Redis要使用SDS而不是C字串,我們可以從以下幾個方面來分析。

2.1 緩衝區溢位

C字串記憶體溢位

    C字串,如果程式設計師在字串修改的時候如果忘記給字串重新分配足夠的空間,那麼就會發生記憶體溢位,如上圖所示,忘記給s1分配足夠的記憶體空間, s1的資料就會溢位到s2的空間, 導致s2的內容被修改.。而Redis提供的SDS其內建的空間分配策略則可以完全杜絕這種事情的發生。當API需要對SDS進行修改時, API會首先會檢查SDS的空間是否滿足條件, 如果不滿足, API會自動對它動態擴充套件, 然後再進行修改。

Redis SDS字串拼接

2.2 記憶體重分配

2.2.1 C字串記憶體重分配

    在C字串中,如果對字串進行修改,那麼我們就不得不面臨記憶體重分配。因為C字串是由一個N+1長度的陣列組成,如果字串的長度變長,我們就必須對陣列進行擴容,否則會產生記憶體溢位。而如果字串長度變短,我們就必須釋放掉不再使用的空間,否則會發生記憶體洩漏。

2.2.2 SDS空間分配策略

    對於Redis這種具有高效能要求的記憶體資料庫,如果每次修改字串都要進行記憶體重分配,無疑是巨大的效能損失。而Redis的SDS提供了兩種空間分配策略來解決這個問題。

  1. 空間預分配

    我們知道在陣列進行擴容的時候,往往會申請一個更大的陣列,然後把陣列複製過去。為了提升效能,我們在分配空間的時候並不是分配一個剛剛好的空間,而是分配一個更大的空間。Redis同樣基於這種策略提供了空間預分配。當執行字串增長操作並且需要擴充套件記憶體時,程式不僅僅會給SDS分配必需的空間還會分配額外的未使用空間,其長度存到free屬性中。其分配策略如下:

    • 如果修改後len長度將小於1M,這時分配給free的大小和len一樣,例如修改過後為10位元組, 那麼給free也是10位元組,buf實際長度變成了10+10+1 = 21byte
    • 如果修改後len長度將大於等於1M,這時分配給free的長度為1M,例如修改過後為30M,那麼給free是1M.buf實際長度變成了30M+1M+1byte

  2. 惰性空間釋放

    惰性空間釋放用於字串縮短的操作。當字串縮短是,程式並不是立即使用記憶體重分配來回收縮短出來的位元組,而是使用free屬性記錄起來,並等待將來使用。

    Redis 惰性空間釋放

Redis通過空間預分配和惰性空間釋放策略在字串操作中一定程度上減少了記憶體重分配的次數。但這種策略同樣會造成一定的記憶體浪費,因此Redis SDS API提供相應的API讓我們在有需要的時候真正的釋放SDS的未使用空間。

2.3 二進位制安全

    C字串中的字元必須符合某種編碼(比如ASCII),並且除了字串的末尾之外,字串裡面不能包含空字元,否則最先被程式讀入的空字元將被誤認為是字串結尾,這些限制使得C字串只能儲存文字資料,而不能儲存像圖片、音訊、視訊、壓縮檔案這樣的二進位制資料。如果有一種使用空字元來分割多個單詞的特殊資料格式,就不能用C字串來表示,如"Redis\0String",C字串的函式會把'\0'當做結束符來處理,而忽略到後面的"String"。而SDS的buf位元組陣列不是在儲存字元,而是一系列二進位制陣列,SDS API都會以二進位制的方式來處理buf數組裡的資料,使用len屬性的值而不是空字元來判斷字串是否結束。

2.4 時間複雜度

    我們來看幾個Redis常見操作的時間複雜度。

  1. 獲取SDS長度: 由於SDS中提供了len屬性,因此我們可以直接獲取時間複雜度為O(1),C字串為O(n)。
  2. 獲取SDS未使用空間長度: 時間複雜度為0(1),原因同1。
  3. 清除SDS儲存的內容:由於惰性空間分配策略,複雜度為O(1)。
  4. 建立一個長度為N的字串:時間複雜度為O(n)。
  5. 拼接一個長度為N的C字串:時間複雜度為O(n)。
  6. 拼接一個長度為N的SDS字串:時間複雜度為O(n)。

Redis在獲取字串長度上的時間複雜度為常數級O(1)。

2.5 為什麼要使用SDS

    通過以上分析,我們可以得到,SDS這種資料結構相對於C字串有以下優點:

  • 杜絕緩衝區溢位
  • 減少字串操作中的記憶體重分配次數
  • 二進位制安全
  • 由於SDS遵循以空字元結尾的慣例,因此相容部門C字串函式

Redis定位於一個高效能的記憶體資料庫,其面向的就是大資料量,大併發,頻繁讀寫,高響應速度的業務。因此在保證安全穩定的情況下,效能的提升非常重要。而SDS這種資料結構遮蔽了C字串的一些缺點,可以提供安全高效能的字串操作。

三、小結

    Redis在網際網路專案中的應用越來越廣泛,會用只是學習Redis中最簡單的一步,要想真正的成為Redis高手,瞭解其底層的實現必不可少。本篇文章簡單介紹了Redis中SDS資料結構及其特性,分析了Redis SDS的空間分配策略和其與C字串相比的優勢,後續的文章將繼續分享Redis底層實現的其它資料結構。未完待續......

四、參考

《Redis設計與實現》

《Redis開發與運維》

《Redis官方文件》