從之前的章節“Redis源碼剖析–對象Object” 可以知道,redis中的SET(集合)有兩種可能的數據存儲方式。分別是整數集合REDIS_ENCODING_INTSET和哈希表REDIS_ENCODING_HT。
robj *setTypeCreate(sds value) {
if (isSdsRepresentableAsLongLong(value,NULL) == C_OK)
return createIntsetObject();
return createSetObject();
}
第一個添加到集合的元素決定了初始的存儲方式,如果第一個原始是一個整數,初始的編碼就會是REDIS_ENCODING_INTSET,否則就是REDIS_ENCODING_HT,所以集合中的數據操作的時候基本都涉及到兩個存儲方式的判斷。
SET的結構
typedef struct redisObject {
unsigned type:4; // 此字段爲OBJ_SET
unsigned encoding:4; // 如果是set結構,編碼爲OBJ_ENCODING_INTSET或OBJ_ENCODING_HT
unsigned lru:LRU_BITS;
int refcount;
void *ptr;
} robj;
同其他的對象一樣, set結構也是存儲在redisObject結構體中,通過指定 type=OBJ_SET 來確定這是一個集合對象,當是一個集合對象的時候,配套的endoding只能有對應的兩種取值。
SET命令
命令 | 說明 |
---|---|
sadd | 將一個或多個 member 元素加入到集合 key 當中,已經存在於集合的 member 元素將被忽略 |
scard | 返回集合 key 的基數(集合中元素的數量) |
spop | 移除並返回集合中的一個隨機元素 |
smembers | 返回集合 key 中的所有成員 |
sismember | 判斷 member 元素是否集合 key 的成員 |
sinter | 返回多個集合的交集,多個集合由 keys 指定 |
srandmember | 返回集合中的一個隨機元素 |
srandmember | 返回集合中的 count 個隨機元素 |
srem | 移除集合 key 中的一個或多個 member 元素,不存在的 member 元素會被忽略 |
sunion | 返回多個集合的並集,多個集合由 keys 指定 |
sdiff | 返回一個集合的全部成員,該集合是所有給定集合之間的差集 |
SET命令實現
SET編碼轉換
如果一個集合使用 REDIS_ENCODING_INTSET 編碼, 那麼當以下任何一個條件被滿足時, 這個集合會被轉換成 REDIS_ENCODING_HT 編碼:
- intset 保存的整數值個數超過
- server.set_max_intset_entries (默認值爲 512 )。
試圖往集合裏添加一個新元素,並且這個元素不能被表示爲 long long 類型(也即是,它不是一個整數)。
/* Convert the set to specified encoding. The resulting dict (when converting
* to a hash table) is presized to hold the number of elements in the original
* set. */
void setTypeConvert(robj *setobj, int enc) {
setTypeIterator *si;
serverAssertWithInfo(NULL,setobj,setobj->type == OBJ_SET &&
setobj->encoding == OBJ_ENCODING_INTSET);
if (enc == OBJ_ENCODING_HT) {
int64_t intele;
dict *d = dictCreate(&setDictType,NULL);
sds element;
/* 提前擴張字典結構的大小,避免出現rehash過程 */
dictExpand(d,intsetLen(setobj->ptr));
/* To add the elements we extract integers and create redis objects */
si = setTypeInitIterator(setobj); // 初始化迭代器
// 循環遍歷intset,以及加入到dict中
while (setTypeNext(si,&element,&intele) != -1) {
element = sdsfromlonglong(intele);
serverAssert(dictAdd(d,element,NULL) == DICT_OK);
}
setTypeReleaseIterator(si); // 釋放迭代器
// 設定轉換後的編碼類型
setobj->encoding = OBJ_ENCODING_HT;
zfree(setobj->ptr);
setobj->ptr = d; // 指針指向新創建的dict
} else {
// 如果要轉換的類型不是OBJ_ENCODING_HT,退出
serverPanic("Unsupported set conversion");
}
}
當SET用HT編碼的時候,所有元素其實是保存在字典的鍵裏邊,而字典的值全都設置成了NULL,這樣就能保證集合中所有元素的唯一性。
SADD接口實現
/* Add the specified value into a set.
*
* If the value was already member of the set, nothing is done and 0 is
* returned, otherwise the new element is added and 1 is returned. */
// 插入元素到集合,如果元素已存在,返回0, 插入成功返回1
int setTypeAdd(robj *subject, sds value) {
long long llval;
// 如果是HT編碼結構,在dict中填加元素
if (subject->encoding == OBJ_ENCODING_HT) {
dict *ht = subject->ptr;
dictEntry *de = dictAddRaw(ht,value,NULL);
if (de) {
dictSetKey(ht,de,sdsdup(value));
dictSetVal(ht,de,NULL);
return 1;
}
} else if (subject->encoding == OBJ_ENCODING_INTSET) {
// 如果是整數集合編碼結構,判斷新加的元素是否能夠用long long表示
if (isSdsRepresentableAsLongLong(value,&llval) == C_OK) {
// 如果是整數結構,在intset中插入整數
uint8_t success = 0;
subject->ptr = intsetAdd(subject->ptr,llval,&success);
if (success) {
/* Convert to regular set when the intset contains
* too many entries. */
// 插入成功之後,判斷是否超出範圍,如果超過了set_max_intset_entries,需要做結構轉換
if (intsetLen(subject->ptr) > server.set_max_intset_entries)
setTypeConvert(subject,OBJ_ENCODING_HT);
return 1;
}
} else {
// 如果要插入的不是整數,先將原先的intset做結構轉換,然後執行插入
/* Failed to get integer from object, convert to regular set. */
setTypeConvert(subject,OBJ_ENCODING_HT);
/* The set *was* an intset and this value is not integer
* encodable, so dictAdd should always work. */
serverAssert(dictAdd(subject->ptr,sdsdup(value),NULL) == DICT_OK);
return 1;
}
} else {
serverPanic("Unknown set encoding");
}
return 0;
}
往SET中插入的時候,需要先判斷當前的SET的底層結構,如果是HT格式,直接調用dict的插入命令;如果是intset結構,需要先判斷要插入的新元素是否也是整數,如果不是整數,需要將SET的編碼格式轉換爲HT結構體,再執行插入,如果是整數,正常插入之後也要檢查一遍是否超過了intset的最大設置。
其他的比如移除元素之類的操作,也是類似,需要考慮在不同的編碼格式下的處理方案。
集合命令的實現
SET相比較其他格式的結構,多了集合命令的實現,就是我們所熟知的集合的交差並集。這邊講一下求交集的算法。
求交集算法
/* Iterate all the elements of the first (smallest) set, and test
* the element against all the other sets, if at least one set does
* not include the element it is discarded */
// 獲取第一個(基數最小的)集合,循環遍歷
si = setTypeInitIterator(sets[0]);
while((encoding = setTypeNext(si,&elesds,&intobj)) != -1) {
for (j = 1; j < setnum; j++) {
if (sets[j] == sets[0]) continue;
// 如果底層編碼是INTSET
if (encoding == OBJ_ENCODING_INTSET) {
/* intset with intset is simple... and fast */
if (sets[j]->encoding == OBJ_ENCODING_INTSET &&
!intsetFind((intset*)sets[j]->ptr,intobj))
{
break;
/* in order to compare an integer with an object we
* have to use the generic function, creating an object
* for this */
} else if (sets[j]->encoding == OBJ_ENCODING_HT) {
elesds = sdsfromlonglong(intobj);
if (!setTypeIsMember(sets[j],elesds)) {
sdsfree(elesds);
break;
}
sdsfree(elesds);
}
} else if (encoding == OBJ_ENCODING_HT) {
// 如果底層編碼HT
if (!setTypeIsMember(sets[j],elesds)) {
break;
}
}
}
/* Only take action when all sets contain the member */
// 如果j==setNum,說明已經檢查到了最後,所有的set都有這個元素
if (j == setnum) {
if (!dstkey) {
if (encoding == OBJ_ENCODING_HT)
addReplyBulkCBuffer(c,elesds,sdslen(elesds));
else
addReplyBulkLongLong(c,intobj);
cardinality++;
} else {
if (encoding == OBJ_ENCODING_INTSET) {
elesds = sdsfromlonglong(intobj);
setTypeAdd(dstset,elesds);
sdsfree(elesds);
} else {
setTypeAdd(dstset,elesds);
}
}
}
}
setTypeReleaseIterator(si);
// 如果目標key存在,保存結果
if (dstkey) {
/* Store the resulting set into the target, if the intersection
* is not an empty set. */
int deleted = dbDelete(c->db,dstkey);
// 如果最終的結果set非空, 將最終set結果放到dstkey中
if (setTypeSize(dstset) > 0) {
dbAdd(c->db,dstkey,dstset);
addReplyLongLong(c,setTypeSize(dstset));
notifyKeyspaceEvent(NOTIFY_SET,"sinterstore",
dstkey,c->db->id);
} else {
// 如果結果集合爲空,刪除
decrRefCount(dstset);
addReply(c,shared.czero);
if (deleted)
notifyKeyspaceEvent(NOTIFY_GENERIC,"del",
dstkey,c->db->id);
}
signalModifiedKey(c->db,dstkey);
server.dirty++;
} else {
setDeferredSetLen(c,replylen,cardinality);
}
zfree(sets);
}
通俗的來講,求交集主要分爲以下幾個步驟:
- 將所有的集合按照基數進行排序
- 使用基數最小的集合作爲結果集
- 遍歷這個基數最小集合中的每個元素,檢查這個元素在剩餘的其他集合中是否存在
- 如果都存在,就將這個元素加入到最終的結果集中
算法的複雜度是O(N2)。