redis集合的實現與 求交/並/差集
給新觀衆老爺的開場
大家好,我是弟弟!
最近讀了一遍 黃健宏大佬的 <<Redis 設計與實現>>,對Redis 3.0版本有了一些認識
該書作者有一版添加了註釋的 redis 3.0源碼
👉官方redis的github傳送門。
👉黃健宏大佬添加了註釋的 redis 3.0源碼傳送門
網上說Redis代碼寫得很好,爲了加深印象和學習redis大佬的代碼寫作藝術,瞭解工作中使用的redis 命令背後的源碼邏輯,便有了寫博客記錄學習redis源碼過程的想法。
redis集合(set)
redis 的集合是 整數類型,或者字符串 類型的無序集合,集合中的成員是唯一的。
rediis 集合有兩種實現方式,這取決於集合中的對象類型。
如果集合中的對象都是 int64範圍內的整數,那集合的實現方式就是整數集合,
否則集合的實現方式就是哈希表。
redis集合實現方式 之 整數集合
在創建一個集合數據類型時,redis會先判斷是否可以使用整數集合,如果可以的話,將使用整數集合。
整數集合結構一覽
typedef struct intset {
// 編碼方式
uint32_t encoding;
// 集合包含的元素數量
uint32_t length;
// 保存元素的數組
int8_t contents[];
} intset;
- encoding字段
intset支持的編碼方式
/* Note that these encodings are ordered, so:
* INTSET_ENC_INT16 < INTSET_ENC_INT32 < INTSET_ENC_INT64.
* intset 的編碼方式
*/
#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))
- length就是數組長度
- contents 字段
該結構中的 int8_t contents[], 僅僅作爲一個數組首地址使用
存儲的元素的實際類型取決於encoding
我們可以從 _intsetSet函數 (在指定整數集合的指定位置設置指定值) ,觀察到這一點
/* Set the value at pos, using the configured encoding.
* 根據集合的編碼方式,將底層數組在 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);
}
}
當加入整數集合的元素的變量的編碼類型大於整數集合的編碼類型是,將對該整數集合進行擴容。
/* Resize the intset
* 調整整數集合的內存空間大小
* 如果調整後的大小要比集合原來的大小要大,
* 那麼集合中原有元素的值不會被改變。
* 返回值:調整大小後的整數集合
*/
static intset *intsetResize(intset *is, uint32_t len) {
// 計算數組的空間大小
uint32_t size = len*intrev32ifbe(is->encoding);
// 根據空間大小,重新分配空間
// 注意這裏使用的是 zrealloc ,
// 所以如果新空間大小比原來的空間大小要大,
// 那麼數組原有的數據會被保留
is = zrealloc(is,sizeof(intset)+size);
return is;
}
整數集合是怎麼保證元素唯一的?
在 intsetAdd函數中時(往整數集合中添加一個元素),
會調用intsetSearch 來查找這個值應該放在哪個位置
如果找到了就直接返回了,
沒找到就從pos處到數據尾 整體 後一一位,將該值設置上去
redis 整數集合中存放的值是按從小到大順序排列的
查看 intsetSearch函數,可以看到二分查找
/* Search for the position of "value".
* 在集合 is 的底層數組中查找值 value 所在的索引。
* Return 1 when the value was found and
* sets "pos" to the position of the value within the intset.
* 成功找到 value 時,函數返回 1 ,並將 *pos 的值設爲 value 所在的索引。
* Return 0 when the value is not present in the intset
* and sets "pos" to the position where "value" can be inserted.
* 當在數組中沒找到 value 時,返回 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 */
// 處理 is 爲空時的情況
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. */
// 因爲底層數組是有序的,如果 value 比數組中最後一個值都要大
// 那麼 value 肯定不存在於集合中,
// 並且應該將 value 添加到底層數組的最末端
if (value > _intsetGet(is,intrev32ifbe(is->length)-1)) {
if (pos) *pos = intrev32ifbe(is->length);
return 0;
// 因爲底層數組是有序的,如果 value 比數組中最前一個值都要小
// 那麼 value 肯定不存在於集合中,
// 並且應該將它添加到底層數組的最前端
} else if (value < _intsetGet(is,0)) {
if (pos) *pos = 0;
return 0;
}
}
// 在有序數組中進行二分查找
// T = O(log N)
while(max >= min) {
mid = (min+max)/2;
cur = _intsetGet(is,mid);
if (value > cur) {
min = mid+1;
} else if (value < cur) {
max = mid-1;
} else {
break;
}
}
// 檢查是否已經找到了 value
if (value == cur) {
if (pos) *pos = mid;
return 1;
} else {
if (pos) *pos = min;
return 0;
}
}
redis集合實現方式之 哈希表
當集合成員是一個字符串,或者是超過了int64範圍的整數,或者整數集合中的成員數超過了 set_max_intset_entries (默認512) 時,redis將 使用哈希表作爲集合的實現數據結構。
一個set的 成員+空值 作爲一個鍵值對存到了哈希表裏。
關於哈希表的詳細信息之前博客有寫過,可以點擊下方鏈接。
上上篇博客傳送門-redis的哈希表
上篇博客傳送門-redis不穩定哈希表的遍歷
sadd 命令源碼邏輯
通過在命令表中搜索 “SADD” 命令,可以找到對應的處理命令 saddCommand。
源碼邏輯大致爲
-
從 c->db->dict 字典中,通過 集合名作爲key (c->argv[1]) 取出集合對象 set。
若不存在key,將set名字作爲key,創建出來的set對象作爲value,加入到c->db->dict字典中。 -
遍歷參數中的set成員將其加入set對象中
👇
void saddCommand(redisClient *c) {
robj *set;
int j, added = 0;
// 取出集合對象
set = lookupKeyWrite(c->db,c->argv[1]);
// 對象不存在,創建一個新的,並將它關聯到數據庫
if (set == NULL) {
set = setTypeCreate(c->argv[2]);
dbAdd(c->db,c->argv[1],set);
// 對象存在,檢查類型
}
...
// 將所有輸入元素添加到集合中
for (j = 2; j < c->argc; j++) {
c->argv[j] = tryObjectEncoding(c->argv[j]);
// 只有元素未存在於集合時,纔算一次成功添加
if (setTypeAdd(set,c->argv[j])) added++;
}
...
// 返回添加元素的數量
addReplyLongLong(c,added);
}
創建集合對象
若key不存在時,創建集合對象會根據 第一個成員的值,來判斷是否能創建整數集合,否則創建創建一個哈希表實現的set集合。
* 返回一個可以保存值 value 的集合。
* 當對象的值可以被編碼爲整數時,返回 intset ,
* 否則,返回普通的哈希表。
robj *setTypeCreate(robj *value) {
if (isObjectRepresentableAsLongLong(value,NULL) == REDIS_OK)
return createIntsetObject(); //創建一個整數集合
return createSetObject();//創建一個哈希表實現的set集合
}
* 創建一個 SET 編碼的集合對象
robj *createSetObject(void) {
dict *d = dictCreate(&setDictType,NULL);
robj *o = createObject(REDIS_SET,d);
o->encoding = REDIS_ENCODING_HT;
return o;
}
/* Sets type hash table */
dictType setDictType = {
dictEncObjHash, /* hash function */
NULL, /* key dup */
NULL, /* val dup */
dictEncObjKeyCompare, /* key compare */
dictRedisObjectDestructor, /* key destructor */
NULL /* val destructor */
};
/*
* 創建一個 INTSET 編碼的集合對象
*/
robj *createIntsetObject(void) {
intset *is = intsetNew();
robj *o = createObject(REDIS_SET,is);
o->encoding = REDIS_ENCODING_INTSET;
return o;
}
/* 創建並返回一個新的空整數集合*/
intset *intsetNew(void) {
// 爲整數集合結構分配空間
intset *is = zmalloc(sizeof(intset));
// 設置初始編碼
is->encoding = intrev32ifbe(INTSET_ENC_INT16);
// 初始化元素數量
is->length = 0;
return is;
}
求多個集合的交、並、差集
相信通過前面哈希表的文章各位觀衆老爺對集合元素的增刪改查應該沒有問題
那我們就來看下集合上的特色操作先看下求差集吧
求差集
衆所周知,求差集的命令長這樣,SDIFF FIRST_KEY OTHER_KEY1…OTHER_KEYN
返回的結果就是 FIRST_KEY裏 都沒有在後續集合中出現過的 成員們。
通過 sdiff命令我們找到了 t_set.c/sdiffCommand 函數
先描述下該函數跟取差集相關的大致流程
- 通過key取出各個set
- 對各個set一頓遍歷/查詢,得出所求的差集
觀衆老爺: “😤…”
對各個set一頓遍歷/查詢,求差集,有兩種不同的方式。
主要是考慮到不同數據量情況下,不同方式在不同條件下性能不同。
但本質都還是一樣的,看看FIRST_KEY中的元素是否都在別的集合中出現過,如果沒有就加入結果集。
在處理前先遍歷一遍所有元素,算出以下兩個值 👇
long long algo_one_work = 0, algo_two_work = 0;
// 遍歷所有集合
// sets[0]就是FIRST_KEY對應的集合,後續的依此類推
for (j = 0; j < setnum; j++) {
if (sets[j] == NULL) continue;
// 計算 setnum 乘以 sets[0] 的基數之積
algo_one_work += setTypeSize(sets[0]);
// 計算所有集合的基數之和
algo_two_work += setTypeSize(sets[j]);
}
- 在FIRST_KEY中的元素數量,相對其他集合較小時會採用第一種處理方式👇
/* Algorithm 1 has better constant times and performs less operations
* if there are elements in common. Give it some advantage. */
// 算法 1 的常數比較低,優先考慮算法 1
algo_one_work /= 2;
diff_algo = (algo_one_work <= algo_two_work) ? 1 : 2;
第一種處理方式:
流程如下
- 對第二個集合開始的後面所有集合,按集合元素個數從小到大排序
- 遍歷第一個集合中每個元素,在第二個及以後的集合中查找是否存在,不存在就加入結果集
- 遍歷完後結果集裏的元素就是差集
- 這就是爲什麼當第一個集合個數相對相對較小時使用這種方式,
明顯可以看到 這一波查找 遍歷到的次數是 N*M,N是第一個集合的元素個數,M是剩餘集合個數 - 其次將剩餘集合按元素大小從小到大排序的意義在於,
如果M裏的第一個集合就包含了元素,能最快發現並跳出循環,當然數據有各種各樣的反例。
只能說從平均情況來看,這樣是比較快的
(這個應該要從數學概率統計角度去給出一個完美的證明🙃️)
- 這就是爲什麼當第一個集合個數相對相對較小時使用這種方式,
當第一個集合的元素的個數較大時,第一種方式不太適合。
第二種處理方式
流程如下
- 先將第一個集合中的元素全部拿出來放入結果集。
- 遍歷剩餘集合的所有元素,從結果集中刪除剩餘集合中的所有元素
- 遍歷完後結果集裏的元素就是差集
交集/並集相信觀衆老爺也會求了,那今天就先到這裏。求差集源碼見文末
小結
- 集合有兩種實現方式,一種是整數集合,一種是哈希表
- 集合的交/並/差集這個功能,可以用來做點東西
比如交集兩個人的共同好友
差集推薦朋友認識的人,朋友的朋友認識的人,朋友的朋友的朋友…
並集這幾個人的朋友圈裏一共出現了多少個人 - set的元素是唯一的,也比較適合用來做點贊數,統計獨立ip這種事情
- 對於集合上的求交/並/差集也是可以直接存儲到一個新的集合對象的
往期博客回顧
- redis服務器的部分啓動過程
- GET命令背後的源碼邏輯
- redis的基礎數據結構之 sds
- redis的基礎數據結構之 list
- redis的基礎數據結構 之 ziplist
- redis 基礎數據結構之 hash表
- redis不穩定字典的遍歷
求差集源碼👇
void sunionDiffGenericCommand(redisClient *c, robj **setkeys, int setnum, robj *dstkey, int op) {
// 集合數組
robj **sets = zmalloc(sizeof(robj*)*setnum);
setTypeIterator *si;
robj *ele, *dstset = NULL;
int j, cardinality = 0;
int diff_algo = 1;
// 取出所有集合對象,並添加到集合數組中
for (j = 0; j < setnum; j++) {
robj *setobj = dstkey ?
lookupKeyWrite(c->db,setkeys[j]) :
lookupKeyRead(c->db,setkeys[j]);
// 不存在的集合當作 NULL 來處理
if (!setobj) {
sets[j] = NULL;
continue;
}
// 有對象不是集合,停止執行,進行清理
if (checkType(c,setobj,REDIS_SET)) {
zfree(sets);
return;
}
// 記錄對象
sets[j] = setobj;
}
/* Select what DIFF algorithm to use.
*
* 選擇使用那個算法來執行計算
*
* Algorithm 1 is O(N*M) where N is the size of the element first set
* and M the total number of sets.
*
* 算法 1 的複雜度爲 O(N*M) ,其中 N 爲第一個集合的基數,
* 而 M 則爲其他集合的數量。
*
* Algorithm 2 is O(N) where N is the total number of elements in all
* the sets.
*
* 算法 2 的複雜度爲 O(N) ,其中 N 爲所有集合中的元素數量總數。
*
* We compute what is the best bet with the current input here.
*
* 程序通過考察輸入來決定使用那個算法
*/
if (op == REDIS_OP_DIFF && sets[0]) {
long long algo_one_work = 0, algo_two_work = 0;
// 遍歷所有集合
for (j = 0; j < setnum; j++) {
if (sets[j] == NULL) continue;
// 計算 setnum 乘以 sets[0] 的基數之積
algo_one_work += setTypeSize(sets[0]);
// 計算所有集合的基數之和
algo_two_work += setTypeSize(sets[j]);
}
/* Algorithm 1 has better constant times and performs less operations
* if there are elements in common. Give it some advantage. */
// 算法 1 的常數比較低,優先考慮算法 1
algo_one_work /= 2;
diff_algo = (algo_one_work <= algo_two_work) ? 1 : 2;
if (diff_algo == 1 && setnum > 1) {
/* With algorithm 1 it is better to order the sets to subtract
* by decreasing size, so that we are more likely to find
* duplicated elements ASAP. */
// 如果使用的是算法 1 ,那麼最好對 sets[0] 以外的其他集合進行排序
// 這樣有助於優化算法的性能
qsort(sets+1,setnum-1,sizeof(robj*),
qsortCompareSetsByRevCardinality);
}
}
/* We need a temp set object to store our union. If the dstkey
* is not NULL (that is, we are inside an SUNIONSTORE operation) then
* this set object will be the resulting object to set into the target key
*
* 使用一個臨時集合來保存結果集,如果程序執行的是 SUNIONSTORE 命令,
* 那麼這個結果將會成爲將來的集合值對象。
*/
dstset = createIntsetObject();
// 執行的是並集計算
if (op == REDIS_OP_UNION) {
/* Union is trivial, just add every element of every set to the
* temporary set. */
// 遍歷所有集合,將元素添加到結果集裏就可以了
for (j = 0; j < setnum; j++) {
if (!sets[j]) continue; /* non existing keys are like empty sets */
si = setTypeInitIterator(sets[j]);
while((ele = setTypeNextObject(si)) != NULL) {
// setTypeAdd 只在集合不存在時,纔會將元素添加到集合,並返回 1
if (setTypeAdd(dstset,ele)) cardinality++;
decrRefCount(ele);
}
setTypeReleaseIterator(si);
}
// 執行的是差集計算,並且使用算法 1
} else if (op == REDIS_OP_DIFF && sets[0] && diff_algo == 1) {
/* DIFF Algorithm 1:
*
* 差集算法 1 :
*
* We perform the diff by iterating all the elements of the first set,
* and only adding it to the target set if the element does not exist
* into all the other sets.
*
* 程序遍歷 sets[0] 集合中的所有元素,
* 並將這個元素和其他集合的所有元素進行對比,
* 只有這個元素不存在於其他所有集合時,
* 纔將這個元素添加到結果集。
*
* This way we perform at max N*M operations, where N is the size of
* the first set, and M the number of sets.
*
* 這個算法執行最多 N*M 步, N 是第一個集合的基數,
* 而 M 是其他集合的數量。
*/
si = setTypeInitIterator(sets[0]);
while((ele = setTypeNextObject(si)) != NULL) {
// 檢查元素在其他集合是否存在
for (j = 1; j < setnum; j++) {
if (!sets[j]) continue; /* no key is an empty set. */
if (sets[j] == sets[0]) break; /* same set! */
if (setTypeIsMember(sets[j],ele)) break;
}
// 只有元素在所有其他集合中都不存在時,纔將它添加到結果集中
if (j == setnum) {
/* There is no other set with this element. Add it. */
setTypeAdd(dstset,ele);
cardinality++;
}
decrRefCount(ele);
}
setTypeReleaseIterator(si);
// 執行的是差集計算,並且使用算法 2
} else if (op == REDIS_OP_DIFF && sets[0] && diff_algo == 2) {
/* DIFF Algorithm 2:
*
* 差集算法 2 :
*
* Add all the elements of the first set to the auxiliary set.
* Then remove all the elements of all the next sets from it.
*
* 將 sets[0] 的所有元素都添加到結果集中,
* 然後遍歷其他所有集合,將相同的元素從結果集中刪除。
*
* This is O(N) where N is the sum of all the elements in every set.
*
* 算法複雜度爲 O(N) ,N 爲所有集合的基數之和。
*/
for (j = 0; j < setnum; j++) {
if (!sets[j]) continue; /* non existing keys are like empty sets */
si = setTypeInitIterator(sets[j]);
while((ele = setTypeNextObject(si)) != NULL) {
// sets[0] 時,將所有元素添加到集合
if (j == 0) {
if (setTypeAdd(dstset,ele)) cardinality++;
// 不是 sets[0] 時,將所有集合從結果集中移除
} else {
if (setTypeRemove(dstset,ele)) cardinality--;
}
decrRefCount(ele);
}
setTypeReleaseIterator(si);
/* Exit if result set is empty as any additional removal
* of elements will have no effect. */
if (cardinality == 0) break;
}
}
/* Output the content of the resulting set, if not in STORE mode */
// 執行的是 SDIFF 或者 SUNION
// 打印結果集中的所有元素
if (!dstkey) {
addReplyMultiBulkLen(c,cardinality);
// 遍歷並回復結果集中的元素
si = setTypeInitIterator(dstset);
while((ele = setTypeNextObject(si)) != NULL) {
addReplyBulk(c,ele);
decrRefCount(ele);
}
setTypeReleaseIterator(si);
decrRefCount(dstset);
// 執行的是 SDIFFSTORE 或者 SUNIONSTORE
} else {
/* If we have a target key where to store the resulting set
* create this key with the result set inside */
// 現刪除現在可能有的 dstkey
int deleted = dbDelete(c->db,dstkey);
// 如果結果集不爲空,將它關聯到數據庫中
if (setTypeSize(dstset) > 0) {
dbAdd(c->db,dstkey,dstset);
// 返回結果集的基數
addReplyLongLong(c,setTypeSize(dstset));
notifyKeyspaceEvent(REDIS_NOTIFY_SET,
op == REDIS_OP_UNION ? "sunionstore" : "sdiffstore",
dstkey,c->db->id);
// 結果集爲空
} else {
decrRefCount(dstset);
// 返回 0
addReply(c,shared.czero);
if (deleted)
notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"del",
dstkey,c->db->id);
}
signalModifiedKey(c->db,dstkey);
server.dirty++;
}
zfree(sets);
}