redis 是 C 語言寫的,那麼我們思考一下 redis 是如何表示一個字串的?redis 的資料結構和 C 語言的資料結構是一樣的嗎?
我們可以看到 redis 原始碼中的 sds 庫函式,和 sds 的具體實現,分別有如下 2 個檔案:
- sds.h
- sds.c
具體路徑是:deps/hiredis/sds.h
, deps/hiredis/sds.c
sds.h 中涉及如下資料結構:
SDS
redis 中 SDS simple Dynamic string
簡單動態字串
C 語言中表示字串的方式是字元陣列,例如:
char data[]="xiaomotong"
如果 C 語言需要擴容的話需要重新分配一個再大一點的記憶體,存放新的字串,若每次都要重新分配字串,對於效率和效能必然會大大降低,並且若某一個字串是 “xiaomo\0tong”
這個時候,實際上 C 中 遇到 ‘\0’ 就結束了,因此實際 “xiaomo\0tong”
只會讀取到xiaomo
,字串長度就是 6
因此 redis 中的 sds 資料結構是這樣設計的,是通過一個成員來標誌字串的長度:
SDS:
free:0
len:6
char buf[]="xiaomo"
若這個時候,我們需要在字串後面追加字串, sds 就會進行擴容,例如在後面加上 “tong” , 那麼 sds 的資料結構中的值會變成如下:
free:10
len:10
char buf[]="xiaomotong"
最後的 "xiaomotong"
也是帶有 \0
的,這也保持了 C 語言的標準,redis 中對於 sds 資料結構擴容是成倍增加的,但是到了一定的級別,例如 1M 的時候,就不會翻倍的擴容,而是做加法 例如 1M 變成 2M , 2M 變成 3M 等等
SDS 的優勢:
- 二進位制安全的資料結構
- 記憶體預分配機制,避免了頻繁的記憶體分配
- 相容 C 語言的庫函式
redis 原始碼 sds 資料結構
現在我們看到的是 reids-6.2.5 sds 的資料結構,將以前的表示一個長度使用了 int 型別,是 32 位元組的,能表示的長度可以達到 42 億,其實遠遠沒有必要使用 int32 ,太浪費資源了
下面的資料結構,可以根據不同的需求,選取不同的資料結構進行使用
struct __attribute__ ((__packed__)) hisdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) hisdshdr8 {
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__)) hisdshdr16 {
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__)) hisdshdr32 {
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__)) hisdshdr64 {
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[];
};
- hisdshdr5
用於長度在 0 -- 2^5 - 1 範圍內
- hisdshdr8
用於長度在 2^5-- 2^8 - 1 範圍內
- hisdshdr16
用於長度在 2^8 -- 2^16 - 1 範圍內
- hisdshdr32
用於長度在 2^16 -- 2^32 - 1 範圍內
- hisdshdr64
用於長度在 2^32 -- 2^64 - 1 範圍內
上述的 unsigned char flags
佔用 1 個位元組,8個 bit 位:
- 其中 3 位 用於表示型別
- 其中 5 位 用於表示字串的長度
前面 3 個 bit 位,能表示的數字範圍是 0 - 7 ,對於應到如下巨集
#define HI_SDS_TYPE_5 0
#define HI_SDS_TYPE_8 1
#define HI_SDS_TYPE_16 2
#define HI_SDS_TYPE_32 3
#define HI_SDS_TYPE_64 4
#define HI_SDS_TYPE_MASK 7
原始碼實現是通過與操作來獲取到具體的資料結構型別的:
我們們以 hisdshdr8 資料結構為例子,unsigned char flags
是這樣的
- len
表示已經使用的長度
- alloc
預分配的空間大小
- flag
表示使用哪一種資料結構(前 3 個 bit)
- buf
實際儲存的字串
那麼,我們就能夠計算出來,該資料結構的空間剩餘 free = alloc - len
原始碼中 sds.h 下的函式 hisds hi_sdsnewlen(const void *init, size_t initlen)
使用 一個 init 指標和 initlen 長度,來建立一個字串
hisds hi_sdsnewlen(const void *init, size_t initlen) {
void *sh;
hisds s;
// 計算type,獲取需要使用的資料結構型別
char type = hi_sdsReqType(initlen);
// 現在預設使用 HI_SDS_TYPE_8 了
if (type == HI_SDS_TYPE_5 && initlen == 0) type = HI_SDS_TYPE_8;
int hdrlen = hi_sdsHdrSize(type);
unsigned char *fp; /* flags pointer. */
// 分配記憶體
sh = hi_s_malloc(hdrlen+initlen+1);
if (sh == NULL) return NULL;
if (!init)
memset(sh, 0, hdrlen+initlen+1);
s = (char*)sh+hdrlen;
fp = ((unsigned char*)s)-1;
// 根據不同的型別對資料結構初始化
switch(type) {
case HI_SDS_TYPE_5: {
*fp = type | (initlen << HI_SDS_TYPE_BITS);
break;
}
case HI_SDS_TYPE_8: {
HI_SDS_HDR_VAR(8,s);
sh->len = initlen;
sh->alloc = initlen;
*fp = type;
break;
}
case HI_SDS_TYPE_16: ...
case HI_SDS_TYPE_32: ...
case HI_SDS_TYPE_64: ...
}
if (initlen && init)
memcpy(s, init, initlen);
// 相容 C 庫,字串後面加上 \0
s[initlen] = '\0';
return s;
}
- hi_sdsReqType
根據字串的長度來計算所使用的資料型別
- hi_sdsHdrSize
根據不同的型別,獲取該型別需要分配的空間大小
- hi_s_malloc
開闢記憶體,呼叫的是 alloc.h
中的 hi_malloc
,具體實現就看不到了
- switch(type) …
根據不同的型別,來將對應的資料結構做初始化
- s[initlen] = '\0'
相容 C 庫,字串後面加上 ’\0’
redis k-v 底層設計原理
redis 是如何儲存海量資料的?
redis 中資料是以 key-value 的方式來儲存的,key 都是字串,而 value 根據不同的資料結構表現形式也不太一樣
他們的儲存方式是以 陣列 + 連結串列的方式儲存的:
- 陣列
陣列中存放的是連結串列的地址
- 連結串列
連結串列中儲存的是具體的資料
舉個例子:
上面有說到 redis 裡面的 key 都是字串的方式,那麼如何與陣列和連結串列進行結合呢?
具體邏輯是使用 hash 函式,將字串 key 按照演算法計算出一個索引值,這個值就是陣列的索引,該索引對應的陣列元素是指向一個連結串列的,連結串列中存放具體的資料
- dict[10] 作為陣列,每一個元素會指向一條連結串列
- 現在我們要插入 k1 - v1 , k2 - v2 , k3 - v3
通過 hash 函式進行計算:
hash(k1) % 10 = 0
hash(k2) % 10 = 1
此處對 10 取模的原因是,整個陣列就只能存放 10 個元素
那麼結果是這樣的
dict[0] -> (k1,v1) -> null
dict[1] -> (k2,v2) -> null
若這個時候我們們插入的 (k3,v3) 計算出來的索引與前面已有資料的衝突了咋辦?
hash(k3) % 10 = 1
這就會出現 hash 衝突了,當 hash 衝突的時候,若 k3 與 k2 是相等了,那麼就會直接更新 k2 對應的 value 值
若 k3 與 k2 不同,則會通過鏈地址法來解決 hash 衝突,會把 (k3,v3) 通過頭插法來插入到原有的連結串列中,如:
dict[0] -> (k1,v1) -> null
dict[1] -> (k3,v3) -> (k2,v2) -> null
小結:
- 對於上述的 hash ,相同的輸入,一定會有相同的輸出
- 不同的輸入,也有可能有相同的輸出,此時就 hash 衝突了,是需要解決的
參考資料:
- redis_doc
- reids 原始碼 reids-6.2.5 Redis 6.2.5 is the latest stable version.
歡迎點贊,關注,收藏
朋友們,你的支援和鼓勵,是我堅持分享,提高質量的動力
歡迎大家對文章中的原始碼細節進行討論和分享,不足之處還請多多指教,如果大佬們有更好的學習方法還請給予指導,謝謝
1、評論區超過 10 人互動(不含作者本人),作者可以以自己的名義抽獎送出掘金徽章 2 枚(掘金官方承擔)
好了,本次就到這裡
技術是開放的,我們的心態,更應是開放的。擁抱變化,向陽而生,努力向前行。
我是小魔童哪吒,歡迎點贊關注收藏,下次見~