1. 程式人生 > 資料庫 >Redis設計與實現學習記錄《一》

Redis設計與實現學習記錄《一》

文章目錄

資料結構章節

  • 資料庫鍵總是一個字串物件
  • 資料庫鍵的值可以是:
    • 字串物件
    • 列表物件 list object
    • 雜湊物件 hash object
    • 集合物件 set object
    • 有序集合物件 sorted set object

SDS 資料結構

  • redis 使用的一種名為簡單動態字串(simple dynamic string , SDS)的抽象型別,

    用作redis的預設字串標識。

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

*在這裡插入圖片描述

如果執行sdscat(s, " Cluster");

目前s的空間不足以拼接,遂sdscat 會先擴充套件s的空間,然後再執行拼接操作。

拼接完成後的SDS如下圖:

在這裡插入圖片描述

sdscat 不僅對這個SDS 進行了拼接的操作,還為SDS分配了13位元組的未使用空間,並且拼接之後的字串也正好是13位元組長, 這不是BUG也不是巧合,她和SDS的空間分配策略有關

  • 普通的C字串的缺陷:

    因為C字串並不記錄自身的長度,所以對於一個包含了N個字元的C字串來說,底層實現總是一個N+1個字元長的陣列。

    因此,每次增長或縮短一個C字串,程式都總要對儲存這個C字串的陣列進行一次記憶體分重分配的操作:

    • 如果程式執行的是增長字串的操作,比如拼接操作,那麼程式需要在執行操作之前先通過記憶體重分配來 擴充套件底層陣列的空間大小
      ——如果忘記這一步, 就會產生 緩衝區溢位
    • 如果程式執行的是縮短字串的操作,比如截斷操作,那麼程式需要再執行操作之前先通過記憶體重分配來 釋放字串不再使用的那部分空間——如果忘記這一步,就會產生 記憶體洩漏

*因為記憶體重分配涉及複雜的演算法(具體是什麼呢?待了解)***並且可能需要執行系統呼叫,所以它通常是一個比較耗時的操作。

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

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

1、空間預分配

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

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

    • 如果對SDS進行修改後,SDS的長度(len屬性的值)將小於1MB,那麼程式分配和len屬性同樣大小的未使用空間,即free 和 len 值相同,eg:如果進行修改後SDS的len將變成13位元組,那麼free 也將是13 ,那SDS的buf陣列的實際長度將變為:

      13+13+1=27位元組(不要忘記最後一個位元組的空字元

    • 如果對SDS進行修改之後,SDS的長度將大於等於1MB,那麼程式將會分配1MB的未使用空間

    • 總結:free 的大小在SDS進行空間擴充套件時,將會變化成不超過1MB的數值

  • 案例:

在這裡插入圖片描述

執行sdscat(s, " Cluster");

在這裡插入圖片描述

執行sdscat(s, " Tutorial");

在這裡插入圖片描述

注意三張圖中,free,len,buf的內容變化!

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

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

2、惰性空間釋放

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

  • 案例:

    sdstrim 函式接收一個SDS和一個C 字串作為引數,移出SDS中所有在C 字串中出現過的字元。
    在這裡插入圖片描述

    執行sdstrim(s, "XY"); //移出SDS字串中所有的'X'和'Y'

在這裡插入圖片描述

觀察free的值變化,執行了sdstrim之後SDS並沒有釋放多出來的8位元組口徑建,而是將這8位元組空間作為未使用空間保留在了SDS中,之後如果要對SDS進行增長操作的話,就能用上了。

接著執行sdscat(s, " Redis");

在這裡插入圖片描述

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

當然,SDS也提供了響應的API(學習原始碼時留意一下),讓我們可以真正的釋放SDS的未使用空間, 所以不用擔心策略造成的記憶體浪費

3、二進位制安全

注意到SDS 資料結構中的buf陣列嗎?註釋寫的是位元組陣列而非字元陣列

因為len屬性的存在,SDS是使用len的值而不是使用空字元來判斷字串是否結束,

所以buf數組裡可以存放二進位制資料,而不用擔心像C字串那樣,讀取遇到空字元就結束了。

並且,SDS的API都是二進位制安全的,所有的SDS API都會以處理二進位制的方式來處理buf數組裡的資料

4、相容部分C字串函式

SDS的API同樣遵循C字串以空字元結尾的慣例:這些API總會將SDS儲存的資料的末尾設定為空字元,並且總會在為buf陣列分配空間時多分配一個位元組來容納這個空字元,這是為了讓那些儲存了文字資料的SDS可以重用一部分<string.h> 庫定義的函式。