1. 程式人生 > >Redis 設計與實現 3:字串 SDS

Redis 設計與實現 3:字串 SDS

> 本文的分析沒有特殊說明都是基於 Redis 6.0 版本原始碼 > redis 6.0 原始碼:https://github.com/redis/redis/tree/6.0 在 Redis 中,字串都用自定義的結構**簡單動態字串(Simple Dynamic Strings,SDS)**。 Redis 中使用到的字串都是用 SDS,例如 key、string 型別的值、sorted set 的 member、hash 的 field 等等等等。。。 ## 資料結構 ### 舊版本的結構 在 `3.2` 版本之前,sds 的定義是這樣的: ```c struct sdshdr { // buf 陣列中已使用的位元組數量,也就是 sds 本身的字串長度 unsigned int len; // buf 陣列中未使用的位元組數量 unsigned int free; // 位元組陣列,用於儲存字串 char buf[]; }; ``` ![舊版本 SDS 結構示例](https://img2020.cnblogs.com/blog/756647/202012/756647-20201225171335110-1086638312.png) 這樣的結構有幾個**好處**: - 單獨記錄長度`len`,獲取字串長度的時間複雜度是 $O(1)$ 。傳統的 C 字串獲取長度需要遍歷字串,直到遇到`\0`,時間複雜度是 $O(N)$。 - buf 陣列末尾遵循 C 字串以 `\0` 結尾的慣例,可以相容 C 處理字串的函式。 - 減少修改字串帶來的記憶體重分配次數,Redis 使用了 **空間預分配**(預先申請大一點點的空間) 和 **空間惰性釋放**(字串變短修改`len`欄位即可)來減少字串修改引起的記憶體重新分配。 - 不以`\0`為結尾的判斷,二進位制安全。因為圖片等二進位制資料中,可能包含`\0`,傳統 C 字串一遇到 `\0` 就認為字串結束了,會導致不能完整儲存。 缺點: - `len` 和 `free` 的定義用了 4 個位元組,可以表示 `2^32` 的長度。但是我們實際使用的字串,往往沒有那麼長。4 個位元組造成了浪費。 ### 新版本的結構 舊版本中我們說到,`len` 和 `free` 的缺點是用了太長的變數,新版本解決了這個問題。 我們來看一下新版本的 `SDS` 結構。 在 Redis 3.2 版本之後,Redis 將 SDS 劃分為 5 種類型: | 型別 | 位元組 | 位 | |--|--| -- | | sdshdr5 | < 1 | <8 | | sdshdr8 | 1 | 8 | | sdshdr16 | 2 | 16 | | sdshdr32 | 4 | 32 | | sdshdr64 | 8 | 64 | 新版本新增加了一個 `flags` 欄位來標識型別,長度 1 位元組(8 位)。 型別只佔用了前 3 位。在 `sdshdr5 ` 中,後 5 位用來儲存字串的長度。其他型別後 5 位沒有用。 ```c struct __attribute__ ((__packed__)) sdshdr5 { unsigned char flags; /* 前 3 位儲存型別,後 5 位儲存字串長度 */ char buf[]; }; struct __attribute__ ((__packed__)) sdshdr8 { uint8_t len; /* 字串長度,1 位元組 8 位 */ uint8_t alloc; /* 申請的總長度,1 位元組 8 位 */ unsigned char flags; /* 前 3 位儲存型別,後 5 位未使用 */ char buf[]; }; struct __attribute__ ((__packed__)) sdshdr16 { uint16_t len; /* 字串長度,2 位元組 16 位 */ uint16_t alloc; /* 申請的總長度,2 位元組 16 位 */ unsigned char flags; /* 前 3 位儲存型別,後 5 位未使用 */ char buf[]; }; struct __attribute__ ((__packed__)) sdshdr32 { uint32_t len; /* 字串長度,4 位元組 32 位 */ uint32_t alloc; /* 申請的總長度,4 位元組 32 位 */ unsigned char flags; /* 前 3 位儲存型別,後 5 位未使用 */ char buf[]; }; struct __attribute__ ((__packed__)) sdshdr64 { uint64_t len; /* 字串長度,8 位元組 64 位 */ uint64_t alloc; /* 申請的總長度,8 位元組 64 位 */ unsigned char flags; /* 前 3 位儲存型別,後 5 位未使用 */ char buf[]; }; ``` 優點: - 舊版本相對於傳統 C 字串的優點,新版本都有 - 相對於舊版本,新版本可以通過字串的長度,選擇不同的結構,可以節約記憶體 - 使用 `__attribute__ ((__packed__))` ,讓編譯器取消結構在編譯過程中的優化對齊,按照實際佔用位元組數進行對齊,可以節約記憶體 ## SDS 的初始化 sds 的定義,跟傳統的C語言字串保持型別相容 `char *`。但是 sds 是二進位制安全的,中間可能包含`\0`。 **sds.h** ```c typedef char *sds; ``` **sds.c** ```c // 初始化 sds sds sdsnewlen(const void *init, size_t initlen) { // 指向 sdshdr 開始地方的指標 void *sh; // sds 實際是一個指標,指向 buf 開始的位置 sds s; // 根據初始化的長度,返回 sds 的型別 char type = sdsReqType(initlen); // initlen == 0,是空字串,空字串往往就是用來往後新增位元組的,使用 SDS_TYPE_8 比 SDS_TYPE_5 更好 if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8; // 根據型別獲取 struct sdshdr 的長度 int hdrlen = sdsHdrSize(type); // flags 欄位的指標 unsigned char *fp; // 開始分配空間,+1 是為了最後一個的結束符號 \0 sh = s_malloc(hdrlen+initlen+1); if (sh == NULL) return NULL; // const char *SDS_NOINIT = "SDS_NOINIT"; if (init==SDS_NOINIT) init = NULL; else if (!init) // 不是 init 則清空 sh 的記憶體 memset(sh, 0, hdrlen+initlen+1); // s 指向了 buf 開始的地址 // 從上面結構可以看出,記憶體地址的順序: len, alloc, flag, buf // 因為 buf 本身不佔用空間,hdrlen 實際上就是結構的頭(len、alloc、flags) s = (char*)sh+hdrlen; // flags 佔用 1 個位元組,所以 s 退一位就是 flags 的開始位置了 fp = ((unsigned char*)s)-1; switch(type) { case SDS_TYPE_5: { // #define SDS_TYPE_BITS 3 // 前 3 位儲存型別,後 5 位儲存長度 *fp = type | (initlen << SDS_TYPE_BITS); break; } case SDS_TYPE_8: { // define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T))); // sh 變數賦值了 struct sdshdr SDS_HDR_VAR(8,s); sh->
len = initlen; sh->alloc = initlen; *fp = type; break; } // 下面是對 SDS_TYPE_16、SDS_TYPE_32、SDS_TYPE_64 的初始化,跟 SDS_TYPE_8 的類似,篇幅有限,省略... } // 如果 init 非空,則把 init 字串賦值給 s,實際上也是 buf 的初始化 if (initlen && init) memcpy(s, init, initlen); // 最後加一個結束標誌 \0 s[initlen] = '\0'; return s; } ``` ## SDS 的擴/縮容 ### 擴容 擴容就不跟初始化一樣寫註釋寫得那麼詳細了,直接拉最重要的幾句程式碼就行。 ```c sds sdsMakeRoomFor(sds s, size_t addlen) { // #define SDS_MAX_PREALLOC (1024*1024) // 當新的長度小於 1M 的時候,長度會增長一倍 // 當新的長度達到 1M 之後,最多就增長 1M 了 if (newlen < SDS_MAX_PREALLOC) newlen *= 2; else newlen += SDS_MAX_PREALLOC; // ... } ``` ### 縮容 sds 縮短不會真正縮小 buf,而是隻改長度而已,型別也不變。 **sds.c** ```c // 刪掉字串的左右字元中指定的字元 sds sdstrim(sds s, const char *cset) { char *start, *end, *sp, *ep; size_t len; sp = start = s; ep = end = s+sdslen(s)-1; while(sp <= end && strchr(cset, *sp)) sp++; while(ep > sp && strchr(cset, *ep)) ep--; len = (sp > ep) ? 0 : ((ep-sp)+1); if (s != sp) memmove(s, sp, len); // 結尾符 s[len] = '\0'; // 縮短長度 sdssetlen(s,len); return s; } ``` **sds.h** ```c static inline void sdssetlen(sds s, size_t newlen) { // 設定sds長度,只是修改 sdshdr 結構中的長度欄位,型別不會變 unsigned char flags = s[-1]; switch(flags&SDS_TYPE_MASK) { case SDS_TYPE_5: { unsigned char *fp = ((unsigned char*)s)-1; *fp = (unsigned char)(SDS_TYPE_5 | (newlen << SDS_TYPE_BITS)); } break; case SDS_TYPE_8: SDS_HDR(8,s)->len = (uint8_t)newlen; break; case SDS_TYPE_16: SDS_HDR(16,s)->len = (uint16_t)newlen; break; case SDS_TYPE_32: SDS_HDR(32,s)->len = (uint32_t)newlen; break; case SDS_TYPE_64: SDS_HDR(64,s)->len = (uint64_t)newlen; break; } } ```