《redis深度歷險》八(字串和字典)
字串
SDS(Simple Dynamic String)是一個帶長度資訊的位元組陣列
struct SDS<T> {
T capacity; // 陣列容量
T len; // 陣列長度
byte flags; // 特殊標識
byte[] content; // 陣列內容
}
content裡儲存了真正的字串內容,類似於Java的ArrayList結構,需要比實際內容長度多分配一些冗餘空間,capacity表示所分配陣列的長度,len表示字串的實際長度。字串是可以修改的,要支援append操作,如果陣列沒有冗餘空間,那麼追加操作必然涉及到分配新陣列,將舊內容複製,append新內容,如果字串長度很長,這樣分配和複製開銷就會很大。
/* Append the specified binary-safe string pointed by 't' of 'len' bytes to the * end of the specified sds string 's'. * * After the call, the passed sds string is no longer valid and all the * references must be substituted with the new pointer returned by the call. */ sds sdscatlen(sds s, const void *t, size_t len) { size_t curlen = sdslen(s); // 原字串長度 // 按需調整空間,如果 capacity 不夠容納追加的內容,就會重新分配位元組陣列並複製原字 符串的內容到新陣列中 s = sdsMakeRoomFor(s,len); if (s == NULL) return NULL; // 記憶體不足 memcpy(s+curlen, t, len); // 追加目標字串的內容到位元組陣列中 sdssetlen(s, curlen+len); // 設定追加後的長度值 s[curlen+len] = '\0'; // 讓字串以\0 結尾,便於除錯列印,還可以直接使用 glibc 的字串 函式進行操作 return s; }
為什麼使用泛型T而不使用int,因為字串較短時,len和capacity可以用byte和short表示,對記憶體做了極致優化,不同長度的字串可以使用不同的結構體表示。
redis規定字串的長度不得超過512M,建立字串時,len和capacity一樣長,不會分配冗餘空間,因為絕大多數場景不會修改字串。
embstr vs raw
127.0.0.1:6379> set str abcdefghijklmnopqrstuvwxyz012345678912345678 OK 127.0.0.1:6379> debug object str Value at:0x7fb20e50d8d0 refcount:1 encoding:embstr serializedlength:45 lru:11843071 lru_seconds_idle:8 127.0.0.1:6379> set codehole abcdefghijklmnopqrstuvwxyz0123456789123456789 OK 127.0.0.1:6379> debug object codehole Value at:0x7fb20f90b690 refcount:1 encoding:raw serializedlength:46 lru:11843088 lru_seconds_idle:5
所有的redis物件都有下面這個結構頭
struct RedisObject {
int4 type; // 4bits
int4 encoding; // 4bits
int24 lru; // 24bits
int32 refcount; // 4bytes
void *ptr; // 8bytes,64-bit system
} robj;
不同的物件具有不同的型別 type(4bit),同一個型別的 type 會有不同的儲存形式 encoding(4bit),為了記錄物件的 LRU 資訊,使用了 24 個 bit 來記錄 LRU 資訊。每個對 象都有個引用計數,當引用計數為零時,物件就會被銷燬,記憶體被回收。ptr 指標將指向對 象內容 (body) 的具體儲存位置。這樣一個 RedisObject 物件頭需要佔據 16 位元組的儲存空 間。
SDS物件頭最少時19(16+3)位元組
struct SDS {
int8 capacity; // 1byte
int8 len; // 1byte
int8 flags; // 1byte
byte[] content; // 內聯陣列,長度為 capacity
}
而記憶體分配器 jemalloc/tcmalloc 等分配記憶體大小的單位都是 2、4、8、16、32、64 等 等,為了能容納一個完整的 embstr 物件,jemalloc 最少會分配 32 位元組的空間,如果字元 串再稍微長一點,那就是 64 位元組的空間。如果總體超出了 64 位元組,Redis 認為它是一個 大字串,不再使用 emdstr 形式儲存,而該用 raw 形式。
SDS 結構體中的 content 中的字串是以位元組\0 結尾的字串,之所以 多出這樣一個位元組,是為了便於直接使用 glibc 的字串處理函式,以及為了便於字串的 除錯列印輸出。
看上面這張圖可以算出,留給 content 的長度最多隻有 45(64-19) 位元組了。字串又是
以\0 結尾,所以 embstr 最大能容納的字串長度就是 44。
擴容策略
字串在長度小於 1M 之前,擴容空間採用加倍策略,也就是保留 100% 的冗餘空 間。當長度超過 1M 之後,為了避免加倍後的冗餘空間過大而導致浪費,每次擴容只會多分 配 1M 大小的冗餘空間。
字典
dict內部結構
dict結構內層包含兩個hashtable,通常只有一個hashtable有值,但是在dict擴容縮容時,需要分配新的hashtable,然後進行漸進式搬遷,搬遷完畢後,舊的hashtable被刪除。
hashtable幾乎和java重的hashmap資料結構一樣,陣列+連結串列。
查詢過程
元素是在第二維的連結串列上,首先找出元素對應的連結串列。
func get(key){
let index = hash_func(key)%size;
let entry = table[index];
while(entry != null){
if entry.key = target{
return entry.value;
}
entry = entry.next;
}
}
hash_func,會將key對映為一個整數,不同的key會被對映成分佈均勻的散亂整數,只有hash值均勻了,整個hashtable才是平衡的,所有的二維連結串列的長度就不會差很遠,比較穩定。
擴容條件
正常情況下,當 hash 表中元素的個數等於第一維陣列的長度時,就會開始擴容,擴容 的新陣列是原陣列大小的 2 倍。不過如果 Redis 正在做 bgsave,為了減少記憶體頁的過多分 離 (Copy On Write),Redis 儘量不去擴容 (dict_can_resize),但是如果 hash 表已經非常滿 了,元素的個數已經達到了第一維陣列長度的 5 倍 (dict_force_resize_ratio),說明 hash 表 已經過於擁擠了,這個時候就會強制擴容。
縮容條件
當 hash 表因為元素的逐漸刪除變得越來越稀疏時,Redis 會對 hash 表進行縮容來減少
hash 表的第一維陣列空間佔用。縮容的條件是元素個數低於陣列長度的 10%。縮容不會考 慮 Redis 是否正在做 bgsave。
集合
Redis裡面set的的結構底層實現也是字典,只不過所有的value都是null,其他特性和字典一樣。
127.0.0.1:6379> sadd country china japan usa
(integer) 3
127.0.0.1:6379> debug object country
Value at:0x7fb20e60d3f0 refcount:1 encoding:hashtable serializedlength:17 lru:12015767 lru_seconds_idle:9