當一個集合中只包含整數,而且元素的個數不是不少的話,redis 會用整數集合做爲底層存儲,它的一個優勢就是能夠節省不少內存,雖然字典結構的效率很高,可是它的實現結構相對複雜而且會分配較多的內存空間。java
而咱們的整數集合(intset)能夠作到使用較少的內存空間卻達到和字典同樣效率的實現,但也是前提的,集合中只能包含整型數據而且數量不能太多。整數集合最多能存多少個元素在 redis 中也是有體現的。git
OBJ_SET_MAX_INTSET_ENTRIES 512程序員
也就是超過 512 個元素,或者向集合中添加了字符串或其餘數據結構,redis 會將整數集合向字典結構進行轉換。github
intset 的結構定義很簡單,有如下成員構成:redis
typedef struct intset {
uint32_t encoding;
uint32_t length;
int8_t contents [];
} intset;
複製代碼
encoding 記錄當前 intset 使用編碼,有三個取值:數組
#define INTSET_ENC_INT16 (sizeof(int16_t)) #define INTSET_ENC_INT32 (sizeof(int32_t)) #define INTSET_ENC_INT64 (sizeof(int64_t)) 複製代碼
length 記錄整數集合中目前存儲了多少個元素,contents 記錄咱們實際的數據集合,雖然咱們看到結構體中給數組元素的類型定死成 int8_t,但實際上這個 int8_t 定義的毫無心義,由於這裏的處理方式很是規的數組操做,content 字段雖然被定義成指向一個 int8_t 類型數據的指針,但實際上 redis 不管是讀取數組元素仍是新增元素進去都依賴 encoding 和 length 兩個字段直接操做的內存。bash
基本數據結構仍是很是的簡單的,下面咱們來看看它的一些核心方法。微信
一、初始化一個 intsetmarkdown
intset *intsetNew(void) { intset *is = zmalloc(sizeof(intset)); is->encoding = intrev32ifbe(INTSET_ENC_INT16); is->length = 0; return is; } 複製代碼
可見,默認的 inset 配置是使用 INTSET_ENC_INT16 做爲數據存儲大小,而且不會爲 content 數組初始化。常規的數組須要先預先肯定數組長度,而後分配內存,繼而經過 contents[x] 能夠訪問數組中任一元素。數據結構
可是,inset 這裏是很是規式操做數組,encoding 字段定義了數組中每一個元素實際類型,lenth 字段定義了數組中實際的元素個數,那麼 contents[x] 是失效的,這種方式只會按照 int8_t 進行內存偏移,這種方式是拿不到正確的數據的,因此 redis 中經過 memcpy 按照 encoding 字段的值暴力直接偏移地址操做內存讀取數據。
因此,這也是爲何 intset 初始化時不初始化 content 數組的緣由所在,由於沒有必要。而每當新增一個元素的時候都會去動態擴容原數組的長度以盛放下新插入進來的元素,擴容不會擴容不少,恰好一個新元素所佔用的內存便可。具體的細節,咱們接着看。
二、添加新元素
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) { //計算獲得新插入的元素的編碼 uint8_t valenc = _intsetValueEncoding(value); uint32_t pos; if (success) *success = 1; //若是大於 intset 目前存儲元素的編碼大小 if (valenc > intrev32ifbe(is->encoding)) { //觸發 intset 升級 return intsetUpgradeAndAdd(is,value); } else { //二分搜索當前元素,若是元素已經存在會直接返回 //若是沒找到元素,pos 的值就是該元素的位置索引 if (intsetSearch(is,value,&pos)) { if (success) *success = 0; return is; } //resize 集合,擴容一個元素的內存空間 is = intsetResize(is,intrev32ifbe(is->length)+1); //移動 pos 後面的元素,以插入咱們的新元素 if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1); } //賦值 _intsetSet(is,pos,value); is->length = intrev32ifbe(intrev32ifbe(is->length)+1); return is; } 複製代碼
由此,咱們應該知道爲何 intset 內的數據是有序且無重複的了,二分查找 O(logN),可是 intset 插入一個元素卻不是 O(logN),由於有些狀況會觸發升級操做,或者極端狀況下,會移動全部元素,時間複雜度達到 O(N)。
三、升級
咱們先看示意圖的變化,而後再分析源碼,假設原 intset 使用 16 位的編碼存儲數據,先來了一個 32 位的數據,觸發了咱們的編碼升級。
原 intset 結構以下:
新 intset 結構會擴容成這樣:
雖然數據佔用的內存已經分配好了,可是還須要作的是遷移每一個元素佔用的比特位。 作法是這樣的,假設咱們的新元素是 int_32 類型的數值 65536,那麼首先咱們會將這個 65536 放到[128-159]比特位區間,而後將 78 放到[96-127]比特位區間,並向前以此類推,最後咱們會獲得升級完成以後 intset。
下面咱們看 redis 中代碼的實現:
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) { //intset目前的編碼 uint8_t curenc = intrev32ifbe(is->encoding); //intset即將擴展到的編碼 uint8_t newenc = _intsetValueEncoding(value); int length = intrev32ifbe(is->length); int prepend = value < 0 ? 1 : 0; //根據新的元素內存大小從新分配 intset 內存大小 is->encoding = intrev32ifbe(newenc); is = intsetResize(is,intrev32ifbe(is->length)+1); //這個地方我先標記一下 @1,下面詳細分析 //整體上你能夠理解,就是咱們上圖畫的那樣,從原集合的最後一個元素 //開始擴大它佔用的比特位 while(length--) _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc)); //將新元素放進 intset 中 if (prepend) _intsetSet(is,0,value); else _intsetSet(is,intrev32ifbe(is->length),value); is->length = intrev32ifbe(intrev32ifbe(is->length)+1); return is; } 複製代碼
別的再也不解釋,我重點解釋一下我作標記的 @1,這個循環實際上是這個方法的核心點,它完成了將舊元素擴充比特位這麼一個操做。
首先明確的一點是,升級操做只有兩種狀況會觸發,一種是新插入一個較大的數值,另外一種是新插入一個負很大的值,這兩種狀況都會致使類型不夠存儲,須要擴大數據位。
_intsetGetEncoded 這個方法能夠根據給定了 length,也就是元素在數組中的下標取出舊數組中對應的元素,很顯然,這裏是從後往前倒着來的。
由於咱們的 intsetResize 方法已經完成了擴容內存的操做,也就是說新元素的內存已經分配完畢,那麼 _intsetSet 方法就會將 _intsetGetEncoded 取出的元素從新的向數組中賦值。循環結束時,就是全部元素從新歸位的時候,最後再將新元素賦值進入數組最後的位置。
但其實細心的同窗會發現,_intsetSet 方法在傳下標索引的時候實際傳的是 length+prepend,這其實就是咱們說,若是 value 是小於零的,length+prepend 最終會致使全部的舊元素日後挪了一個偏移量,而後新的元素會被賦值的索引爲零的位置。也就是說,若是新插入的數值是負數,它會被頭插進數組的第一個位置。
核心的幾個 API 咱們都已經介紹了,其餘的一些 API 你能夠自行參閱源碼,相信對你不難。
總結一下,整數集合(intset)使用了很是簡潔的數據結構,能夠更少的佔用內存存儲一些整數,但終究是基於數組的,也就避免不了不能存儲大量數據的缺點。整體來講,插入一個元素,最好狀況 O(logN),最壞的狀況是 O(N),攤還時間複雜度爲 O(N),查找一個元素,根據索引下標時間複雜度在 O(1)。當 intset 中的元素超過 512 個,或者向其中添加了字符串,redis 會將 intset 轉換成字典。
一樣的,若是以爲我寫的對你有點幫助的話,順手點一波關注吧,也歡迎加做者微信深刻探討,咱們下一講,壓縮列表,盡請關注。
關注公衆不迷路,一個愛分享的程序員。 公衆號回覆「1024」加做者微信一塊兒探討學習! 每篇文章用到的全部案例代碼素材都會上傳我我的 github github.com/SingleYam/o… 歡迎來踩!