Redis 整數集合(intset)
1. 介紹
整數集合(intset)是集合鍵底層實現之一。集合鍵另一實現是值爲空的散列表(hash table),雖然使用散列表對集合的加入刪除元素,判斷元素是否存在等等操作時間複雜度爲O(1),但是當存儲的元素是整型且元素數目較少時,如果使用散列表存儲,就會比較浪費內存,因此整數集合(intset)類型因爲節約內存就存在。
在redis集合鍵命令:redis集合鍵命令詳解
127.0.0.1:6379> SADD set1 1 2 3
(integer) 3
127.0.0.1:6379> SADD set1 1
(integer) 0
127.0.0.1:6379> SMEMBERS set1
1) "1"
2) "2"
3) "3"
127.0.0.1:6379> SISMEMBER set1 2
(integer) 1
127.0.0.1:6379> SREM set1 1
(integer) 1
127.0.0.1:6379> SMEMBERS set1
1) "2"
2) "3"
2. 整數集合結構的實現
redis根目錄下的intset.h文件
typedef struct intset {
uint32_t encoding; //編碼格式,有如下三種格式,初始值默認爲INTSET_ENC_INT16
uint32_t length; //集合元素數量
int8_t contents[]; //保存元素的數組,元素類型並不一定是ini8_t類型,柔性數組不佔intset結構體大小,並且數組中的元素從小到大排列。
} intset; //整數集合結構
#define INTSET_ENC_INT16 (sizeof(int16_t)) //16位,2個字節,表示範圍-32,768~32,767
#define INTSET_ENC_INT32 (sizeof(int32_t)) //32位,4個字節,表示範圍-2,147,483,648~2,147,483,647
#define INTSET_ENC_INT64 (sizeof(int64_t)) //64位,8個字節,表示範圍-9,223,372,036,854,775,808~9,223,372,036,854,775,807
3. 升級
intset整數集合之所以有三種表示編碼格式的宏定義,是因爲根據存儲的元素數值大小,能夠選取一個最”合適”的類型存儲,”合適”可以理解爲:既能夠表示元素的大小,又可以節省空間。
因此,當新添加的元素,例如:65535,超過當前集合編碼格式所能表示的範圍,就要進行升級操作。
我們使用剛纔命令中的集合,它在結構如下圖:
我們根據代碼和圖一起理解升級的過程。
3.1獲得新元素的編碼格式
當前新元素要插入到集合中時,首先就要判獲得新元素的編碼格式,所以調用_intsetValueEncoding()來返回一個”適合”該元素的編碼格式。65535的最”適合”的編碼格式是INTSET_ENC_INT32。
/* Return the required encoding for the provided value. */
static uint8_t _intsetValueEncoding(int64_t v) { //返回合適v的編碼方式
if (v < INT32_MIN || v > INT32_MAX) //如果超出32位所能表示數值的範圍則返回INTSET_ENC_INT64
return INTSET_ENC_INT64;
else if (v < INT16_MIN || v > INT16_MAX) //如果超出16位所能表示數值的範圍則返回INTSET_ENC_INT32
return INTSET_ENC_INT32;
else
return INTSET_ENC_INT16; //否則返回用16位表示的INTSET_ENC_INT16
}
3.2 調整內存空間
當得到新元素的編碼格式後,就要將集合中所有元素的編碼格式都要變成升級後的編碼格式,因此,需要調整集合數組contents的內存空間大小,調用intsetResize()函數。
/* Resize the intset */
static intset *intsetResize(intset *is, uint32_t len) { //調整集合的內存空間大小
uint32_t size = len*intrev32ifbe(is->encoding); //計算數組的大小
is = zrealloc(is,sizeof(intset)+size);
//分配空間,如果新空間的大小比原來的空間大,那麼數組的元素會被保留
return is;
}
- intrev32ifbe()是一個宏定義,定義和實現在redis根目錄下的endianconv.h和endianconv.c中根據主機字節序用來做整數大小端的轉換。
已經獲知65535的編碼格式,因此調整內存空間的大小等於編碼格式的大小乘以集合元素的個數。如果圖:
注意:encoding成員已經發生變化,但是length並沒有更新。
3.3 根據編碼格式設置對應的值
調整好內存空間後就根據編碼格式來設置集合元素的值和最後將新元素添加到集合中,都調用_intsetSet()函數。
/* 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) { //如果是64位
((int64_t*)is->contents)[pos] = value; //設置下標pos的值爲value
memrev64ifbe(((int64_t*)is->contents)+pos); //如果需要轉換大小端
} else if (encoding == INTSET_ENC_INT32) { //如果是32位
((int32_t*)is->contents)[pos] = value; //設置下標pos的值爲value
memrev32ifbe(((int32_t*)is->contents)+pos); //如果需要轉換大小端
} else {
((int16_t*)is->contents)[pos] = value; //設置下標pos的值爲value
memrev16ifbe(((int16_t*)is->contents)+pos); //如果需要轉換大小端
}
}
- memrev16ifbe()是一個宏定義,定義和實現在redis根目錄下的endianconv.h和endianconv.c中根據主機字節序用來做內存大小端的轉換。
將集合中原來的元素和新插入的元素以”合適”的編碼格式INTSET_ENC_INT32寫到數組中,順序過程如下圖:
最後要更新length。
3.4 升級實現源碼
/* Upgrades the intset to a larger encoding and inserts the given integer. */
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) { //根據value的編碼方式,對整數集合is的編碼格式升級
uint8_t curenc = intrev32ifbe(is->encoding); //當前集合的編碼方式
uint8_t newenc = _intsetValueEncoding(value); //得到value合適的編碼方式
int length = intrev32ifbe(is->length); //集合元素數量
int prepend = value < 0 ? 1 : 0; //如果value小於0,則要將value添加到數組最前端,因此爲移動1個編碼長度
//集合的編碼格式要升級,也就是內存增大
//因爲 value 的編碼比集合原有的其他元素的編碼都要大,所以value如果是負數,就是最小值,如果是正數則是最大值
//索引value要麼放在數組集合的最前端,要麼最後端,根據prepend判斷
/* First set new encoding and resize */
is->encoding = intrev32ifbe(newenc); //更新集合is的編碼方式
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. */
//_intsetGetEncoded()得到下標爲length的值
//_intsetSet設置下標爲prepend+length的值爲_intsetGetEncoded返回的值
//但是,編碼格式已經發生改變,數組元素沒變但是內存大小改變
while(length--)
_intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
/* Set the value at the beginning or the end. */
if (prepend) //value是負數,要放在最前端
_intsetSet(is,0,value); //設置下標爲0的值爲value
else
_intsetSet(is,intrev32ifbe(is->length),value); //value爲正數,設置最末尾+1的值爲value
is->length = intrev32ifbe(intrev32ifbe(is->length)+1); //數組元素加1
return is;
}
3.5 升級的特點
- 提升靈活性:因爲C語言是靜態類型的語言,通常在在數組中只是用一種類型保存數據,例如,要麼只用int16_t類型,要麼只用int32_t類型。通過自動升級底層數組來適應不同類型的新元素,不必擔心類型的錯誤。
- 節約內存:整數集合既可以讓集合保存三種不同類型的值,又可以確保升級操作只在有需要的時候進行,這樣就節省了內存。
- 不支持降級:一旦對數組進行升級,編碼就會一直保存升級後的狀態。
4.整數集合的其他操作
源代碼註釋下載:redis源碼註釋
intset *intsetNew(void); //創建一個空集合
intset *intsetAdd(intset *is, int64_t value, uint8_t *success);//將value添加到is集合中,如果成功success被設置爲1否則爲0
intset *intsetRemove(intset *is, int64_t value, int *success);//從集合中刪除value,刪除成功success設置爲1,失敗爲0
uint8_t intsetFind(intset *is, int64_t value);//返回1表示value是集合中的元素,否則返回0
int64_t intsetRandom(intset *is);//隨機返回一個元素
uint8_t intsetGet(intset *is, uint32_t pos, int64_t *value);//獲得下標爲pos的值並保存在value中
uint32_t intsetLen(intset *is);//返回集合的元素個數
size_t intsetBlobLen(intset *is);//返回集合所佔用的字節總量
/* Return the required encoding for the provided value. */
static uint8_t _intsetValueEncoding(int64_t v) { //返回合適v的編碼方式
if (v < INT32_MIN || v > INT32_MAX) //如果超出32位所能表示數值的範圍則返回INTSET_ENC_INT64
return INTSET_ENC_INT64;
else if (v < INT16_MIN || v > INT16_MAX) //如果超出16位所能表示數值的範圍則返回INTSET_ENC_INT32
return INTSET_ENC_INT32;
else
return INTSET_ENC_INT16; //返回用16位表示的INTSET_ENC_INT16
}
/* Return the value at pos, given an encoding. */
static int64_t _intsetGetEncoded(intset *is, int pos, uint8_t enc) { //根據編碼方式enc,返回在集合is中下標爲pos的元素
int64_t v64;
int32_t v32;
int16_t v16;
if (enc == INTSET_ENC_INT64) { //64位編碼
memcpy(&v64,((int64_t*)is->contents)+pos,sizeof(v64)); //從下標pos開始的內存空間拷貝64bit的數據到v64
memrev64ifbe(&v64); //如果是大端序,就會轉換成小端序
return v64;
} else if (enc == INTSET_ENC_INT32) {//32位編碼
memcpy(&v32,((int32_t*)is->contents)+pos,sizeof(v32));//從下標pos開始的內存空間拷貝32bit的數據到v32
memrev32ifbe(&v32); //32位大小端轉換
return v32;
} else {//16位編碼
memcpy(&v16,((int16_t*)is->contents)+pos,sizeof(v16));//從下標pos開始的內存空間拷貝16bit的數據到v16
memrev16ifbe(&v16); //16位大小端轉換
return v16;
}
}
/* Return the value at pos, using the configured encoding. */
static int64_t _intsetGet(intset *is, int pos) { //根據集合is設置的編碼方式,返回下標爲pos的值
return _intsetGetEncoded(is,pos,intrev32ifbe(is->encoding));
//intrev32ifbe()函數返回參數的編碼格式並且根據需求轉換大小端
}
/* Set the value at pos, using the configured encoding. */
static void _intsetSet(intset *is, int pos, int64_t value) { //根據集合is設置的編碼方式,設置下標爲pos的值爲value
uint32_t encoding = intrev32ifbe(is->encoding); //獲取集合設置的編碼方式
if (encoding == INTSET_ENC_INT64) { //如果是64位
((int64_t*)is->contents)[pos] = value; //設置下標pos的值爲value
memrev64ifbe(((int64_t*)is->contents)+pos); //如果需要轉換大小端
} else if (encoding == INTSET_ENC_INT32) { //如果是32位
((int32_t*)is->contents)[pos] = value; //設置下標pos的值爲value
memrev32ifbe(((int32_t*)is->contents)+pos); //如果需要轉換大小端
} else {
((int16_t*)is->contents)[pos] = value; //設置下標pos的值爲value
memrev16ifbe(((int16_t*)is->contents)+pos); //如果需要轉換大小端
}
}
/* Create an empty intset. */
intset *intsetNew(void) { //創建一個空集合
intset *is = zmalloc(sizeof(intset)); //分配空間
is->encoding = intrev32ifbe(INTSET_ENC_INT16); //設置編碼方式
is->length = 0; //集合爲空
return is;
}
/* Resize the intset */
static intset *intsetResize(intset *is, uint32_t len) { //調整集合的內存空間大小
uint32_t size = len*intrev32ifbe(is->encoding); //計算數組的大小
is = zrealloc(is,sizeof(intset)+size); //分配空間,如果新空間的大小比原來的空間大,那麼數組的元素會被保留
return is;
}
/* 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; //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大於數組最大值
if (pos) *pos = intrev32ifbe(is->length); //可以將pos設置爲數組末尾
return 0;
} else if (value < _intsetGet(is,0)) { //如果小於數組的最小值
if (pos) *pos = 0; //pos可以是下標爲0的位置
return 0;
}
}
while(max >= min) { //有序集合中進行二分查找
mid = ((unsigned int)min + (unsigned int)max) >> 1; //(min+max)/2,找到中間數的下標
cur = _intsetGet(is,mid); //等到下標爲mid的值cur
if (value > cur) { //value大於當前值cur
min = mid+1; //後一半找
} else if (value < cur) { //value小於當前值cur
max = mid-1; //前一半找
} else {
break; //找到退出循環
}
}
if (value == cur) { //確認找到
if (pos) *pos = mid; //設置pos爲找到的位置,返回1
return 1;
} else {
if (pos) *pos = min; //此時min和max相等,所以pos可以設置爲min或max,返回0
return 0;
}
}
/* Upgrades the intset to a larger encoding and inserts the given integer. */
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) { //根據value的編碼方式,對整數集合is的編碼格式升級
uint8_t curenc = intrev32ifbe(is->encoding); //當前集合的編碼方式
uint8_t newenc = _intsetValueEncoding(value); //得到value合適的編碼方式
int length = intrev32ifbe(is->length); //集合元素數量
int prepend = value < 0 ? 1 : 0; //如果value小於0,則要將value添加到數組最前端,因此爲移動1個編碼長度
//集合的編碼格式要升級,也就是內存增大
//因爲 value 的編碼比集合原有的其他元素的編碼都要大,所以value如果是負數,就是最小值,如果是正數則是最大值
//索引value要麼放在數組集合的最前端,要麼最後端,根據prepend判斷
/* First set new encoding and resize */
is->encoding = intrev32ifbe(newenc); //更新集合is的編碼方式
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. */
//_intsetGetEncoded()得到下標爲length的值
//_intsetSet設置下標爲prepend+length的值爲_intsetGetEncoded返回的值
//但是,編碼格式已經發生改變,數組元素沒變但是內存大小改變
while(length--)
_intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
/* Set the value at the beginning or the end. */
if (prepend) //value是負數,要放在最前端
_intsetSet(is,0,value); //設置下標爲0的值爲value
else
_intsetSet(is,intrev32ifbe(is->length),value); //value爲正數,設置最末尾+1的值爲value
is->length = intrev32ifbe(intrev32ifbe(is->length)+1); //數組元素加1
return is;
}
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); //獲得集合is的默認編碼方式
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); //從src開始移動bytes個字節到dst
}
/* Insert an integer in the intset */
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {//將value添加到is集合中,如果成功success被設置爲1否則爲0
uint8_t valenc = _intsetValueEncoding(value); //獲得value適合的編碼類型
uint32_t pos;
if (success) *success = 1; //設置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)) { //如果value的編碼類型大於集合的編碼類型
/* This always succeeds, so we don't need to curry *success. */
return intsetUpgradeAndAdd(is,value); //升級集合,並且將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)) { //查找value,若果value已經存在,intsetSearch返回1,如果不存在,pos保存value可以插入的位置
if (success) *success = 0; //value存在,success設置爲0
return is;
}
//value在集合中不存在,且pos保存可以插入的位置
is = intsetResize(is,intrev32ifbe(is->length)+1); //調整集合大小
if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1); //如果pos不是在數組末尾則要移動調整集合
}
_intsetSet(is,pos,value); //設置pos下標的值爲value
is->length = intrev32ifbe(intrev32ifbe(is->length)+1); //集合節點數量加1
return is;
}
/* Delete integer from intset */
intset *intsetRemove(intset *is, int64_t value, int *success) { //從集合中刪除value,刪除成功success設置爲1,失敗爲0
uint8_t valenc = _intsetValueEncoding(value); //獲得value適合的編碼類型
uint32_t pos;
if (success) *success = 0; //設置success默認爲0
//如果value的編碼格式小於集合的編碼格式且value在集合中已存在,pos保存着下標
if (valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,&pos)) {
uint32_t len = intrev32ifbe(is->length); //備份當前集合元素數量
/* We know we can delete */
if (success) *success = 1; //刪除成功,設置success爲1
/* Overwrite value with tail and update length */
if (pos < (len-1)) intsetMoveTail(is,pos+1,pos); //如果不是最後一個元素,則移動元素覆蓋掉被刪除的元素
is = intsetResize(is,len-1); //縮小大小
is->length = intrev32ifbe(len-1); //更新集合元素個數
}
return is;
}
/* Determine whether a value belongs to this set */
uint8_t intsetFind(intset *is, int64_t value) { //返回1表示value是集合中的元素,否則返回0
uint8_t valenc = _intsetValueEncoding(value); //獲得value適合的編碼類型
return valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,NULL);
//如果value的編碼格式小於集合的編碼格式且value在集合中已存在,返回1,其中任何一個不成立返回0
}
/* Return random member */
int64_t intsetRandom(intset *is) { //隨機返回一個元素
return _intsetGet(is,rand()%intrev32ifbe(is->length)); //隨機生成一個下標,返回該下標的值
}
/* Sets the value to the value at the given position. When this position is
* out of range the function returns 0, when in range it returns 1. */
uint8_t intsetGet(intset *is, uint32_t pos, int64_t *value) { //獲得下標爲pos的值並保存在value中
if (pos < intrev32ifbe(is->length)) { //如果pos小於數組長度
*value = _intsetGet(is,pos); //返回pos下標的值,保存在value中
return 1;
}
return 0;
}
/* Return intset length */
uint32_t intsetLen(intset *is) { //返回集合的元素個數
return intrev32ifbe(is->length); //返回length成員
}
/* Return intset blob size in bytes. */
size_t intsetBlobLen(intset *is) { //返回集合所佔用的字節總量
return sizeof(intset)+intrev32ifbe(is->length)*intrev32ifbe(is->encoding); //編碼格式×元素個數+集合大小
}