Redis原始碼學習——基礎資料結構之SDS
Redis資料結構-SDS
Redis是一個開源(BSD許可),記憶體儲存的資料結構伺服器,可用作資料庫,快取記憶體和訊息佇列代理。
首先介紹下Redis的基礎資料結構 —— SDS
Redis沒有使用傳統C語言的字串(字元陣列)表示。而是自己構建了一種名為sds(Simple Dymamic String)的抽象型別,作為redis的預設字元型別。 SDS用於儲存資料庫中的字串值,使用者客戶端的輸入的緩衝區,AOF模組中的緩衝區都是由SDS實現的。
SDS相比於C字串的優點:
- 常數複雜度獲取字串長度
- 緩衝區溢位
- 減少改變字串長度時帶來的記憶體重新分配
- 二進位制安全
同時,sds也支援c字串的部分操作函式。
SDS的資料結構:
以下是SDS的資料結構
/*
* 儲存字串物件的結構
*/
struct sdshdr {
// buf 中已佔用空間的長度
int len;
// buf 中剩餘可用空間的長度
int free;
// 資料空間
char buf[];
};
// ps: 在redis 3.0中 為了更加節省記憶體,可用的sdshdr分成4種,len和free屬性分別可以是uint8_t,uint16_t,uint32_t,uint64_t 這四種型別,會隨著sds所儲存的字串長度不同,而分配為不同的sdshdr。
獲取長度;
Redis中獲取字元長度的操作是STRLEN key
C語言中的字串並不記錄自身的長度資訊。 如果我們想要獲取一個c字串的長度,我們要遍歷整個字串,直到遇到代表結尾符的'/n'為止。 毫無疑問,其複雜度是O(N)。
而在sds中,我們記錄了每個sds物件中所存字串的長度。 sds提供了一系列操作sds的函式,若出現改變陣列長度的草走,都會同步更新len欄位,保證len欄位的實時性。 這樣每個STRLEN的複雜度就變成了O(1).
快取區溢位:
C語言中提供了strcat方法,可以將strSrc字串拼到strDest字串尾部。 然而每次執行strcat操作時,都假設了我們已經strDest指標分配了足夠多的記憶體。 然而一旦當分配的記憶體不足, 機會出現快取區溢位。如下:
#include <stdio.h>
#include <string.h>
int main(void)
{
char dest[20] = "Hey ";
for(int i = 0; i < 20; i++) {
strcat(dest, ", Man");
printf("%d time, lenght is: %ld \n", i, strlen(dest));
}
return 0;
}
執行結果:
0 time, lenght is: 9
1 time, lenght is: 14
2 time, lenght is: 19
[1] 29751 abort ./str
可以看到,當執行到第三次的時候,並不會由於dest已經快到其最大容量。 所以第四次strcat執行時,會出現溢位,中斷程式。
而在sds中,執行sdscat操作,會判斷為目標sds物件所分配的記憶體是否可以容納拼接後的字元記憶體。 程式碼如下:
sds sdscat(sds s, const char *t) {
return sdscatlen(s, t, strlen(t));
}
sds sdscatlen(sds s, const void *t, size_t len) {
struct sdshdr *sh;
// 獲取原字元長度
size_t curlen = sdslen(s);
// 擴充套件空間。 若原指標分配記憶體不足,則重新分配記憶體。 返回新的地址
// T = O(N)
s = sdsMakeRoomFor(s,len);
// 若記憶體不足直接返回
if (s == NULL) return NULL;
// 獲取sds控制程式碼sdshdr 的指標位置
sh = (void*) (s-(sizeof(struct sdshdr)));
// 複製將t中字串複製到目標字串後面
// T = O(N)
memcpy(s+curlen, t, len);
// 更新屬性
sdssetlen(s, curlen+len);
// 新增新結尾符號
s[curlen+len] = '\0';
// 返回新 sds
return s;
}
可以看到, 每次執行字串拼接操作,都會判斷所分配的記憶體是否足夠,如果不足,會重新分配記憶體。 其他操作,例如sdsrange(僅保留部分字串),sdstrim(裁剪特定字元)都會有以上類似邏輯,保證每次操作都會即時釋放多餘記憶體,且不會出現記憶體不足。
減少改變字串長度時帶來的記憶體重新分配
然而通過C字串執行會改變字串長度的操作, 也可以通過判斷字串長度實現預分配記憶體。每次記憶體重新分配都要將原記憶體中的字元複製到新記憶體中, 複雜度是O(N),然而當我們頻繁地對一個字串進行改變長度的操作,會導致每次操作都引起一次O(N)的操作。
在sds中, 每次重新分配記憶體都會預留一部分作為buffer,我們可以從上文的程式碼中看到,重新分配記憶體是通過sdsMakeRoomFor函式呼叫。 那麼我們看下sdsMakeRoomFor中,分配記憶體的策略:
sds sdsMakeRoomFor(sds s, size_t addlen) {
struct sdshdr *sh, *newsh;
// 獲取s目前剩餘的空間長度
size_t free = sdsavail(s);
size_t len, newlen;
// s 目前的空餘空間已經足夠,無須再進行擴充套件,直接返回
if (free >= addlen) return s;
// 獲取 s 目前已佔用空間的長度
len = sdslen(s);
// 獲取控制程式碼指標位置
sh = (void*) (s-(sizeof(struct sdshdr)));
// 擴充套件後s至少需要的長度
newlen = (len+addlen);
// 根據新長度,為 s 分配新空間所需的大小
if (newlen < SDS_MAX_PREALLOC)
// 如果新長度小於 SDS_MAX_PREALLOC
// 那麼為它分配兩倍於所需長度的空間
// 對新建的sds或者重新分配記憶體的sds,都會採用此策略,保留1倍的長度
newlen *= 2;
else
// 否則,所保留的buffer長度為 SDS_MAX_PREALLOC
newlen += SDS_MAX_PREALLOC;
// T = O(N)
// 分配記憶體,獲取新的指標地址
newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);
// 若記憶體不足,分配失敗,返回
if (newsh == NULL) return NULL;
// 設定剩餘空間長度。
newsh->free = newlen - len;
// 返回 sds
return newsh->buf;
}
redis中為了減少記憶體的重新分配, 使用了預留buffer的方法。 將原來n次字串操作一定有n次O(N)複雜度的記憶體分配, 調整為最多有n次O(N)複雜度的記憶體分配。
tips: 當執行sdstrim這樣減少字串長度的操作時, 即時裁剪後多餘的記憶體大於len的一半,sds也不會立即將多餘的空間釋放,而是保留下來未將來的增長操作做了優化。 且提供了sdsRemoveFreeSpace這個APi,用於我們可以手動將這樣多餘的記憶體釋放。
二進位制安全
二進位制安全是一個主要用來處理字串操作的程式設計術語。二進位制安全功能本質上是把輸入當作一個沒有任何特殊的原生流,其在操作上應包含一個字元所能有的256種可能的值(假設為8為字元)。
由於C字串需要通過'0'判斷字串的結尾。 所以當我們儲存字串時,需要將'0'這樣的特殊字元過濾掉。 在sds中,由於我們維護了字串的長度,所以並沒有這樣的顧慮。 sds符合二進位制安全。
支援c字串的部分操作函式
由於sds的api提供的入參都是sds格式,指向的都是其buf屬性的指標位置。 我們以一個建立sds的api為例:
sds sdsnewlen(const void *init, size_t initlen) {
struct sdshdr *sh;
// do somethings to create sds...
sh->len = initlen;
sh->free = 0;
sh->buf[initlen] = '\0';
// 返回 buf 部分,而不是整個 sdshdr
return (char*)sh->buf;
}
我們可以看到,sds物件,我們獲取的並不是整個sdshdr指標的位置, 而是其buf屬性的指標,也就是存字串的指標位置,且sds遵守了以'0'作為一個字串結尾的條件(但並不是通過'0'的位置判斷字串的長度)。 這樣就保證了我們對一部分C字串介面傳入sds物件的指標,是可以當作char[]用的。
相關文章
- Redis 資料結構 之 SDSRedis資料結構
- Redis基礎資料結構之字串Redis資料結構字串
- Redis基礎資料結構之MapRedis資料結構
- Redis基礎資料結構之SkipListRedis資料結構
- 【Redis 系列】redis 學習十五,redis sds資料結構和底層設計原理Redis資料結構
- redis原始碼分析(二)、sds動態字串學習總結Redis原始碼字串
- 資料結構基礎學習之緒論資料結構
- Redis基礎資料結構之連結串列Redis資料結構
- redis原始碼分析(二)、redis原始碼分析之sds字串Redis原始碼字串
- Redis基礎資料結構Redis資料結構
- 資料結構基礎學習之線性表資料結構
- Redis基礎知識(學習筆記11--SDS)Redis筆記
- Redis sds資料結構實現分析ZFRedis資料結構
- 資料結構基礎學習之(棧和佇列)資料結構佇列
- 資料結構基礎學習之(串與陣列)資料結構陣列
- Redis基礎知識(學習筆記1--五種基礎資料結構)Redis筆記資料結構
- Redis基礎——剖析基礎資料結構及其用法Redis資料結構
- 淺析Redis基礎資料結構Redis資料結構
- 資料結構學習系列之從原始碼來看HashMap資料結構原始碼HashMap
- Redis原始碼之SDS簡單動態字串Redis原始碼字串
- Redis原始碼閱讀:Redis字串SDSRedis原始碼字串
- 資料結構基礎學習之時間複雜度分析資料結構時間複雜度
- Redis學習筆記——SDSRedis筆記
- Redis 的基礎資料結構(三)物件Redis資料結構物件
- redis原始碼分析(八)、redis資料結構之壓縮ziplist--------ziplist.c ziplist.h學習筆記Redis原始碼資料結構筆記
- 資料結構學習之樹結構資料結構
- Redis資料結構概覽(原始碼分析)Redis資料結構原始碼
- Redis基礎(一)資料結構與資料型別Redis資料結構資料型別
- zk原始碼閱讀9:資料模型ACL之基礎資料結構以及驗證原始碼模型資料結構
- 基礎資料結構之陣列資料結構陣列
- 基礎資料結構之遞迴資料結構遞迴
- 深入理解Redis 資料結構—簡單動態字串sdsRedis資料結構字串
- Redis 學習-資料結構基本簡介Redis資料結構
- redis原始碼學習之slowlogRedis原始碼
- 通俗易懂的Redis資料結構基礎教程Redis資料結構
- Redis學習筆記(二)redis 底層資料結構Redis筆記資料結構
- 探索Redis設計與實現3:Redis內部資料結構詳解——sdsRedis資料結構
- Redis 基礎學習Redis