1. 程式人生 > 實用技巧 >《redis深度歷險》八(字串和字典)

《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