雖然內部數據結構很是強大,可是建立一系列完整的數據結構自己也是一件至關耗費內存的工redis
做,當一個對象包含的元素數量並很少,或者元素自己的體積並不大時,使用代價高昂的內部算法
數據結構並非最好的辦法。數組
爲了解決這一問題,Redis 在條件容許的狀況下,會使用內存映射數據結構來代替內部數據結構。數據結構
內存映射數據結構是一系列通過特殊編碼的字節序列,建立它們所消耗的內存一般比做用相似ide
的內部數據結構要少得多,若是使用得當,內存映射數據結構能夠爲用戶節省大量的內存。函數
不過,由於內存映射數據結構的編碼和操做方式要比內部數據結構要複雜得多,因此內存映射ui
數據結構所佔用的CPU 時間會比做用相似的內部數據結構要多。編碼
這一部分將對Redis 目前正在使用的兩種內存映射數據結構進行介紹。spa
一、整數集合指針
整數集合(intset)用於有序、無重複地保存多個整數值,它會根據元素的值,自動選擇該用什
麼長度的整數類型來保存元素。
舉個例子,若是在一個intset 裏面,最長的元素能夠用int16_t 類型來保存,那麼這個intset
的全部元素都以int16_t 類型來保存。
另外一方面,若是有一個新元素要加入到這個intset ,而且這個元素不能用int16_t 類型來保存
——好比說,新元素的長度爲int32_t ,那麼這個intset 就會自動進行「升級」 :先將集合中現
有的全部元素從int16_t 類型轉換爲int32_t 類型,接着再將新元素加入到集合中。
根據須要,intset 能夠自動從int16_t 升級到int32_t 或int64_t ,或者從int32_t 升級到
int64_t 。
整數集合的應用
Intset 是集合鍵的底層實現之一,若是一個集合:
1. 只保存着整數元素;
2. 元素的數量很少;
那麼Redis 就會使用intset 來保存集合元素。
數據結構和主要操做
如下是intset.h/intset 類型的定義:
typedef struct intset { // 保存元素所使用的類型的長度 uint32_t encoding; // 元素個數 uint32_t length; // 保存元素的數組 int8_t contents[]; } intset;
encoding 的值能夠是如下三個常量的其中一個(定義位於intset.c ):
#define INTSET_ENC_INT16 (sizeof(int16_t)) #define INTSET_ENC_INT32 (sizeof(int32_t)) #define INTSET_ENC_INT64 (sizeof(int64_t))
contents 數組是實際保存元素的地方,數組中的元素有如下兩個特性:
沒有重複元素;
元素在數組中從小到大排列;
contents 數組的int8_t 類型聲明比較容易讓人誤解,實際上,intset 並不使用int8_t 類型
來保存任何元素,結構中的這個類型聲明只是做爲一個佔位符使用:在對contents 中的元素
進行讀取或者寫入時,程序並非直接使用contents 來對元素進行索引,而是根據encoding
的值,對contents 進行類型轉換和指針運算,計算出元素在內存中的正確位置。在添加新元
素,進行內存分配時,分配的容量也是由encoding 的值決定。
intset 運行實例
讓咱們跟蹤一個intset 的建立和添加過程,籍此瞭解intset 的運做方式。
建立新intset
intset.c/intsetNew 函數建立一個新的intset ,併爲它設置初始化值:
intset *is = intsetNew(); // intset->encoding = INTSET_ENC_INT16; // intset->length 0; // intset->contents = [];
注意encoding 使用INTSET_ENC_INT16 做爲初始值。
添加新元素到intset
建立intset 以後,就能夠對它添加新元素了。
添加新元素到intset 的工做由intset.c/intsetAdd 函數完成,它須要處理如下三種狀況:
1. 元素已存在於集合,不作動做;
2. 元素不存在於集合,而且添加新元素並不須要升級;
3. 元素不存在於集合,可是要在升級以後,才能添加新元素;
而且,intsetAdd 須要維持intset->contents 的如下性質:
1. 確保數組中沒有重複元素;
2. 確保數組中的元素按從小到大排序;
整個intsetAdd 的執行流程能夠表示爲下圖:
如下兩個小節分別演示添加操做在升級和不升級兩種狀況下的執行過程。
添加新元素到intset (不須要升級)
若是intset 現有的編碼方式適用於新元素,那麼能夠直接將新元素添加到intset ,無須對intset
進行升級。
如下代碼演示了將三個int16_t 類型的整數添加到集合的過程,以及在添加過程當中,集合的狀
態:
intset *is = intsetNew(); intsetAdd(is, 10, NULL); // is->encoding = INTSET_ENC_INT16; // is->length = 1; // is->contents = [10]; intsetAdd(is, 5, NULL); // is->encoding = INTSET_ENC_INT16; // is->length = 2; // is->contents = [5, 10]; intsetAdd(is, 12, NULL); // is->encoding = INTSET_ENC_INT16; // is->length = 3; // is->contents = [5, 10, 12]
由於添加的三個元素均可以表示爲int16_t ,所以is->encoding 一直都是INTSET_ENC_INT16
。
另外一方面,is->length 和is->contents 的值則隨着新元素的加入而被修改。
添加新元素到intset (須要升級)
當要添加新元素到intset ,而且intset 當前的編碼並不適用於新元素的編碼時,就須要對inset
進行升級。
如下代碼演示了帶升級的添加操做的執行過程:
intset *is = intsetNew(); intsetAdd(is, 1, NULL); // is->encoding = INTSET_ENC_INT16; // is->length = 1; // is->contents = [1]; // 全部值使用int16_t 保存 intsetAdd(is, 65535, NULL); // is->encoding = INTSET_ENC_INT32; // 升級 // is->length = 2; // is->contents = [1, 65535]; // 全部值使用int32_t 保存 intsetAdd(is, 70000, NULL); // is->encoding = INTSET_ENC_INT32; // is->length = 3; // is->contents = [1, 65535, 70000]; intsetAdd(is, 4294967295, NULL); // is->encoding = INTSET_ENC_INT64; // 升級 // is->length = 4; // is->contents = [1, 65535, 70000, 4294967295]; // 全部值使用int64_t 保存
在添加65535 和4294967295 以後,encoding 屬性的值,以及contents 數組保存值的方式,
都被改變了。
升級
在添加新元素時,若是intsetAdd 發現新元素不能用現有的編碼方式來保存,它就會將升級集
合和添加新元素的任務轉交給intsetUpgradeAndAdd 來完成:
intsetUpgradeAndAdd 須要完成如下幾個任務:
1. 對新元素進行檢測,看保存這個新元素須要什麼類型的編碼;
2. 將集合encoding 屬性的值設置爲新編碼類型,並根據新編碼類型,對整個contents 數
組進行內存重分配。
3. 調整contents 數組內原有元素在內存中的排列方式,讓它們從舊編碼調整爲新編碼。
4. 將新元素添加到集合中。
整個過程當中,最複雜的就是第三步,讓咱們用一個例子來理解這個步驟。
升級實例
假設有一個intset ,裏面包含三個用int16_t 方式保存的數值,分別是1 、2 和3 ,它的結
構以下:
如今,咱們要要將一個長度爲int32_t 的值65535 加入到集合中,intset 須要執行如下步驟:
1. 將encoding 屬性設置爲INTSET_ENC_INT32 。
2. 根據encoding 屬性的值,對contents 數組進行內存重分配。
重分配完成以後,contents 在內存中的排列以下:
contents 數組如今共有可容納4 個int32_t 值的空間。
3. 由於原來的3 個int16_t 值還「擠在」contents 前面的48 個位裏,因此程序須要對它們
進行移動和類型轉換,從而讓它們適應集合的新編碼方式。
首先是移動3 :
4. 最後,將新值65535 添加到數組:
至此,集合的升級和添加操做完成,如今的intset 結構以下:
intset->encoding = INTSET_ENC_INT32; intset->length = 4; intset->contents = [1, 2, 3, 65535];
關於升級
關於升級操做,還有兩點須要提醒一下:
第一,從較短整數到較長整數的轉換,並不會更改元素裏面的值。
在C 語言中,從長度較短的帶符號整數到長度較長的帶符號整數之間的轉換(好比從int16_t
轉換爲int32_t )老是可行的(不會溢出)、無損的。
另外一方面,從較長整數到較短整數之間的轉換多是有損的(好比從int32_t 轉換爲int16_t
)。
由於intset 只進行從較短整數到較長整數的轉換(也便是,只「升級」 ,不「降級」 ),所以, 「升
級」操做並不會修改元素原有的值。
第二,集合編碼元素的方式,由元素中長度最大的那個值來決定。
就像前面演示的例子同樣,當要將一個int32_t 編碼的新元素添加到集合時,集合原有的全部
int16_t 編碼的元素,都必須轉換爲int32_t 。
儘管這個集合真正須要用int32_t 長度來保存的元素只有一個,但整個集合的全部元素都必須
轉換爲這種類型。
關於元素移動
在進行升級的過程當中,須要對數組內的元素進行「類型轉換」和「移動」操做。
其中,移動不只出如今升級(intsetUpgradeAndAdd)操做中,還出現其餘對contents 數組內
容進行增刪的操做上,好比intsetAdd 和intsetRemove ,由於這種移動操做須要處理intset
中的全部元素,因此這些函數的複雜度都不低於O(N) 。
其餘操做
如下是一些關於intset 其餘操做的討論。
讀取
有兩種方式讀取intset 的元素,一種是_intsetGet ,另外一種是intsetSearch :
_intsetGet 接受一個給定的索引pos ,並根據intset->encoding 的值進行指針運算,
計算出給定索引在intset->contents 數組上的值。
intsetSearch 則使用二分查找算法,判斷一個給定元素在contents 數組上的索引。
寫入
除了前面介紹過的intsetAdd 和intsetUpgradeAndAdd 以外,_intsetSet 也對集合進行寫
入操做:它接受一個索引pos ,以及一個new_value ,將contents 數組pos 位置的值設爲
new_value 。
刪除
刪除單個元素的工做由intsetRemove 操做,它先調用intsetSearch 找到須要被刪除的元素
在contents 數組中的索引,而後使用內存移位操做,將目標元素從內存中抹去,最後,經過
內存重分配,對contents 數組的長度進行調整。
降級
Intset 不支持降級操做。
Intset 定位爲一種受限的中間表示, 只能保存整數值, 並且元素的個數也不能超過
redis.h/REDIS_SET_MAX_INTSET_ENTRIES (目前版本值爲512 ) 這些條件決定了它被保
存的時間不會太長,所以對它進行太複雜的操做,沒有必要。
小結
Intset 用於有序、無重複地保存多個整數值,它會根據元素的值,自動選擇該用什麼長度
的整數類型來保存元素。
當一個位長度更長的整數值添加到intset 時,須要對intset 進行升級,新intset 中每一個
元素的位長度都等於新添加值的位長度,但原有元素的值不變。
升級會引發整個intset 進行內存重分配,並移動集合中的全部元素,這個操做的複雜度
爲O(N) 。
Intset 只支持升級,不支持降級。
Intset 是有序的,程序使用二分查找算法來實現查找操做,複雜度爲O(lgN) 。