Redis源码剖析(十三)整数集合
Redis提供一種叫整數集合的數據結構,當數據中只包含整數,并且數據數量不多時,Redis便會采用整數集合存儲
Redis保證整數集合有以下幾個特性
- 所含元素全是整數,且不重復
- 內部元素有序,通常是會從小到大排序
- 內部編碼統一,盡可能采用合適的編碼保存數據
- 當編碼不合適時,執行升級操作
接下來會針對上述幾個特性分別進行分析,可以看到,整數集合有點類似連續數組,只是在某種程度上添加了編碼,同時為了編碼統一,會有升級相關操作
命令格式
Redis提供SADD命令向數據庫中添加整數集合
127.0.0.1:6379> SADD digits 1 2 3 4 5 //向數據庫中添加整數集合 (integer) 5 127.0.0.1:6379> SMEMBERS digits //獲取整數集合digits的成員 1) "1" 2) "2" 3) "3" 4) "4" 5) "5" 127.0.0.1:6379> OBJECT ENCODING digits //獲取digits內部存儲結構 "intset" 127.0.0.1:6379>不過,有以下幾種情況Redis會更改底層的存儲結構,將整數集合改為哈希表
- 元素個數過多時
- 存在非整數元素時
當采用SADD向整數集合中添加元素,但是其中包含一個字符串類型數據時,那么再次獲取內部存儲結構時會發現返回的是”hashtable”而非”intset”
127.0.0.1:6379> SADD digits-str 1 2 3 a 4 5 //其中帶有字符串a (integer) 6 127.0.0.1:6379> SMEMBERS digits-str //獲取元素 1) "1" 2) "3" 3) "2" 4) "4" 5) "a" 6) "5" 127.0.0.1:6379> OBJECT ENCODING digits-str //獲取內部存儲結構,發現是哈希表 "hashtable" 127.0.0.1:6379>存儲結構
前面也提高過,整數集合很像連續數組,內部保存的是整數。但是Redis為整數集合添加了編碼的功能,也就是類型。可以根據元素大小,選擇合適的編碼來存儲,當然,是為了節約內存
Redis提供三種編碼,分別對應int16_t,int32_t,int64_t三種類型,對于采用int16_t就可以存儲的數據,采用int32_t或int64_t就顯得過于浪費了,這便是編碼的實際作用,采用最適合的類型保存數據
//intset.c #define INTSET_ENC_INT16 (sizeof(int16_t)) #define INTSET_ENC_INT32 (sizeof(int32_t)) #define INTSET_ENC_INT64 (sizeof(int64_t))整數集合的定義如下,其中保存著內部元素的編碼,元素個數,和元素數組
//intset.h /* 整數集合,用于節約內存 */ typedef struct intset {uint32_t encoding; //編碼,不同的整數大小(2, 4, 8字節)uint32_t length; //多少個數據int8_t contents[]; //數據 } intset;需要注意的是,整數集合中的所有數據的編碼都是一樣的,也就是說,如果其中一個元素的編碼要改變,所有元素的編碼都需要同時改變,這一點在后面添加元素時會看到
另外,雖然contents數組中的元素類型是int8_t類型,但是這并不代表數據是這個類型的。int8_t只是為了用最小的類型記錄數據,在存放數據時,一個數據可以同時占用多個int8_t(這是由于內部數據的地址空間是連續的)
整數集合相關操作
添加數據
添加數據功能由intsetAdd函數完成,函數內部首先判斷要添加的數據是否能被當前編碼保存,如果不能,則需要將整個集合的數據重新改寫編碼,也就是升級操作
//intset.c /* 在整數集中添加一個元素 */ /* 根據編碼長度判斷是否需要先執行升級操作再添加 */ intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {uint8_t valenc = _intsetValueEncoding(value);uint32_t pos;if (success) *success = 1;/* 如果要添加的數據無法被當前編碼保存,就需要升級操作 */if (valenc > intrev32ifbe(is->encoding)) {return intsetUpgradeAndAdd(is,value);} else {/* 如果value已在集合中,則不添加 */if (intsetSearch(is,value,&pos)) {if (success) *success = 0;return is;}/* 擴充集合,為新數據分配空間 */is = intsetResize(is,intrev32ifbe(is->length)+1);/* 為了保證集合中元素有序,需要執行移動操作 */if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);}/* 將新數據放在pos位置 */_intsetSet(is,pos,value);/* 更新集合數據個數 */is->length = intrev32ifbe(intrev32ifbe(is->length)+1);return is; }移動數據
intsetMoveTail函數用于將源下標開始的數據移動到目的下標處
//intset.c /* 將整數集合中從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,內存移動操作 */memmove(dst,src,bytes); }升級操作
如果不考慮升級操作,添加函數還是比較容易理解的。找到新數據的位置,然后移動元素,將新元素放到它的位置上,這些操作和數組的添加操作非常像。
前面說過,編碼的加入是為了節約內存占用,但是帶來的問題就是內部編碼統一,整個集合都需要采用相同的編碼保存數據,那么當一個數據無法被當前編碼保存時,就需要將整個集合的編碼升級,這就導致所有原有數據的編碼也要被改變
舉例來說,假設之前采用int16_t就可以保存所有數據,此時需要一個int32_t類型才能保存的數據,那么就需要將以前的數據都改為int32_t類型以保證編碼統一
編碼統一的原因是整數集合內部采用數組保存數據,每個數據的大小都必須是一樣的,這樣才可以通過偏移量(下標)來獲取數據
intsetUpgradeAndAdd函數先將集合編碼升級,然后再添加數據
//intset.c /* 將數據插入到整數集中,如果當前整數集的編碼不足以容納value,那么將整數集執行升級操作 */ /* 升級操作是將整數集的編碼加大,這需要將原有數據的編碼也進行加大 */ static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {/* 當前整數集的編碼 */uint8_t curenc = intrev32ifbe(is->encoding);/* 適合value的最小編碼 *//* 其實就是根據value的大小找到一個編碼使其不溢出 */uint8_t newenc = _intsetValueEncoding(value);/* 整數集中數據個數 */int length = intrev32ifbe(is->length);/* value為正,則在尾部插入,否則在頭部插入 */int prepend = value < 0 ? 1 : 0;/* 將整數集的編碼設置成新編碼 */is->encoding = intrev32ifbe(newenc);/* 多申請一個空間存放value,此時申請的空間是根據新編碼大小進行分配的 */is = intsetResize(is,intrev32ifbe(is->length)+1);/* 將原有數據進行調整,加大其編碼長度,同時改變在整數集的位置以便容納新元素 *//* _intsetGetEncoded函數根據給定編碼獲取整數集中的某個下標元素(此處通過源編碼找到以前的元素)* _intsetSet函數將給定元素添加到整數集的某個下標位置,根據當前編碼 */while(length--)_intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));/* 根據在頭部插入還是在尾部插入將value插進整數集中 */if (prepend)_intsetSet(is,0,value);else_intsetSet(is,intrev32ifbe(is->length),value);/* 更新元素個數 */is->length = intrev32ifbe(intrev32ifbe(is->length)+1);return is; }Redis的整數集合不支持降級操作,也就是一旦將編碼調高,就無法將其降低,這是沒有辦法的事情,因為如果要降級,就需要遍歷數據判斷是否需要降級,這個操作是十分耗時的
對象系統中的整數集合
整數集合在對象系統中作為集合的底層實現
//object.c /* 創建整數集合對象 */ robj *createIntsetObject(void) {intset *is = intsetNew();robj *o = createObject(OBJ_SET,is);o->encoding = OBJ_ENCODING_INTSET;return o; }小結
整數集合部分還是很容易理解的,實際上就是數組外套一個編碼,根據編碼統一適當進行升級操作。另外,整數集合作為集合的底層實現,保證了數據的有序性,無重復性,但是只適用于數據個數較少,且都是整數的情況,當數據個數很多,或者存在其他類型的數據(如字符串)時,Redis會采用hashtable作為集合的底層實現
總結
以上是生活随笔為你收集整理的Redis源码剖析(十三)整数集合的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Redis源码剖析(十二)有序集合跳表实
- 下一篇: 解决MySQL使用LOAD导入中文数据乱