整數集合
《Redis設計與實現》
整數集合(intset)是Redis中集合鍵的底層實現之一,當一個集合只包含整數值元素,並且這個集合的元素不多時,Redis就會使用整數集合作爲集合鍵的底層實現。
在Redis中,它可以保存類型爲int16_t
、int32_t
或int64_t
的的整數值,並且保證集合中不會出現重複元素。
代碼結構
// intset.h
typedef struct intset {
uint32_t encoding; // 編碼方式
uint32_t length; // 集合中的元素數量
int8_t contents[]; // 集合中保存元素的數組
} intset;
encoding
: 有3種屬性值INTSET_ENC_INT16
、INTSET_ENC_INT32
、INTSET_ENC_INT64
用於表示保存在contents
中的元素真正的類型:INTSET_ENC_INT16
: 表示contents
中保存的是int16_t
類型的整數值(-32768~32767)。INTSET_ENC_INT32
: 表示contents
中保存的是int32_t
類型的整數值(-2147483648~2147483647)。INTSET_ENC_INT64
: 表示contents
中保存的是int64_t
類型的整數值(-9223372036854775808~9223372036854775807)。
length
: 表示的是集合中元素的數量,但不是contents
中int8_t
的長度。contents
: 集合中的元素是按值得大小,從小到大有序的保存在content
中,並且不包含任何重複的項目;如果encoding
是INTSET_ENC_INT64
,一個元素在contents
中將會佔8個int8_t
的大小。
特點
《Redis設計與實現》
- 整數集合是集合鍵的底層實現之一。
- 整數集合的底層實現爲數組,這個數組以有序、無重複的方式保存集合元素,在有需要時,程序會根據新添加元素的類型,改變這個數組的類型。
- 升級操作爲整數集合帶來了操作上的靈活性,並且儘可能地節約了內存。
- 整數集合只支持升級操作,不支持降級操作。
升級
當將一個新的元素加入到整數集合中,而且新的元素類型比整數集合現有的元素類型encoding
要長,整數集合需要進行升級,擴展數組空間並將原有的元素都轉換成更長的類型。
升級整數集合的順序:
- 根據新元素的類型,擴展整數集合底層數組的空間大小,併爲新元素分配空間。
- 將底層數組現有的所有元素都轉換成與新元素相同的類型,並將類型轉換後的元素放置到正確的位上,同時在這個過程中,要保持底層數組的有序性質不變。
- 將新元素添加到底層數組中。
升級的優點:
- 靈活性,可以使數組很靈活的轉換整數類型。
- 節省內存。
降級
Redis中,整數集合不支持降級操作,所以一旦對數組進行了升級,編碼會一直保持升級後的狀態。
部分代碼解析
-
intset *intsetNew(void)
創建一個新的整數集合:/* Create an empty intset. */ intset *intsetNew(void) { // 分配內存 intset *is = zmalloc(sizeof(intset)); // 默認 int16_t, 將 INTSET_ENC_INT16 轉爲 int32_t 格式保存到 encoding 中 is->encoding = intrev32ifbe(INTSET_ENC_INT16); // 初始化長度 0 is->length = 0; return is; }
-
intset *intsetAdd(intset *is, int64_t value, uint8_t *success)
給定元素value
添加到整數集合is
中,成功或者失敗的標記將會傳入success
中:/* Insert an integer in the intset */ intset *intsetAdd(intset *is, int64_t value, uint8_t *success) { // 通過_intsetValueEncoding()函數,判斷value需要哪個整數的類型,並將類型傳回valenc中 uint8_t valenc = _intsetValueEncoding(value); uint32_t pos; // 將成功標誌默認設爲 1 if (success) *success = 1; /* Upgrade encoding if necessary. If we need to upgrade, we know that * this value should be either appended (if > 0) or prepended (if < 0), * because it lies outside the range of existing values. */ // 判斷是否需要對整數集合進行升級 if (valenc > intrev32ifbe(is->encoding)) { /* This always succeeds, so we don't need to curry *success. */ // 執行升級並將值value添加進整數集合 // 然後返回結果 return intsetUpgradeAndAdd(is,value); } else { /* Abort if the value is already present in the set. * This call will populate "pos" with the right position to insert * the value when it cannot be found. */ // 在整數集合中查找值value是否已經存在 // 如果value不存在於 整數集合is 中,pos的值將是值value適合插入的位置 // 如果已經存在,將 *success 置爲1,並返回 if (intsetSearch(is,value,&pos)) { if (success) *success = 0; return is; } // 對 整數集合is 的內存空間進行擴展,添加一個元素的空間大小 is = intsetResize(is,intrev32ifbe(is->length)+1); // 如果插入位置不是數組尾,則通過intsetMoveTail()調理數組中插入位置後的元素的位置 if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1); } // 執行插入函數,將值value插入is的pos位置 _intsetSet(is,pos,value); // 長度加1 is->length = intrev32ifbe(intrev32ifbe(is->length)+1); return is; } /* Return the required encoding for the provided value. */ // 判斷傳入的值大小是需要int16_t、int32_t 還是 int64_t // 並返回所需要的類型標識 static uint8_t _intsetValueEncoding(int64_t v) { if (v < INT32_MIN || v > INT32_MAX) return INTSET_ENC_INT64; else if (v < INT16_MIN || v > INT16_MAX) return INTSET_ENC_INT32; else return INTSET_ENC_INT16; } /* Upgrades the intset to a larger encoding and inserts the given integer. */ // 升級 並 將 value 插入 整數集合is 中 static intset *intsetUpgradeAndAdd(intset *is, int64_t value) { // 當前集合中整數的類型 uint8_t curenc = intrev32ifbe(is->encoding); // 集合中整數將要升級的類型 uint8_t newenc = _intsetValueEncoding(value); // 集合中元素的個數 int length = intrev32ifbe(is->length); // 新插入的值value是否爲負數 int prepend = value < 0 ? 1 : 0; /* First set new encoding and resize */ // 更新 整數集合is 中的整數類型 編碼 is->encoding = intrev32ifbe(newenc); // 爲is中的數組重新分配大小 // 大小 = 新整數類型的大小 * (is中原本元素的個數 + 1) is = intsetResize(is,intrev32ifbe(is->length)+1); /* Upgrade back-to-front so we don't overwrite values. * Note that the "prepend" variable is used to make sure we have an empty * space at either the beginning or the end of the intset. */ // 從後往前調整數組中的元素,這樣就不會修改元素本來的順序 // 因爲插入了value,而導致了 整數集合需要升級,則說明value要麼是最大的正數,要麼是最小的負數 // 所以如果是負數,則數組中元素不僅要調整結構,還要往後移位,空出第一個空位 // 如果是正數,則數組中元素只需要調整結構 while(length--) // _intsetGetEncoded()是通過元素的索引和編碼獲取元素的值 _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc)); /* Set the value at the beginning or the end. */ if (prepend) // 如果是負數,插入第一位 _intsetSet(is,0,value); else // 如果是正數,插入最後一位 _intsetSet(is,intrev32ifbe(is->length),value); // 長度加1 is->length = intrev32ifbe(intrev32ifbe(is->length)+1); return is; } /* Return the value at pos, given an encoding. */ static int64_t _intsetGetEncoded(intset *is, int pos, uint8_t enc) { // 通過傳入的索引 pos 和 整數結構編碼 enc 在 is 中獲取元素的值 int64_t v64; int32_t v32; int16_t v16; if (enc == INTSET_ENC_INT64) { memcpy(&v64,((int64_t*)is->contents)+pos,sizeof(v64)); memrev64ifbe(&v64); return v64; } else if (enc == INTSET_ENC_INT32) { memcpy(&v32,((int32_t*)is->contents)+pos,sizeof(v32)); memrev32ifbe(&v32); return v32; } else { memcpy(&v16,((int16_t*)is->contents)+pos,sizeof(v16)); memrev16ifbe(&v16); return v16; } } /* Search for the position of "value". Return 1 when the value was found and * sets "pos" to the position of the value within the intset. Return 0 when * the value is not present in the intset and sets "pos" to the position * where "value" can be inserted. */ // 在 is 中查找 value,如果存在則返回1,pos爲值所在的位置 // 如果值不存在,則返回0,pos爲值value可插入的位置 static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) { int min = 0, max = intrev32ifbe(is->length)-1, mid = -1; int64_t cur = -1; /* The value can never be found when the set is empty */ if (intrev32ifbe(is->length) == 0) { if (pos) *pos = 0; return 0; } else { /* Check for the case where we know we cannot find the value, * but do know the insert position. */ if (value > _intsetGet(is,intrev32ifbe(is->length)-1)) { // 判斷值value是否大於集合is中的最大值 if (pos) *pos = intrev32ifbe(is->length); return 0; } else if (value < _intsetGet(is,0)) { // 判斷值value是否小於集合is中的最小值 if (pos) *pos = 0; return 0; } } // 二分查找法 查找value的位置或者適合插入的位置 while(max >= min) { // 通過 >> 1 進行除2操作 mid = ((unsigned int)min + (unsigned int)max) >> 1; cur = _intsetGet(is,mid); if (value > cur) { min = mid+1; } else if (value < cur) { max = mid-1; } else { break; } } if (value == cur) { if (pos) *pos = mid; return 1; } else { if (pos) *pos = min; return 0; } } /* Resize the intset */ // 爲整數集合is擴展數組空間,len爲指定的元素個數 static intset *intsetResize(intset *is, uint32_t len) { uint32_t size = len*intrev32ifbe(is->encoding); is = zrealloc(is,sizeof(intset)+size); return is; } // 在整數集合is中 從指定位置 from開始 拷貝 數據 到 to 位置 static void intsetMoveTail(intset *is, uint32_t from, uint32_t to) { void *src, *dst; uint32_t bytes = intrev32ifbe(is->length)-from; uint32_t encoding = intrev32ifbe(is->encoding); if (encoding == INTSET_ENC_INT64) { src = (int64_t*)is->contents+from; dst = (int64_t*)is->contents+to; bytes *= sizeof(int64_t); } else if (encoding == INTSET_ENC_INT32) { src = (int32_t*)is->contents+from; dst = (int32_t*)is->contents+to; bytes *= sizeof(int32_t); } else { src = (int16_t*)is->contents+from; dst = (int16_t*)is->contents+to; bytes *= sizeof(int16_t); } memmove(dst,src,bytes); } /* Set the value at pos, using the configured encoding. */ // 在is指定位置pos中將值修改爲value static void _intsetSet(intset *is, int pos, int64_t value) { uint32_t encoding = intrev32ifbe(is->encoding); if (encoding == INTSET_ENC_INT64) { ((int64_t*)is->contents)[pos] = value; memrev64ifbe(((int64_t*)is->contents)+pos); } else if (encoding == INTSET_ENC_INT32) { ((int32_t*)is->contents)[pos] = value; memrev32ifbe(((int32_t*)is->contents)+pos); } else { ((int16_t*)is->contents)[pos] = value; memrev16ifbe(((int16_t*)is->contents)+pos); } }
-
intset *intsetRemove(intset *is, int64_t value, int *success)
將值value
從整數集合*is
中刪除,如果成功,則*success
爲1,否則爲0:/* Delete integer from intset */ intset *intsetRemove(intset *is, int64_t value, int *success) { uint8_t valenc = _intsetValueEncoding(value); uint32_t pos; if (success) *success = 0; // 判斷值是否在整數編碼的範圍內,如果是的話,調用intsetSearch()查找出 value 的位置 if (valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,&pos)) { // value 存在,並且位置在 pos uint32_t len = intrev32ifbe(is->length); /* We know we can delete */ if (success) *success = 1; /* Overwrite value with tail and update length */ // 如果value不是在數組末位,則將pos+1位置後的數據往前移1位,覆蓋掉原本值value if (pos < (len-1)) intsetMoveTail(is,pos+1,pos); // 釋放掉多出一位的內存空間 is = intsetResize(is,len-1); // 長度減1 is->length = intrev32ifbe(len-1); } return is; }
整數集合API
參考之《Redis設計與實現》
函數 | 作用 |
---|---|
intset *intsetNew(void) | 創建一個新的整數集合 |
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) | 將給定元素value 添加到整數集合is 中,成功或者失敗的標記將會傳入success 中 |
intset *intsetRemove(intset *is, int64_t value, int *success) | 從整數集合is 中移除給定元素value ,成功或者失敗的標記將會傳入success 中 |
uint8_t intsetFind(intset *is, int64_t value) | 檢查給定元素value 是否在整數集合is 中 |
int64_t intsetRandom(intset *is) | 從整數集合is 中隨機返回一個元素 |
uint8_t intsetGet(intset *is, uint32_t pos, int64_t *value) | 取出整數集合is 底層數組中在給定索引pos 上的元素並寫入value 中,如果給定的索引pos 超出數組長度,函數返回0,否則返回1 |
uint32_t intsetLen(const intset *is) | 返回整數集合is 中的元素個數 |
size_t intsetBlobLen(intset *is) | 返回整數集合is 中佔用的內存字節數 |