redis 原理系列之–字符串存儲的實現原理(1)
- 2019 年 10 月 3 日
- 筆記
背景
redis功能強大,幾乎已經成了現代大中型服務必備的緩存技術了。 除了十分給力的緩存功能,redis當做消息隊列,數據庫也有着不錯的表現。
我們都知道,redis 有五種數據類型,string,list, hash, set 和zset。 其中 最基本的,同時也是最常用的 就是string了。 本文就來談談 redis內部,string 的實現原理:SDS(simple dynamic string)。
redis簡單動態字符竄:SDS
- 在redis里,C語言的字符竄只用來放字符串字面量,即只有當無序對字符串修改的時候才用C的字符串,例如打印日誌的時候。
- 除了基本的字符串存儲之外,sds還用做緩衝區。AOF模塊的緩衝區,和客戶端狀態的輸入緩衝區,都是sds實現的。
SDS 定義
struct sdshdr { // buf 中已佔用空間的長度 int len; // buf 中剩餘可用空間的長度 int free; // 數據空間 char buf[]; };
圖示如下:
-
簡單解釋一下: buf是一個位元組數組,是用來放具體數據的。其長度是按一定策略伸縮的,具體解釋在下面。 len 表示buf 中已經使用掉的長度,free表示 buf中尚未使用的長度。
-
buf內 sds 的字符串,總是以空字符結尾,這一點同c字符串一致。 因此sds 可以直接重用一部分c字符串函數庫的函數。
SDS 與C字符串的對比 和優點
1,O(1) 獲取字符串長度
- 因為sds已經存了數據的長度,所以獲取字符串長度複雜度為O(1),而C字符串獲取長度為O(n)。
2,杜絕緩衝區溢出導致的內存問題
- 假設內存區域有s1:「hi」,s2: 「redis」 兩個字符串,位置緊鄰,如下圖:
- 此時需要給s1 追加一個「boy」, 如果是C字符串,忘記了在追加之前先給s1 分配空間,此時追加將導致 s2的值被意外的修改。 而使用 sds則不會有這個問題。 因為其封裝好的函數,會在追加數據之前先檢查 空間是否夠用,如果不夠用就擴容。
3,通過空間預分配和空間惰性釋放 減少內存分配問題
-
當給sds的值追加一個字符串,而當前的剩餘空間不夠時,就會觸發sds的擴容機制。擴容採用了空間預分配的優化策略,即分配空間的時候:如果sds 值大小< 1M ,則增加一倍; 反之如果>1M , 則當前空間加1M作為新的空間。
-
當sds的字符竄縮短了,sds的buf內會多出來一些空間,這個空間並不會馬上被回收,而是暫時留着以防再用的時候進行多餘的內存分配。這個是惰性空間釋放的策略
4, 二進制安全
- c字符串必須符合某種編碼(例如ASCII),且不能包含空字符。 這些限制使得 c字符竄不能保存圖片,音頻等二進制文件。 而sds的api 都是二進制安全的,其所有api 都會以處理二進制的方式來處理buf內的數據,所以不會有任何的限制。
SDS 的API接口列表
函數 | 作用 | 複雜度 |
---|---|---|
sdsnew | 以一個c字符竄為參數新建sds | O(N) |
sdsempty | 新建空的sds字符串 | O(1) |
sdsfree | 釋放sds | O(N) |
sdslen | 獲取已使用長度 | O(1) |
sdsavail | 獲取未使用長度 | O(1) |
sdsdup | 創建一個sds的副本 | O(N) |
sdsclear | 清空 | O(1) |
sdscat | 追加C字符串到sds | O(N) |
sdscatsds | 追加sds字符串到sds | O(N) |
sdscpy | 用c字符串覆蓋sds值 | O(N) |
sdsgrowzero | 用空字符串擴展 sds至給定長度 | O(N) |
sdsrange | 刪除給定區間外的數據 | O(N) |
sdscmp | 對比sds是否相同 | O(N) |
sdstrim | 從sds中去除給定c字符串中出現過的字符 | O(N*M) |
總結
sds 其實就是字符串數組的一個封裝,但是由於考慮了多種場景,作者給它適配了多個高效、優雅的接口,使得 sds成為了一個存儲字符串的優秀設計。使得sds成為一個獨立的、可提供高效優質服務的基礎實體。
我們在設計一些偏底層的數據結構、對象、甚至是數據庫表的時候,可以參考sds的設計,從中尋找一些啟發。
參考: 《Redis 設計與實現》 黃健宏
有收穫就點個贊吧~