有序集合對象其實跟集合對象類似,只不過它多了一個score的參數,集合中的每個元素都有一個分值,在集合中元素是按照score排序的。
有序集合的底層編碼也是有兩種實現,壓縮列表REDIS_ENCODING_ZIPLIST以及跳躍表REDIS_ENCODING_SKIPLIST。和集合的一樣,有序集合的編碼方式是通過檢查第一個被加入的元素來決定的。
ZSET結構
/* RedisObject結構 */
typedef struct redisObject {
unsigned type:4; // OBJ_ZSET表示有序集合對象
unsigned encoding:4; // 編碼字段爲OBJ_ENCODING_ZIPLIST或OBJ_ENCODING_SKIPLIST
unsigned lru:LRU_BITS; // LRU_BITS爲24位
int refcount;
void *ptr; // 指向數據部分
} robj;
同其他的對象一樣, zset結構也是存儲在redisObject結構體中,通過指定 type= OBJ_ZSET 來確定這是一個有序集合對象,當是一個有序集合對象的時候,配套的endoding只能有對應的兩種取值。
ZIPLIST編碼的有序集合
當用REDIS_ENCODING_ZIPLIST作爲有序集合的底層編碼時,有序集合中的元素按score值從小到大排序,先保存value,在保存score,示意圖如下
|<-- element 1 -->|<-- element 2 -->|<-- ....... -->|
+---------+---------+--------+---------+--------+---------+---------+---------+
| ZIPLIST | | | | | | | ZIPLIST |
| ENTRY | member1 | score1 | member2 | score2 | ... | ... | ENTRY |
| HEAD | | | | | | | END |
+---------+---------+--------+---------+--------+---------+---------+---------+
SKIPLIST編碼的有序集合
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
當有序集合用REDIS_ENCODING_SKIPLIST編碼的時候,並不僅僅是用了跳躍表skiplist,還用到了字典dict。爲什麼要這麼做呢。 跳躍表的優點是能夠在O(log_N_)的期望時間內根據分值score對元素進行定位,對於一些範圍查找命令,比如ZRANGE能夠較好的支持。
但是如果要取出對應元素的分值,或者查看沒有某個元素是否在有序集合內,單純靠跳躍表就不夠用了, 因爲跳躍表是根據score組織的,不是根據元素值組織的。所以在有序集合中另外用了一個dict來支持這些操作。dict中key就是元素的值,value就是score。這樣就能夠在O(1)的複雜度內找到對應的分值,或者判斷一個元素是否在有序集合中。
ZSET編碼轉換
如果一個有序集合一開始是用壓縮列表REDIS_ENCODING_ZIPLIST作爲底層編碼,只要滿足下邊的條件,就會將底層編碼轉換爲REDIS_ENCODING_SKIPLIST:
- ziplist 所保存的元素數量超過服務器屬性 server.zset_max_ziplist_entries 的值(默認值爲 128 )
- 新添加元素的長度大於服務器屬性 server.zset_max_ziplist_value 的值(默認值爲 64 )
void zsetConvert(robj *zobj, int encoding) {
zset *zs;
zskiplistNode *node, *next;
sds ele;
double score;
// 如果已經是目標編碼格式,返回
if (zobj->encoding == encoding) return;
// 編碼格式從ZIPLIST轉成SKIPLIST
if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {
unsigned char *zl = zobj->ptr;
unsigned char *eptr, *sptr;
unsigned char *vstr;
unsigned int vlen;
long long vlong;
// 如果目標編碼格式不對,返回錯誤
if (encoding != OBJ_ENCODING_SKIPLIST)
serverPanic("Unknown target encoding");
// 創建新的zset,底層爲SKIPLIST編碼,所以需要兩個結構體:一個dict和一個skiplist
zs = zmalloc(sizeof(*zs));
zs->dict = dictCreate(&zsetDictType,NULL);
zs->zsl = zslCreate();
eptr = ziplistIndex(zl,0);
serverAssertWithInfo(NULL,zobj,eptr != NULL);
sptr = ziplistNext(zl,eptr);
serverAssertWithInfo(NULL,zobj,sptr != NULL);
// 循環遍歷ZIPLIST
while (eptr != NULL) {
// 獲取score
score = zzlGetScore(sptr);
serverAssertWithInfo(NULL,zobj,ziplistGet(eptr,&vstr,&vlen,&vlong));
// 獲得對應的元素的值
if (vstr == NULL)
ele = sdsfromlonglong(vlong);
else
ele = sdsnewlen((char*)vstr,vlen);
// 根據元素值和score新建node,並插入到dict中
node = zslInsert(zs->zsl,score,ele);
serverAssert(dictAdd(zs->dict,ele,&node->score) == DICT_OK);
zzlNext(zl,&eptr,&sptr);
}
// 將zobj指向新的zset
zfree(zobj->ptr);
zobj->ptr = zs;
zobj->encoding = OBJ_ENCODING_SKIPLIST;
} else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {
// 編碼格式從SKIPLIST轉換成ZIPLIST
unsigned char *zl = ziplistNew();
// 檢查目標編碼格式,錯誤退出
if (encoding != OBJ_ENCODING_ZIPLIST)
serverPanic("Unknown target encoding");
/* Approach similar to zslFree(), since we want to free the skiplist at
* the same time as creating the ziplist. */
zs = zobj->ptr;
// 釋放dict的空間
dictRelease(zs->dict);
// skiplist的頭節點空間
node = zs->zsl->header->level[0].forward;
// 釋放表頭
zfree(zs->zsl->header);
zfree(zs->zsl);
// 遍歷跳躍表
while (node) {
// 將元素ele和score添加到ziplist中
zl = zzlInsertAt(zl,NULL,node->ele,node->score);
next = node->level[0].forward;
zslFreeNode(node);
node = next;
}
// 釋放zs,並集鞥zobj指向新的zl
zfree(zs);
zobj->ptr = zl;
zobj->encoding = OBJ_ENCODING_ZIPLIST;
} else {
serverPanic("Unknown sorted set encoding");
}
}
ZSET命令
命令 | 說明 |
---|---|
ZADD key score member [[score member] [score member] …] | 將一個或多個 member 元素及其 score 值加入到有序集 key 當中 |
zcard | 返回有序集 key 的基數 |
ZCOUNT key min max | 返回有序集 key 中, score 值在 min 和 max 之間(默認包括 score 值等於 min 或 max )的成員的數量 |
ZINCRBY key increment member | 爲有序集 key 的成員 member 的 score 值加上增量 increment |
zrange | 返回有序集 key 中,指定區間內的成員 |
zrevrange | 返回有序集 key 中,指定區間內的成員 |
zrangeByScore | 返回有序集 key 中,所有 score 值介於 min 和 max 之間(包括等於 min 或 max )的成員 |
zrank | 返回有序集 key 中成員 member 的排名。其中有序集成員按 score 值遞增(從小到大)順序排列 |
zrevrank | 返回有序集 key 中成員 member 的排名。其中有序集成員按 score 值遞減(從大到小)排序 |
zrem | 移除有序集 key 中的一個或多個成員,不存在的成員將被忽略 |
zscore | 返回有序集 key 中,成員 member 的 score 值 |
ZSET命令實現
ZADD接口實現
區別於其他有多種底層編碼格式的實現(比如集合SET),有序集合不是在一個函數中區別不同的底層編碼來實現功能,而是分別搞了兩套機制,比如ZADD命令,有一個壓縮列表編碼的 zzlInsert 函數以及跳躍表編碼的 zslInsert 函數。在命令一進來的時候,就根據不同的編碼格式,調用不同的函數實現。
ZIPLIST的中插入接口
/* Insert (element,score) pair in ziplist. This function assumes the element is
* not yet present in the list. */
unsigned char *zzlInsert(unsigned char *zl, sds ele, double score) {
unsigned char *eptr = ziplistIndex(zl,0), *sptr;
double s;
// 循環遍歷ZIPLIST
while (eptr != NULL) {
sptr = ziplistNext(zl,eptr);
serverAssert(sptr != NULL);
// 獲取分值
s = zzlGetScore(sptr);
// 如果分值大於score,說明已經找到了要插入的位置
if (s > score) {
/* First element with score larger than score for element to be
* inserted. This means we should take its spot in the list to
* maintain ordering. */
// 在對應位置插入元素和score
zl = zzlInsertAt(zl,eptr,ele,score);
break;
} else if (s == score) {
/* Ensure lexicographical ordering for elements. */
// 如果分值相同,按字典序排列
if (zzlCompareElements(eptr,(unsigned char*)ele,sdslen(ele)) > 0) {
zl = zzlInsertAt(zl,eptr,ele,score);
break;
}
}
/* Move to next element. */
eptr = ziplistNext(zl,sptr);
}
/* Push on tail of list when it was not yet inserted. */
// 如果到了最後,說明前邊的score都比目標要小,直接在尾部插入
if (eptr == NULL)
zl = zzlInsertAt(zl,NULL,ele,score);
return zl;
}
SKIPLIST的插入接口在跳躍表那節介紹過,重新貼一下
/* Insert a new node in the skiplist. Assumes the element does not already
* exist (up to the caller to enforce that). The skiplist takes ownership
* of the passed SDS string 'ele'. */
// 跳躍表插入元素
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
unsigned int rank[ZSKIPLIST_MAXLEVEL];
int i, level;
serverAssert(!isnan(score)); // 判斷是否爲數字
x = zsl->header;
// 從最高的level, 也即跨度最大的level開始查找結點
for (i = zsl->level-1; i >= 0; i--) {
/* store rank that is crossed to reach the insert position */
// 當前是否是最高層, 如果是最高層,rank[i]=0,否則,複製上一層的數值
rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
// 如果當前結點的score值小於傳入的score 或者 當前score相等,但是結點的對象不相等
while (x->level[i].forward &&
(x->level[i].forward->score < score ||
(x->level[i].forward->score == score &&
sdscmp(x->level[i].forward->ele,ele) < 0)))
{
// 將當前一層的跨度加到rank[i]
rank[i] += x->level[i].span;
// 在當前層中向前查找
x = x->level[i].forward;
}
// 當前層位於插入位置前的結點x放入update數組
update[i] = x;
}
/* we assume the element is not already inside, since we allow duplicated
* scores, reinserting the same element should never happen since the
* caller of zslInsert() should test in the hash table if the element is
* already inside or not. */
// 隨機生成小於32的層數
level = zslRandomLevel();
// 如果生成的層數大於當前的層數
if (level > zsl->level) {
for (i = zsl->level; i < level; i++) {
// 設定rank數組中大於原level層以上的值爲0
// 同時設定update數組大於原level層以上的數據
rank[i] = 0;
update[i] = zsl->header;
update[i]->level[i].span = zsl->length;
}
zsl->level = level;
}
// 創建層數爲level的新結點
x = zslCreateNode(level,score,ele);
for (i = 0; i < level; i++) {
// 將每一層的前置結點的後續結點指向新結點, 同時設置新結點的後續結點
x->level[i].forward = update[i]->level[i].forward;
update[i]->level[i].forward = x;
/* update span covered by update[i] as x is inserted here */
// 更新每一層的前置結點和新結點的跨度
x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
update[i]->level[i].span = (rank[0] - rank[i]) + 1;
}
/* increment span for untouched levels */
for (i = level; i < zsl->level; i++) {
update[i]->level[i].span++;
}
// 根據最低層的前序結點是否是header結點來設置當前新結點的向後指針
x->backward = (update[0] == zsl->header) ? NULL : update[0];
if (x->level[0].forward)
x->level[0].forward->backward = x;
else
zsl->tail = x;
zsl->length++;
return x;
}