String 類型可以保存二進制字節流,就像“萬金油”一樣,只要把數據轉成二進制字節數組,就可以保存了。但String 類型并不是適用于所有場合的,它有一個明顯的短板,就是它保存數據時所消耗的內存空間較多。
一組圖片 ID 及其存儲對象 ID 的記錄,只需要 16 字節就可以了。但是存為String 類型卻要64 字節,為什么 String 類型內存開銷大?
其實,除了記錄實際數據,String 類型還需要額外的內存空間記錄數據長度、空間使用等信息,這些信息也叫作元數據。當實際保存的數據較小時,元數據的空間開銷就顯得比較大了,有點“喧賓奪主”的意思。
SDS
當保存的數據中包含字符時,String 類型就會用簡單動態字符串(Simple Dynamic String,SDS)結構體來保存,如下圖所示:
buf:字節數組,保存實際數據。為了表示字節數組的結束,Redis 會自動在數組最后加一個“\0”,這就會額外占用 1 個字節的開銷。
len:占 4 個字節,表示 buf 的已用長度。
alloc:也占個 4 字節,表示 buf 的實際分配長度,一般大于 len。
在 SDS 中,buf 保存實際數據,而 len 和 alloc 本身其實是 SDS 結構體的額外開銷。
RedisObject
除了 SDS 的額外開銷,還有一個來自于 RedisObject 結構體的開銷。
因為 Redis 的數據類型有很多,而且,不同數據類型都有些相同的元數據要記錄(比如最后一次訪問的時間、被引用的次數等),所以,Redis 會用一個 RedisObject 結構體來統一記錄這些元數據,同時指向實際數據。
一個 RedisObject 包含了 8 字節的元數據和一個 8 字節指針,這個指針再進一步指向具體數據類型的實際數據所在
為了節省內存空間,Redis 還對 Long 類型整數和 SDS 的內存布局做了專門的設計。一方面,當保存的是 Long 類型整數時,RedisObject 中的指針就直接賦值為整數數據了,這樣就不用額外的指針再指向整數了,節省了指針的空間開銷。
編碼
1,當保存 64 位有符號整數時,String 類型會把它保存為一個 8 字節的 Long 類型整數,這種保存方式通常也叫作 int 編碼方式。
2,當保存的是字符串數據,并且字符串小于等于 44 字節時,RedisObject 中的元數據、指針和 SDS 是一塊連續的內存區域,這樣就可以避免內存碎片。這種布局方式也被稱為 embstr 編碼方式。
3,當字符串大于 44 字節時,SDS 的數據量就開始變多了,Redis 就不再把 SDS 和 RedisObject 布局在一起了,而是會給 SDS 分配獨立的空間,并用指針指向 SDS 結構。這種布局方式被稱為 raw 編碼模式。
dictEntry
Redis 會使用一個全局哈希表保存所有鍵值對,哈希表的每一項是一個 dictEntry 的結構體,用來指向一個鍵值對。
dictEntry 結構中有三個 8 字節的指針,分別指向 key、value 以及下一個 dictEntry,三個指針共 24 字節
jemalloc
jemalloc 在分配內存時,會根據我們申請的字節數 N,找一個比 N 大,但是最接近 N 的 2 的冪次數作為分配的空間,使得內存對齊,減少內存碎片化。
如果你申請 6 字節空間,jemalloc 實際會分配 8 字節空間;如果你申請 24 字節空間,jemalloc 則會分配 32 字節。所以,在我們剛剛說的場景里,dictEntry 結構就占用了 32 字節。
現在可以回答16 字節的數據,存為String類型需要64字節了:ID是Long 類型整數,可以直接用 int 編碼的 RedisObject 保存RedisObject 元數據部分占 8 字節,指針部分被直接賦值也為 8 字節;dictEntry三個指針24字節,由于jemalloc會申請32字節。所以為 16(key) + 16 (value) + 32 (dictEntry)的空間存了分別為8字節的key和value。
為什么dictEntry要有個next指針,有什么意義,又怎么樣才能通過key找到該dictEntry?
不同的key做完哈希后通過dictiht找到dictEntry鏈表再遍歷找到dictEntry。
redis中dictiht的數據結構(上圖標黃的部分)的字段table指向了一個dictEntry數組(數組的初始大小為4),而dictEntry就會按照一定的規則被存放在table中。不同的key做完哈希后,有可能出現哈希值相同的情況,因此可能出現沖突的情況。redis的解決方式是,將哈希值相同的key所在的dictEntry用指針的方式鏈接起來。
看起來dictht已經實現了存儲key-value的所有功能,那為什么還需要dict呢?
dict的出現,主要是為了解決dictiht擴容的問題,當dictiht需要擴容時,會先創建一個2倍大小的新的dictiht,然后逐漸將數據遷移過來,遷移完成后將老的dictiht釋放。即為rehash了。而前面講的hash桶就是dictht。rehash漸進式的將dictiht[0]的數據遷移到dictiht[1]。再釋放dictht[0]。redis數據結構-dict