Redis內部資料結構詳解之整數集合(intset)
整數集合簡介
整數集合intset用於有序、無重複地儲存多個整數值,根據集合中元素的值自動選擇使用整數型別來儲存元素,例如:如果intset中絕對值最大的整數可以用int32_t來儲存,那麼整個intset中所有元素都使用int32_t來儲存。
如果當前intset所使用的型別不能儲存一個即將加入到該intset的新元素時候,需要對intset進行升級,比如新元素的型別是int64_t,而當前intset的型別是int32_t,那麼升級就是先將intset中所有元素由int32_t轉換為int64_t,然後再插入新元素。
對於int8_t,int32_t,int64_t我個人的理解就應該分別對應char,int,long long,使用int8_t,int32_t,int64_t應該是為了區分平臺的差異吧,具體的可以檢視stdint.h檔案。
整數集合的資料結構
typedef struct intset {
uint32_t encoding; //所使用型別的長度,4\8\16
uint32_t length; //元素個數
int8_t contents[]; //儲存元素的陣列
} intset;
encoding的值是下面三個常量中的一個:
#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))
contents陣列用來實際儲存資料,陣列中元素的特性:無重複元素;元素在陣列中遞增排列。
整數集合相關API介紹
函式名稱 |
作用 |
複雜度 |
_intsetValueEncoding |
獲取給定整數的編碼型別 |
O(1) |
_intsetGet |
根據索引獲取整數值 |
O(1) |
_intsetSet |
根據索引設定給定整數值 |
O(1) |
intsetNew |
新建intset |
O(1) |
intsetResize |
為給定的intset重新分配記憶體 |
O(1) |
intsetSearch |
查詢給定的整數是否在intset中 |
O(logN) |
intsetUpgradeAndAdd |
先升級intset然後插入元素 |
O(N) |
intsetAdd |
直接新增元素 |
O(N) |
intsetMoveTail |
將intset中元素偏移 |
O(N) |
intsetRemove |
刪除元素 |
O(N) |
intsetRandom |
隨機返回一個intset中元素 |
O(1) |
intsetLen |
intset中元素的個數 |
O(1) |
intsetBlobLen |
intset所佔的位元組數 |
O(1) |
重要API原始碼的簡單解析
intsetAdd
//新增一個整數
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
uint8_t valenc = _intsetValueEncoding(value); //得到型別的長度
uint32_t pos;
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. */
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. */
//如果該值在集合中已經存在,那麼直接返回
if (intsetSearch(is,value,&pos)) {
if (success) *success = 0;
return is;
}
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;
}
intsetAdd函式新增一個元素value時,首先根據value的位元組數與當前intset的encoding進行比較,分析intset是否需要升級,若需要升級則呼叫intsetUpdateAndAdd函式處理,否則如果value已存在intset中直接pass,不存在,那麼先resize,接著將插入位置之後的所有元素向後偏移,新增value。
intsetMoveTail
/**使用memmove對集合進行向後偏移,下標從0開始,並且已經Resize
例:前 | 1 | 2 | 3 | 4 | 5 | 6 | | |
from = 1, to = 3
length = 6
src = | 2 | 3 | 4 | 5 | 6 |
dst = | 4 | 5 | 6 | | |
bytes = 5 * sizeof(...)
後 | 1 | 2 | 3 | 2 | 3 | 4 | 5 | 6 |
偏移之前肯定需要用intsetResize函式,進行擴容,增加兩個容量
如果不理解前後的變化,建議檢視memmove原始碼,這裡需要考慮到記憶體覆蓋的問題
也就是為什麼必須使用memmove而不能使用memcpy的原因
*/
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);
}
intsetUpdateAndAdd
//對編碼型別進行升級,O(n)
//需要插入的值,要麼比當前集合中的最大值大,要麼比集合中的最小值小,不然不需要升級
//比最大值大還是小,只需要根據value的正負即可判斷
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
uint8_t curenc = intrev32ifbe(is->encoding); //當前編碼型別
uint8_t newenc = _intsetValueEncoding(value);//新的編碼型別
int length = intrev32ifbe(is->length);
int prepend = value < 0 ? 1 : 0;//決定新的值插入的位置(1表示頭,0表示尾)
/* First set new encoding and resize */
is->encoding = intrev32ifbe(newenc); //設定編碼型別
is = intsetResize(is,intrev32ifbe(is->length)+1);//resize
/* 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. */
//通過_intsetGetEncoded得到升級前的該位置的整數值
//設定原來的整數集的值,如果prepend=1表示新值在頭插入,那麼原來的數值全部向後偏移
while(length--)
_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);
is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
return is;
}
intsetRemove
//刪除一個整數
intset *intsetRemove(intset *is, int64_t value, int *success) {
uint8_t valenc = _intsetValueEncoding(value);
uint32_t pos;
if (success) *success = 0;
//value在原集合中
if (valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,&pos)) {
uint32_t len = intrev32ifbe(is->length);
/* We know we can delete */
if (success) *success = 1;
/* Overwrite value with tail and update length */
//如果 pos 不是 is 的最末尾,直接通過memmove記憶體覆蓋的方式刪除該整數值
//如果是末尾,直接resize刪除
if (pos < (len-1)) intsetMoveTail(is,pos+1,pos);
is = intsetResize(is,len-1);//將空間縮小
is->length = intrev32ifbe(len-1);
}
return is;
}
intset新增元素流程圖
小結
intset用於有序、無重複地儲存多個整數值,它會根據元素的值,自動選擇該用什麼長度的整數型別來儲存元素;
當新增新元素時,需要判斷當前intset的編碼型別能否儲存新元素,如果不行需要對intset進行升級,升級後的intset中的元素會擴大其佔有的位元組數,但是值不發生改變;
intset只支援升級,不支援降級,因此相對而言會浪費記憶體;
intset中元素是有序排列的,因此使用折半查詢的時間複雜度為O(logN)。
最後感謝黃健巨集(huangz1990)的Redis設計與實現及其他對Redis2.6原始碼的相關注釋對我在研究Redis2.8原始碼方面的幫助。