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> 庫定義的函式。