1.《redis數據結構概覽》
1.數據結構概覽
數據模型:一共5種,String(字符串)、List(列表)、Hash(哈希)、Set(集合)和 Sorted Set(有序集合)
數據結構:一共有 6 種,分別是簡單動態字符串、雙向鏈表、壓縮列表、哈希表、跳表和整數數組。
對應關系圖:
2.全局哈希表
Redis 使用了一個全局哈希表來保存所有鍵值對
哈希桶中的元素保存的并不是值本身,而是指向具體值的指針。這也就是說,不管值是 String,還是集合類型,哈希桶中的元素都是指向它們的指針。
3.列表(list)
對應兩種實現方法,一種是壓縮列表(ziplist),另一種是雙向循環鏈表。
當列表中存儲的數據量比較小的時候,列表就可以采用壓縮列表的方式實現。具體需要同時滿足下面兩個條件:
列表中保存的單個數據(有可能是字符串類型的)小于 64 字節;(cpu緩存行一行最多64字節)
列表中數據個數少于 512 個。
壓縮列表:
特點:
可以存儲不同字節大小的數據
基于數組實現,但不支持隨機訪問
獲取首尾元素時間復雜度O(1),其他情況O(n)
壓縮列表在表頭有三個字段 zlbytes、zltail 和 zllen,分別表示列表長度(所占空間大小)、列表尾的偏移量(列尾到起始位置的偏移量)和列表中的 entry 個數;
其中每個entry,分為四個部分previous_entry_length,entry_length,encoding,content
分別表示 上一個元素的長度,本元素長度 ,編碼方式,文本內容
4.字典(hash)
有兩種實現方式,一種是壓縮列表,另一種是散列表
有當存儲的數據量比較小的情況下,Redis 才使用壓縮列表來實現字典類型。具體需要滿足兩個條件:
字典中保存的鍵和值的大小都要小于 64 字節;
字典中鍵值對的個數要小于 512 個。
5.集合(set)
有兩種實現方法,一種是基于有序數組,另一種是基于散列表。
當要存儲的數據,同時滿足下面這樣兩個條件的時候,Redis 就采用有序數組,來實現集合這種數據類型。
存儲的數據都是整數;
存儲的數據元素個數不超過 512 個。
6.有序集合(sortedset)
當數據量比較小的時候,Redis 會用壓縮列表來實現有序集合。具體點說就是,使用壓縮列表來實現有序集合的前提,有這樣兩個:
所有數據的大小都要小于 64 字節;
元素個數要小于 128 個。
7.為什么 sortedset,hash,list都會用壓縮列表呢 ?
當數據量小的時候通過數組下標訪問最快、占用內存最小,而壓縮列表只是數組的升級版;
因為數組需要占用連續的內存空間,所以當數據量大的時候,就需要使用鏈表了,同時為了保證速度又需要和數組結合,也就有了散列表。
1、內存利用率,數組和壓縮列表都是非常緊湊的數據結構,它比鏈表占用的內存要更少。Redis是內存數據庫,大量數據存到內存中,此時需要做盡可能的優化,提高內存的利用率。
2、數組對CPU高速緩存支持更友好,所以Redis在設計時,集合數據元素較少情況下,默認采用內存緊湊排列的方式存儲,同時利用CPU高速緩存不會降低訪問速度。當數據元素超過設定閾值后,避免查詢時間復雜度太高,轉為哈希和跳表數據結構存儲,保證查詢效率。
2.《redis 動態字符串詳解》
1.案例:實現一個圖片存儲系統,并快速定位文件位置
案例描述:開發一個圖片存儲系統,要求這個系統能快速地記錄圖片 ID 和圖片在存儲系統中保存時的 ID(可以直接叫作圖片存儲對象 ID)。同時,還要能夠根據圖片 ID 快速查找到圖片存儲對象 ID。因為圖片數量巨大,所以我們就用 10 位數來表示圖片 ID 和圖片存儲對象 ID,
例如,圖片 ID 為 1101000051,它在存儲系統中對應的 ID 號是 3301000051。
photo_id: 1101000051
photo_obj_id: 3301000051
1
2
圖片 ID 和圖片存儲對象 ID 正好一一對應,是典型的“鍵 - 單值”模式,這和 String 類型提供的“一個鍵對應一個值的數據”的保存形式剛好契合。
當我們儲存一億張圖片時,用6.4G內存,但是,隨著圖片數據量的不斷增加,我們的 Redis 內存使用量也在增加,結果就遇到了大內存 Redis 實例因為生成 RDB 而響應變慢的問題。
我們保存了 1 億張圖片的信息,用了約 6.4GB 的內存,一個圖片 ID 和圖片存儲對象 ID 的記錄平均用了 64 字節。但問題是,一組圖片 ID 及其存儲對象 ID 的記錄,實際只需要 16 字節就可以了。
question:那么redis 的動態字符串類型為什么會使用這么多內存呢?另外48Byte 的內存用到哪里了呢?
2.動態字符串結構體 (Simple Dynamic String,SDS)
當我們用String類型鍵值對存儲數據時,
當你保存 64 位有符號整數時,String 類型會把它保存為一個 8 字節的 Long 類型整數,這種保存方式通常也叫作 int 編碼方式。
當你保存的數據中包含字符時,String 類型就會用簡單動態字符串(Simple Dynamic String,SDS)結構體來保存,如下圖所示:
len: 4字節 ,表示buf 使用長度
alloc:4字節,表示 內存分配器分配空間 大小
buf:字節數組,保存實際數據,Redis 會自動在數組最后加一個“\0”,這就會額外占用 1 個字節的開銷。
3.RedisObject
對于 String 類型來說,除了 SDS 的額外開銷,還有一個來自于 RedisObject 結構體的開銷。
因為 Redis 的數據類型有很多,而且,不同數據類型都有些相同的元數據要記錄(比如最后一次訪問的時間、被引用的次數等),所以,Redis 會用一個 RedisObject 結構體來統一記錄這些元數據,同時指向實際數據。
Long類型時: ptr引用指針直接填充賦值為整數數據這樣就不用額外的指針再指向整數了,節省了指針的空間開銷。這種布局方式也被稱為 。
小于等于44字節的字符串:RedisObject 中的元數據、指針和 SDS 是一塊連續的內存區域,這樣就可以避免內存碎片。這種布局方式也被稱為
大于44字節的字符串時,SDS 的數據量就開始變多了,Redis 就不再把 SDS 和 RedisObject 布局在一起了,而是會給 SDS 分配獨立的空間,并用指針指向 SDS 結構。這種布局方式被稱為
下面是 三種編碼方式的結構圖:
所以,例子中,保存“1101000051:3301000051”只使用 了 32個字節,另外32字節消耗在哪里了?
這就要講一下 redis 中的 dicEntry結構了
4.全局哈希表的 dicEntry結構
Redis 會使用一個全局哈希表保存所有鍵值對,哈希表的每一項是一個 dictEntry 的結構體,用來指向一個鍵值對。
dictEntry 結構中有三個 8 字節的指針,分別指向 key、value 以及下一個 dictEntry,三個指針共 24 字節,如下圖所示:
但是,加上 這三個指針也才 24個字節 + 32字節 ,不夠64字節啊,為什么?
應為redis 內存分配器 jemalloc,分配內存 按頁分配,且分配 2的冪次方大小的內存頁
所以,申請 了 56個字節,但是 內存分配器分配了 64字節
至此,保存“1101000051:3301000051”為什么分配了64字節就 真相大白了
那么,用什么來存儲海量 的String類型,單向鍵值對呢?
3.壓縮列表
1. ziplist結構
壓縮列表(ziplist):表頭有 zlbyts(4字節,列表所占字節大小),zltail(4字節,列尾到 列表其實位置的偏移量offset),zllen(2字節,列表entry元素個數); 最后在列表尾部有一個 zlend(1字節)表示列表結束
prev_len: 上一個元素 長度,prev_len 有兩種取值情況:1 字節或 5 字節。
取值 1 字節時,表示上一個 entry 的長度小于 254 字節。雖然 1 字節的值能表示的數值范圍是 0 到 255,但是壓縮列表中 zlend 的取值默認是 255,因此,就默認用 255 表示整個壓縮列表的結束,其他表示長度的地方就不能再用 255 這個值了。所以,當上一個 entry 長度小于 254 字節時,prev_len 取值為 1 字節;
否則,就取值為 5 字節。
len:當前元素長度,4字節
encoding: 編碼方式,1字節
content:保存實際數據
如上圖,展示了一個總長為80字節,包含3個節點的壓縮列表。如果我們有一個指向壓縮列表起始地址的指針p,那么表尾節點的地址就是P+60。
回到 存儲海量圖片并要求快速定位圖片的例子:
每個 entry 保存一個圖片存儲對象 ID(8 字節),此時,每個 entry 的 prev_len 只需要 1 個字節就行,因為每個 entry 的前一個 entry 長度都只有 8 字節,小于 254 字節。這樣一來,一個圖片的存儲對象 ID 所占用的內存大小是 14 字節(1+4+1+8=14),實際分配 16 字節。
2.使用ziplist好處以及 ziplist應用
Redis 基于壓縮列表實現了 List、Hash 和 Sorted Set 這樣的集合類型,這樣做的最大好處就是節省了 dictEntry 的開銷。當你用 String 類型時,一個鍵值對就有一個 dictEntry,要用 32 字節空間。但采用集合類型時,一個 key 就對應一個集合的數據,能保存的數據多了很多,但也只用了一個 dictEntry,這樣就節省了內存。
3.如何用集合類型保存單值的鍵值對?
在保存單值的鍵值對時,可以采用基于 Hash 類型的二級編碼方法。這里說的二級編碼,就是把一個單值的數據拆分成兩部分,前一部分作為 Hash 集合的 key,后一部分作為 Hash 集合的 value,這樣一來,我們就可以把單值數據保存到 Hash 集合中了。
以圖片 ID 1101000060 和圖片存儲對象 ID 3302000080 為例,我們可以把圖片 ID 的前 7 位(1101000)作為 Hash 類型的鍵,把圖片 ID 的最后 3 位(060)和圖片存儲對象 ID 分別作為 Hash 類型值中的 key 和 value。
增加一條記錄后,內存占用只增加了 16 字節:
127.0.0.1:6379> hset 1101000 060 3302000080
(integer) 1
1
2
4.什么時候使用ziplist?
Redis Hash 類型的兩種底層實現結構,分別是壓縮列表和哈希表。
Hash 類型底層結構什么時候使用壓縮列表,什么時候使用哈希表呢?其實,Hash 類型設置了用壓縮列表保存數據時的兩個閾值,一旦超過了閾值,Hash 類型就會用哈希表來保存數據了。
hash-max-ziplist-entries:表示用壓縮列表保存時哈希集合中的最大元素個數。
hash-max-ziplist-value:表示用壓縮列表保存時哈希集合中單個元素的最大長度。
為了能充分使用壓縮列表的精簡內存布局,我們一般要控制保存在 Hash 集合中的元素個數。所以,在剛才的二級編碼中,我們只用圖片 ID 最后 3 位作為 Hash 集合的 key,也就保證了 Hash 集合的元素個數不超過 1000,同時,我們把 hash-max-ziplist-entries 設置為 1000,這樣一來,Hash 集合就可以一直使用壓縮列表來節省內存空間了。
4.集合類型:一億個key要統計怎么辦?
總結
幾種類型使用:
HyperLogLog:基數統計,集合元素量達到億級別而且不需要精確統計
Bitmap:記錄的數據只有 0 和 1 兩個值的狀態
Set 和 Sorted Set:都支持多種聚合統計(交并差集),不過,對于差集計算來說,只有 Set 支持
List和Sorted Set:
排序統計時,List 中的元素雖然有序,但是一旦有新元素插入,原來的元素在 List 中的位置就會移動,那么,按位置讀取的排序結果可能就不準確了。而 Sorted Set 本身是按照集合元素的權重排序,可以準確地按序獲取結果,所以建議你優先使用它。
?