字符串是redis中最為常見的存儲數據存儲類型,其底層實現是簡單的動態字符串sds(simple dynamic string),可以修改的字符串。
sds 介紹
sds本質上是 char *,因為有了表頭sdshdr結構的存在,所以sds比傳統c字符串在某些方面更加優秀,并且能夠兼容傳統C字符串。
sds采用預分配存儲空間的方式來減少內存的頻繁分配,惰性空間釋放的策略來優化sds的縮短操作,降低內存重新分配的概率。
redis 的字符串實現在sds.h 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[];};
上述代碼來自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的數據存儲結構
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[];};
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 定義如下
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))
其中SDS_HDR 能夠將s 的指針位置減去 sdshdr##T的大小,從而將指針位置指向sdsnew內存分配的起始位置dh,進而去通過sh去操作sdshdr##T的成員變量。其中T取值為(5,8,16,32,64)
- 一個 sds 的內部數據結構
----------------------------- | len | alloc | flags | buf | -----------------------------
如上,其中buf位置真正存儲了字符數據, 前面十三個位置分別存儲了buf相關的sds信息。
- len記錄當前字節數組的長度(不包括),使得獲取字符串長度的時間復雜度由O(N)變為了O(1)
- alloc記錄了當前字節數組總共分配的內存大小(不包括)
- flags記錄了當前字節數組的屬性、用來標識到底是sdshdr8還是sdshdr16等
- buf保存了字符串真正的值以及末尾的一個
整個sds的內存是連續的,統一開辟的。在大多數操作中,buf內的字符串實體才是操作對象。統一開辟內存能通過buf頭指針進行尋址,拿到整個struct的指針,而且通過buf的頭指針減1直接就能獲取flags的值, flags = s[-1]。
更詳細的sds的分配可參見sds.c中sdsnewlen的實現部分。