文章導航-readme
圖解Redis之數據結構篇——簡單動態字符串SDS
前言
相信用過Redis的人都知道,Redis提供了一個邏輯上的對象系統構建了一個鍵值對數據庫以供客戶端用戶使用。這個對象系統包括字符串對象,哈希對象,列表對象,集合對象,有序集合對象等。但是Redis面向內存並沒有直接使用這些對象。而是使用了簡單動態字符串,鏈表,字典(散列表),跳躍表,整數集合,壓縮列表這些數據結構來操作內存。
系列文章
一、簡單動態字符串(SDS)
Redis默認並未直接使用C字符串(C字符串僅僅作為字符串字面量,用在一些無需對字符串進行修改的地方,如打印日志)。而是以Struct的形式構造了一個SDS的抽象類型。當Redis需要一個可以被修改的字符串時,就會使用SDS來表示。在Redis數據庫里,包含字符串值的鍵值對都是由SDS實現的(Redis中所有的鍵都是由字符串對象實現的即底層是由SDS實現,Redis中所有的值對象中包含的字符串對象底層也是由SDS實現)。
1.1 SDS
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字符串的區別,關於為什么Redis要使用SDS而不是C字符串,我們可以從以下幾個方面來分析。
2.1 緩沖區溢出
C字符串,如果程序員在字符串修改的時候如果忘記給字符串重新分配足夠的空間,那么就會發生內存溢出,如上圖所示,忘記給s1分配足夠的內存空間, s1的數據就會溢出到s2的空間, 導致s2的內容被修改.。而Redis提供的SDS其內置的空間分配策略則可以完全杜絕這種事情的發生。當API需要對SDS進行修改時, API會首先會檢查SDS的空間是否滿足條件, 如果不滿足, API會自動對它動態擴展, 然后再進行修改。
2.2 內存重分配
2.2.1 C字符串內存重分配
在C字符串中,如果對字符串進行修改,那么我們就不得不面臨內存重分配。因為C字符串是由一個N+1長度的數組組成,如果字符串的長度變長,我們就必須對數組進行擴容,否則會產生內存溢出。而如果字符串長度變短,我們就必須釋放掉不再使用的空間,否則會發生內存泄漏。
2.2.2 SDS空間分配策略
對於Redis這種具有高性能要求的內存數據庫,如果每次修改字符串都要進行內存重分配,無疑是巨大的性能損失。而Redis的SDS提供了兩種空間分配策略來解決這個問題。
-
空間預分配
我們知道在數組進行擴容的時候,往往會申請一個更大的數組,然后把數組復制過去。為了提升性能,我們在分配空間的時候並不是分配一個剛剛好的空間,而是分配一個更大的空間。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
-
惰性空間釋放
惰性空間釋放用於字符串縮短的操作。當字符串縮短是,程序並不是立即使用內存重分配來回收縮短出來的字節,而是使用free屬性記錄起來,並等待將來使用。
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常見操作的時間復雜度。
- 獲取SDS長度: 由於SDS中提供了len屬性,因此我們可以直接獲取時間復雜度為O(1),C字符串為O(n)。
- 獲取SDS未使用空間長度: 時間復雜度為0(1),原因同1。
- 清除SDS保存的內容:由於惰性空間分配策略,復雜度為O(1)。
- 創建一個長度為N的字符串:時間復雜度為O(n)。
- 拼接一個長度為N的C字符串:時間復雜度為O(n)。
- 拼接一個長度為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官方文檔》