Redis底層資料結構--SDS
阿新 • • 發佈:2018-11-20
這是一種用於儲存二進位制資料的一種結構, 具有動態擴容的特點. 其實現位於src/sds.h
與src/sds.c
中, 其關鍵定義如下:
typedef char *sds; /* Note: sdshdr5 is never used, we just access the flags byte directly. * However is here to document the layout of type 5 SDS strings. */ struct __attribute__ ((__packed__)) sdshdr5 { unsigned char flags; /* 3 lsb of type, and 5 msb of string length */ char buf[]; }; struct __attribute__ ((__packed__)) sdshdr8 { uint8_t len; /* used */ uint8_t alloc; /* excluding the header and null terminator */ unsigned char flags; /* 3 lsb of type, 5 unused bits */ char buf[]; }; struct __attribute__ ((__packed__)) sdshdr16 { uint16_t len; /* used */ uint16_t alloc; /* excluding the header and null terminator */ unsigned char flags; /* 3 lsb of type, 5 unused bits */ char buf[]; }; struct __attribute__ ((__packed__)) sdshdr32 { uint32_t len; /* used */ uint32_t alloc; /* excluding the header and null terminator */ unsigned char flags; /* 3 lsb of type, 5 unused bits */ char buf[]; }; struct __attribute__ ((__packed__)) sdshdr64 { uint64_t len; /* used */ uint64_t alloc; /* excluding the header and null terminator */ unsigned char flags; /* 3 lsb of type, 5 unused bits */ char buf[]; };
SDS的總體概覽如下圖:
其中sdshdr
是頭部, buf
是真實儲存使用者資料的地方. 另外注意, 從命名上能看出來, 這個資料結構除了能儲存二進位制資料, 顯然是用於設計作為字串使用的, 所以在buf
中, 使用者資料後總跟著一個\0
. 即圖中 "資料" + "\0" 是為所謂的buf
SDS有五種不同的頭部. 其中sdshdr5
實際並未使用到. 所以實際上有四種不同的頭部, 分別如下:
len
分別以uint8
,uint16
,uint32
,uint64
表示使用者資料的長度(不包括末尾的\0
)alloc
分別以uint8
uint16
,uint32
,uint64
表示整個SDS, 除過頭部與末尾的\0
, 剩餘的位元組數.flag
始終為一位元組, 以低三位標示著頭部的型別, 高5位未使用.
當在程式中持有一個SDS例項時, 直接持有的是資料區的頭指標, 這樣做的用意是: 通過這個指標, 向前偏一個位元組, 就能取到flag
, 通過判斷flag低三位的值, 能迅速判斷: 頭部的型別, 已用位元組數, 總位元組數, 剩餘位元組數. 這也是為什麼sds
型別即是char *
指標類型別名的原因.
建立一個SDS例項有三個介面, 分別是:
// 建立一個不含資料的sds: // 頭部 3位元組 sdshdr8 // 資料區 0位元組 // 末尾 \0 佔一位元組 sds sdsempty(void); // 帶資料建立一個sds: // 頭部 按initlen的值, 選擇最小的頭部型別 // 資料區 從入參指標init處開始, 拷貝initlen個位元組 // 末尾 \0 佔一位元組 sds sdsnewlen(const void *init, size_t initlen); // 帶資料建立一個sds: // 頭部 按strlen(init)的值, 選擇最小的頭部型別 // 資料區 入參指向的字串中的所有字元, 不包括末尾 \0 // 末尾 \0 佔一位元組 sds sdsnew(const char *init);
- 所有建立sds例項的介面, 都不會額外分配預留記憶體空間
sdsnewlen
用於帶二進位制資料建立sds例項,sdsnew
用於帶字串建立sds例項. 介面返回的sds可以直接傳入libc中的字串輸出函式中進行操作, 由於無論其中儲存的是使用者的二進位制資料, 還是字串, 其末尾都帶一個\0, 所以至少呼叫libc中的字串輸出函式是安全的.
在對SDS中的資料進行修改時, 若剩餘空間不足, 會呼叫sdsMakeRoomFor
函式用於擴容空間, 這是一個很低階的API, 通常情況下不應當由SDS的使用者直接呼叫. 其實現中核心的幾行如下:
sds sdsMakeRoomFor(sds s, size_t addlen) {
...
/* Return ASAP if there is enough space left. */
if (avail >= addlen) return s;
len = sdslen(s);
sh = (char*)s-sdsHdrSize(oldtype);
newlen = (len+addlen);
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
...
}
可以看到, 在擴充空間時
- 先保證至少有
addlen
可用 - 然後再進一步擴充, 在總體佔用空間不超過閾值
SDS_MAC_PREALLOC
時, 申請空間再翻一倍. 若總體空間已經超過了閾值, 則步進增長SDS_MAC_PREALLOC
. 這個閾值的預設值為1024 * 1024
SDS也提供了介面用於移除所有未使用的記憶體空間. sdsRemoveFreeSpace
, 該介面沒有間接的被任何SDS其它介面呼叫, 即預設情況下, SDS不會自動回收預留空間. 在SDS的使用者需要節省記憶體時, 由使用者自行呼叫:
sds sdsRemoveFreeSpace(sds s);
總結:
- SDS除了是某些Value Type的底層實現, 也被大量使用在Redis內部, 用於替代C-Style字串. 所以預設的建立SDS例項介面, 不分配額外的預留空間. 因為多數字符串在程式執行期間是不變的. 而對於變更資料區的API, 其內部則是呼叫了
sdsMakeRoomFor
, 每一次擴充空間, 都會預留大量的空間. 這樣做的考量是: 如果一個SDS例項中的資料被變更了, 那麼很有可能會在後續發生多次變更. - SDS的API內部不負責清除未使用的閒置記憶體空間, 因為內部API無法判斷這樣做的合適時機. 即便是在操作資料區的時候導致資料區佔用記憶體減少時, 內部API也不會清除閒置內在空間. 清除閒置記憶體空間責任應當由SDS的使用者自行擔當.
- 用SDS替代C-Style字串時, 由於其頭部額外儲存了資料區的長度資訊, 所以字串的求長操作時間複雜度為O(1)