redis 內部數據結構 sds

redis 內部數據結構 sds

字符串是redis中最為常見的存儲數據存儲類型,其底層實現是簡單的動態字符串sds(simple dynamic string),可以修改的字符串。

sds 介紹

sds本質上是 char *,因為有了表頭sdshdr結構的存在,所以sds比傳統c字符串在某些方面更加優秀,並且能夠兼容傳統C字符串。

sds採用預分配存儲空間的方式來減少內存的頻繁分配,惰性空間釋放的策略來優化sds的縮短操作,降低內存重新分配的概率。

redis 的字符串實現在sds.h sds.c 中。

<code>

typedef

char

*sds;

struct

__

attribute__

((__

packed__

))

sdshdr5

{

unsigned

char

flags;

char

buf[]; }; /<code>

上述代碼來自sds.h

  • __attribute__ ((__packed__))redis3.2 之後,針對不同長度的字符串引入了不同的sds數據結構,並且強制內存對齊1,將內存對齊的交給統一的內存分配函數,從而達到節省內存的目的稍微瞭解c/c++的人都會了解在結構體建立的是時候,會進行字節對齊操作,所以往往比實際變量佔用的字節要多一些,如果我們不想要字節對齊怎麼辦?在結構體聲明當中__attribute__ ((__packed__))關鍵字可以讓我們的結構體按照緊湊排列的方式,佔用內存。如下2種數據結構分別sizeof 將得到不同的結果struct test{ unsigned char flags; int value; }; struct __attribute__ ((__packed__)) test_{ unsigned char flags; int value; }; sizeof(struct test) //size is 8sizeof(struct test_) //size is 5
  • char buf[]char buf[] 等價與 char buf[0] 在標準C和C++中0長數組如char buf[0]是不允許使用的,因為這從語義邏輯上看,是完全沒有意義的。但是,GUN中卻允許使用,而且,很多時候,應用在了變長結構體中。對編譯器來說,此時長度為0的數組並不佔用空間,因為數組名本身不佔空間,它只是一個偏移量, 數組名這個符號本身代表了一個不可修改的地址常量。我們可以優雅的將buf稱之為柔性數組。在結構中,buf是一個數組名;但該數組沒有元素;該數組的真實地址緊隨結構體sdshdr5之後,而這個地址就是結構體後面數據的地址(如果給這個結構體分配的內容大於這個結構體實際大小,後面多餘的部分就是這個buf的內容);這種聲明方法可以巧妙的實現C語言裡的數組擴展。對於0長數組的這個特點,很容易構造出變長結構體,如緩衝區,數據包等等struct Buffer { int len; char cData[0]; }; 假如我們要發送1024個字節,我們如何構造這個數據包呢?char *buffer = (char*)malloc(sizeof(Buffer)+1024)我們首先申請1024字節的空間,其次做一個類型轉換如下代碼Buffer *p = (Buffer*)buffer p->len = 1024 memcpy(p.cData,"1024 data............",1024) send(socket,p,sizeof(Buffer)+1024);//發送數據

sds 數據存儲結構

我們摘取其中一個sdshdr32的數據結構來分析redis中sdsh的數據存儲結構

<code>

struct

__

attribute__

((__

packed__

))

sdshdr32

{

uint32_t

len;

uint32_t

alloc;

unsigned

char

flags;

char

buf[]; }; /<code>

sdsnew(const char *init) 會根據init數據的長度去分配內存,分配內存的大小為s_malloc(hdrlen+initlen+1) 其中 hdrlen 為sdshdr* 的結構體的大小,initlen為傳入的init 變量的數據大小或者為sdsnewlen(const void *init, size_t initlen) 傳入的initlen 的大小。

sdsnewlen 方法會根據initlen 的數值去通過sdsReqType去確定type的類型,然後根據返回的type數值再通過sdsHdrSize(type)獲得hdrlen。 具體代碼實現可參見sds.c文件。

當sds s = sdsnew()之後,其中s的位置並不是內存的起始位置, sh = s_malloc(hdrlen+initlen+1), 而是偏移了sh + hdrlen 後的位置 s = (char*)sh+hdrlen。

sds 有一個關鍵的宏SDS_HDR 定義如下

<code>#define SDS_HDR(T,s) ((

struct

sdshdr

##T *)((s)-(sizeof(

struct

sdshdr

##T)))) /<code>

其中SDS_HDR 能夠將s 的指針位置減去 sdshdr##T的大小,從而將指針位置指向sdsnew內存分配的起始位置dh,進而去通過sh去操作sdshdr##T的成員變量。其中T取值為(5,8,16,32,64)

  • 一個 sds 的內部數據結構
<code>     
    | 

len

| alloc | flags | buf | /<code>

如上,其中buf位置真正存儲了字符數據, 前面十三個位置分別存儲了buf相關的sds信息。

  • len記錄當前字節數組的長度(不包括\0),使得獲取字符串長度的時間複雜度由O(N)變為了O(1)
  • alloc記錄了當前字節數組總共分配的內存大小(不包括\0)
  • flags記錄了當前字節數組的屬性、用來標識到底是sdshdr8還是sdshdr16等
  • buf保存了字符串真正的值以及末尾的一個\0

整個sds的內存是連續的,統一開闢的。在大多數操作中,buf內的字符串實體才是操作對象。統一開闢內存能通過buf頭指針進行尋址,拿到整個struct的指針,而且通過buf的頭指針減1直接就能獲取flags的值, flags = s[-1]。

更詳細的sds的分配可參見sds.c中sdsnewlen的實現部分。


分享到:


相關文章: