redis有序集合的實現 以及 zrank-zadd-zrange的源碼邏輯
給新觀衆老爺的開場
大家好,我是弟弟!
最近讀了一遍 黃健宏大佬的 <<Redis 設計與實現>>,對Redis 3.0版本有了一些認識
該書作者有一版添加了註釋的 redis 3.0源碼
👉官方redis的github傳送門。
👉黃健宏大佬添加了註釋的 redis 3.0源碼傳送門
網上說Redis代碼寫得很好,爲了加深印象和學習redis大佬的代碼寫作藝術,瞭解工作中使用的redis 命令背後的源碼邏輯,便有了寫博客記錄學習redis源碼過程的想法。
redis 有序集合(zset)
redis的有序集合 跟 集合一樣成員是唯一的,跟集合不同的是每個成員都會關聯一個分數用來排序,分數可以重複。
redis有序集合的底層實現數據結構 有兩種,分別是 ziplist和zset。
這取決於有序集合中的成員數量或者 單個成員的key的長度
當有序集合成員數超過 zset_max_ziplist_entries(默認128),
或者成員的key的長度超過 zset_max_ziplist_value(默認64)時,
會將有序集合的實現方式從ziplist轉化爲zset。
在其他一些地方, 也可能從zset轉化成ziplist。
redis有序集合 第一種實現方式 ziplist
ziplist實現的有序集合,會將成員的key和分數拆成兩個緊挨着的ziplist元素,key在前,分數災後。
有序集合成員按 成員分數從小到大排列,
相同分數的有序集合成員,按照成員key的字典序從小到大排列。
ziplist的詳細內容可以參考往期博客 👇
往期博客 redis源碼閱讀 - 基礎數據結構 之 ziplist
redis有序集合 第二種實現方式 zset
zset實現的有序集合,包含了一個 k/v字典 和 一個 跳躍表
zset結構一覽 👇
* 有序集合
typedef struct zset
{
// 字典,鍵爲成員,值爲分值
// 用於支持 O(1) 複雜度的按成員取分值操作
dict *dict;
// 跳躍表,按分值排序成員
// 用於支持平均複雜度爲 O(log N) 的按分值定位成員操作
// 以及範圍操作
zskiplist *zsl;
} zset;
哈希表的詳細結構可參考往期博客
redis 基礎數據結構之 hash表
redis不穩定字典的遍歷
跳躍表
跳躍表可以簡單理解爲 是一個用鏈表實現的
使用二分查找思想來加速查詢,具有區間遍歷功能的數據結構
跳躍表裏有一個雙向鏈表,便於雙向遍歷。每個鏈表元素記錄了成員的key和分數
並且鏈表元素的順序是按分值從小到大排列,相同分值按key的字典序從小到大排列。
在每一個鏈表節點上,有一個層的概念,
在最大層數以下,每一層有兩個字段,
一個是該層的跨度,也就是在該層從當前節點n1,到節點n2 中間跨越了幾個元素
層越高,跨度越大
另一個就是指向節點n2的指針,叫當前層的前進指針
每個鏈表節點還有一個指向前一個鏈表的指針,叫後退指針
這樣不管是計算鏈表節點的排名,還是做區間遍歷,都能快速定位到需要查找的節點
因爲層越高跨度越大,從高層開始遍歷查找是,能經過很少的比較次數,就能快速定位到所要查找節點,在該層所在的區間,然後再一層一層遍歷下去,直到找到
跳躍表結構 與跳躍表節點結構一覽
/*
* 跳躍表
*/
typedef struct zskiplist
{
// 表頭節點和表尾節點
struct zskiplistNode *header, *tail;
// 表中節點的數量
unsigned long length;
// 表中層數最大的節點的層數
int level;
} zskiplist;
/* ZSETs use a specialized version of Skiplists */
/*
* 跳躍表節點
*/
typedef struct zskiplistNode
{
// 成員對象
robj *obj;
// 分值
double score;
// 後退指針,指向前一個跳躍表節點
struct zskiplistNode *backward;
// 層
struct zskiplistLevel
{
// 前進指針,指向跨越span個元素後的跳躍表節點
struct zskiplistNode *forward;
// 跨度
unsigned int span;
} level[];
} zskiplistNode;
在跳躍表上查找元素
在zset中單純的查找key的跳躍表節點,是通過字典,也就是hash表的查找來完成的
當在跳躍表上插入一個跳躍表節點,或者取跳躍表節點的排名等操作時,需要在跳躍表上進行。
查找單個元素,hash表的查詢效率一般是o(1)的,而跳躍表一般是o(logN)
ZRANK 在跳躍表上取節點的排名
zrank跳躍表相關的處理邏輯大致爲
- 通過有序集合的key,找到有序集合(僅討論zset)
- 通過成員名在zset的字典中查找,對應的key是否存在
- 存在,則返回字典中的value,也就是成員的分數
- 通過成員key和成員分數,遍歷跳躍表計算出排名
skiplist計算成員排名的邏輯大致爲
-
從跳躍表的頭節點的最高層開始遍歷
-
在每一層中,通過成員分數以及成員的key,來查找所在的區間
-
查找區間的方法爲,
當前層的前進指針指向的節點的分數3.1
若小於被查找成員分數,若分數相等,且key小於被查找成員的key
則在當前層中向前遍歷,累加當前節點的跨度記錄到rank變量中,
並將指針設爲前進指針3.2
否則,被查找成員落在該區域,層數減一,繼續遍歷 -
結束條件
4.1 找到被查找成員,返回rank值
4.2 層高小於0,沒查到,返回0skiplist的rank值是從1開始的,因爲有一個表頭節點,詳細在後面sadd相關的代碼邏輯裏能看到
讓我們來看下zrank取排名的源碼邏輯,zrank命令對應的處理函數是
void zrankCommand(redisClient *c) {
zrankGenericCommand(c, 0);
}
void zrankGenericCommand(redisClient *c, int reverse) {
robj *key = c->argv[1];
robj *ele = c->argv[2];
robj *zobj;
unsigned long llen;
unsigned long rank;
// 有序集合
if ((zobj = lookupKeyReadOrReply(c,key,shared.nullbulk)) == NULL ||
checkType(c,zobj,REDIS_ZSET)) return;
// 元素數量
llen = zsetLength(zobj);
...
if (zobj->encoding == REDIS_ENCODING_ZIPLIST) {//壓縮列表
...
} else if (zobj->encoding == REDIS_ENCODING_SKIPLIST) {//跳躍表
zset *zs = zobj->ptr;
zskiplist *zsl = zs->zsl;
dictEntry *de;
double score;
// 從字典中取出元素
ele = c->argv[2] = tryObjectEncoding(c->argv[2]);
de = dictFind(zs->dict,ele);
if (de != NULL) {
// 取出元素的分值
score = *(double*)dictGetVal(de);
// 在跳躍表中計算該元素的排位
rank = zslGetRank(zsl,score,ele);
redisAssertWithInfo(c,ele,rank); /* Existing elements always have a rank. */
// ZRANK 還是 ZREVRANK ?
if (reverse)
addReplyLongLong(c,llen-rank);
else
addReplyLongLong(c,rank-1);
} else {
addReply(c,shared.nullbulk);
}
} else {
redisPanic("Unknown sorted set encoding");
}
}
unsigned int zsetLength(robj *zobj) {
int length = -1;
if (zobj->encoding == REDIS_ENCODING_ZIPLIST) {
length = zzlLength(zobj->ptr);
} else if (zobj->encoding == REDIS_ENCODING_SKIPLIST) {
length = ((zset*)zobj->ptr)->zsl->length;
} else {
redisPanic("Unknown sorted set encoding");
}
return length;
}
/* Find the rank for an element by both score and key.
*
* 查找包含給定分值和成員對象的節點在跳躍表中的排位。
*
* Returns 0 when the element cannot be found, rank otherwise.
*
* 如果沒有包含給定分值和成員對象的節點,返回 0 ,否則返回排位。
*
* Note that the rank is 1-based due to the span of zsl->header to the
* first element.
*
* 注意,因爲跳躍表的表頭也被計算在內,所以返回的排位以 1 爲起始值。
*
* T_wrost = O(N), T_avg = O(log N)
*/
unsigned long zslGetRank(zskiplist *zsl, double score, robj *o) {
zskiplistNode *x;
unsigned long rank = 0;
int i;
// 遍歷整個跳躍表
x = zsl->header;
for (i = zsl->level-1; i >= 0; i--) {
// 遍歷節點並對比元素
while (x->level[i].forward &&
(x->level[i].forward->score < score ||
// 比對分值
(x->level[i].forward->score == score &&
// 比對成員對象
compareStringObjects(x->level[i].forward->obj,o) <= 0))) {
// 累積跨越的節點數量
rank += x->level[i].span;
// 沿着前進指針遍歷跳躍表
x = x->level[i].forward;
}
/* x might be equal to zsl->header, so test if obj is non-NULL */
// 必須確保不僅分值相等,而且成員對象也要相等
// T = O(N)
if (x->obj && equalStringObjects(x->obj,o)) {
return rank;
}
}
// 沒找到
return 0;
}
ZADD 在跳躍表中加入一個成員(key+分數)
在跳躍表中插入一個成員的邏輯大概如下
-
首先要找到在何處插入成員,查找方法跟zrank查找的方法一樣
-
該成員從最高層 maxlevel 到最底層 minlevel,每一層都存在一個該分數對應的區間,
這點沒有問題吧,可以想想爲什麼🙃️
遍歷過程中會記錄 相應區間左邊的那個節點 記在update[level]中,
因爲後續會用來更新部分受影響的跳躍表的區間 -
在該成員被插入後,會給該成員隨機產生一個層數 newlevel,
若newlevel > maxlevel
將跳躍表表頭節點的層數從 maxlevel 升到 newlevel ,
並且將 maxlevel 升到 newlevel 的每一層的 span設置爲當前跳躍表元素的個數
並記錄每層需要被更新的節點 update[level] = zsl->header 跳躍表表頭節點newlevel的隨機生成機制,讓越大的層數產生的機率越小。
這樣讓跳躍表在高層具有較大的區間跨度,從高層往底層,區間跨度相對越來越小,用來加速查找。 -
從最底層minlevel,到newlevel,將每一層的update[level]修正
4.1
每層包含了新插入節點的區間會被一分爲二
也就是 update[level] 到 update[level].forward 這一個區間會被一份爲二4.2
並且會修正 minlevel 到 newlevel每層update[level]節點的span值,因爲區間被一分爲二,span值可能會受影響。
若newlevel小於maxlevel,將newlevel到maxlevel的update[level]的span值+14.3
並且會修正minlevel 到 newlevel 每層update[level], update[level].forward,以及新節點的 前/後指針如果沒有這些騷操作,往跳躍表中插入了一個成員會怎樣?
🙃️ 那不就成裸的鏈表了嗎
來看下在跳躍表中插入一個成員的源碼邏輯
/*
* 創建一個成員爲 obj ,分值爲 score 的新節點,
* 並將這個新節點插入到跳躍表 zsl 中。
*
* 函數的返回值爲新節點。
*
* T_wrost = O(N^2), T_avg = O(N log N)
*/
#define ZSKIPLIST_MAXLEVEL 32 /* Should be enough for 2^32 elements */
zskiplistNode *zslInsert(zskiplist *zsl, double score, robj *obj) {
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
unsigned int rank[ZSKIPLIST_MAXLEVEL];
int i, level;
redisAssert(!isnan(score));
// 在各個層查找節點的插入位置
// T_wrost = O(N^2), T_avg = O(N log N)
x = zsl->header;
for (i = zsl->level-1; i >= 0; i--) {
/* store rank that is crossed to reach the insert position */
// 如果 i 不是 zsl->level-1 層
// 那麼 i 層的起始 rank 值爲 i+1 層的 rank 值
// 各個層的 rank 值一層層累積
// 最終 rank[0] 的值加一就是新節點的前置節點的排位
// rank[0] 會在後面成爲計算 span 值和 rank 值的基礎
rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
// 沿着前進指針遍歷跳躍表
// T_wrost = O(N^2), T_avg = O(N log N)
while (x->level[i].forward &&
(x->level[i].forward->score < score ||
// 比對分值
(x->level[i].forward->score == score &&
// 比對成員, T = O(N)
compareStringObjects(x->level[i].forward->obj,obj) < 0))) {
// 記錄沿途跨越了多少個節點
rank[i] += x->level[i].span;
// 移動至下一指針
x = x->level[i].forward;
}
// 記錄將要和新節點相連接的節點
update[i] = x;
}
/* we assume the key is not already inside, since we allow duplicated
* scores, and the re-insertion of score and redis object should never
* happen since the caller of zslInsert() should test in the hash table
* if the element is already inside or not.
*
* zslInsert() 的調用者會確保同分值且同成員的元素不會出現,
* 所以這裏不需要進一步進行檢查,可以直接創建新元素。
*/
// 獲取一個隨機值作爲新節點的層數
// T = O(N)
level = zslRandomLevel();
// 如果新節點的層數比表中其他節點的層數都要大
// 那麼初始化表頭節點中未使用的層,並將它們記錄到 update 數組中
// 將來也指向新節點
if (level > zsl->level) {
// 初始化未使用層
// T = O(1)
for (i = zsl->level; i < level; i++) {
rank[i] = 0;
update[i] = zsl->header;
update[i]->level[i].span = zsl->length;
}
// 更新表中節點最大層數
zsl->level = level;
}
// 創建新節點
x = zslCreateNode(level,score,obj);
// 將前面記錄的指針指向新節點,並做相應的設置
// T = O(1)
for (i = 0; i < level; i++) {
// 設置新節點的 forward 指針
x->level[i].forward = update[i]->level[i].forward;
// 將沿途記錄的各個節點的 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]);
// 更新新節點插入之後,沿途節點的 span 值
// 其中的 +1 計算的是新節點
update[i]->level[i].span = (rank[0] - rank[i]) + 1;
}
/* increment span for untouched levels */
// 未接觸的節點的 span 值也需要增一,這些節點直接從表頭指向新節點
// T = O(1)
for (i = level; i < zsl->level; i++) {
update[i]->level[i].span++;
}
// 設置新節點的後退指針
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;
}
/* Returns a random level for the new skiplist node we are going to create.
*
* 返回一個隨機值,用作新跳躍表節點的層數。
*
* The return value of this function is between 1 and ZSKIPLIST_MAXLEVEL
* (both inclusive), with a powerlaw-alike distribution where higher
* levels are less likely to be returned.
*
* 返回值介乎 1 和 ZSKIPLIST_MAXLEVEL 之間(包含 ZSKIPLIST_MAXLEVEL),
* 根據隨機算法所使用的冪次定律,越大的值生成的機率越小。
*
* T = O(N)
*/
#define ZSKIPLIST_P 0.25 /* Skiplist P = 1/4 */
int zslRandomLevel(void) {
int level = 1;
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
/*
* 創建一個層數爲 level 的跳躍表節點,
* 並將節點的成員對象設置爲 obj ,分值設置爲 score 。
*
* 返回值爲新創建的跳躍表節點
*
* T = O(1)
*/
zskiplistNode *zslCreateNode(int level, double score, robj *obj) {
// 分配空間
zskiplistNode *zn = zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));
// 設置屬性
zn->score = score;
zn->obj = obj;
return zn;
}
到這裏我們就瞭解到了在跳躍表中取排名,已經插入成員的源碼邏輯。
zadd命令就是對這些功能的包裝函數
若zadd添加的是一個不存在的成員,會在跳躍表中插入該成員節點
若是已存在的節點,會先刪除該成員節點,再重新插入該成員節點。
刪除成員節點的邏輯,差不多就是插入的邏輯的逆過程
來讓我們看下zadd與跳躍表相關的源碼邏輯👇
void zaddCommand(redisClient *c) {
zaddGenericCommand(c,0);
}
/* This generic command implements both ZADD and ZINCRBY. */
void zaddGenericCommand(redisClient *c, int incr) {
static char *nanerr = "resulting score is not a number (NaN)";
robj *key = c->argv[1];
robj *ele;
robj *zobj;
robj *curobj;
double score = 0, *scores = NULL, curscore = 0.0;
int j, elements = (c->argc-2)/2;
int added = 0, updated = 0;
// 輸入的 score - member 參數必須是成對出現的
if (c->argc % 2) {
addReply(c,shared.syntaxerr);
return;
}
/* Start parsing all the scores, we need to emit any syntax error
* before executing additions to the sorted set, as the command should
* either execute fully or nothing at all. */
// 取出所有輸入的 score 分值
scores = zmalloc(sizeof(double)*elements);
for (j = 0; j < elements; j++) {
if (getDoubleFromObjectOrReply(c,c->argv[2+j*2],&scores[j],NULL)
!= REDIS_OK) goto cleanup;
}
/* Lookup the key and create the sorted set if does not exist. */
// 取出有序集合對象
zobj = lookupKeyWrite(c->db,key);
if (zobj == NULL) {
// 有序集合不存在,創建新有序集合
if (server.zset_max_ziplist_entries == 0 ||
server.zset_max_ziplist_value < sdslen(c->argv[3]->ptr))
{
zobj = createZsetObject();
} else {
zobj = createZsetZiplistObject();
}
// 關聯對象到數據庫
dbAdd(c->db,key,zobj);
} else {
// 對象存在,檢查類型
if (zobj->type != REDIS_ZSET) {
addReply(c,shared.wrongtypeerr);
goto cleanup;
}
}
// 處理所有元素
for (j = 0; j < elements; j++) {
score = scores[j];
// 有序集合爲 ziplist 編碼
if (zobj->encoding == REDIS_ENCODING_ZIPLIST) {
...
// 有序集合爲 SKIPLIST 編碼
} else if (zobj->encoding == REDIS_ENCODING_SKIPLIST) {
zset *zs = zobj->ptr;
zskiplistNode *znode;
dictEntry *de;
// 編碼對象
ele = c->argv[3+j*2] = tryObjectEncoding(c->argv[3+j*2]);
// 查看成員是否存在
de = dictFind(zs->dict,ele);
if (de != NULL) {
// 成員存在
// 取出成員
curobj = dictGetKey(de);
// 取出分值
curscore = *(double*)dictGetVal(de);
// ZINCRYBY 時執行
...
/* Remove and re-insert when score changed. We can safely
* delete the key object from the skiplist, since the
* dictionary still has a reference to it. */
// 執行 ZINCRYBY 命令時,
// 或者用戶通過 ZADD 修改成員的分值時執行
if (score != curscore) {
// 刪除原有元素
redisAssertWithInfo(c,curobj,zslDelete(zs->zsl,curscore,curobj));
// 重新插入元素
znode = zslInsert(zs->zsl,score,curobj);
incrRefCount(curobj); /* Re-inserted in skiplist. */
// 更新字典的分值指針
dictGetVal(de) = &znode->score; /* Update score ptr. */
server.dirty++;
updated++;
}
} else {
// 元素不存在,直接添加到跳躍表
znode = zslInsert(zs->zsl,score,ele);
incrRefCount(ele); /* Inserted in skiplist. */
// 將元素關聯到字典
redisAssertWithInfo(c,NULL,dictAdd(zs->dict,ele,&znode->score) == DICT_OK);
incrRefCount(ele); /* Added to dictionary. */
server.dirty++;
added++;
}
} else {
redisPanic("Unknown sorted set encoding");
}
}
if (incr) /* ZINCRBY */
addReplyDouble(c,score);
else /* ZADD */
addReplyLongLong(c,added);
cleanup:
...
}
/*
* 創建一個 SKIPLIST 編碼的有序集合
*/
robj *createZsetObject(void) {
zset *zs = zmalloc(sizeof(*zs));
robj *o;
zs->dict = dictCreate(&zsetDictType,NULL);
zs->zsl = zslCreate();
o = createObject(REDIS_ZSET,zs);
o->encoding = REDIS_ENCODING_SKIPLIST;
return o;
}
/*
* 創建並返回一個新的跳躍表
*
* T = O(1)
*/
zskiplist *zslCreate(void) {
int j;
zskiplist *zsl;
// 分配空間
zsl = zmalloc(sizeof(*zsl));
// 設置高度和起始層數
zsl->level = 1;
zsl->length = 0;
// 初始化表頭節點
// T = O(1)
zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);
for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {
zsl->header->level[j].forward = NULL;
zsl->header->level[j].span = 0;
}
zsl->header->backward = NULL;
// 設置表尾
zsl->tail = NULL;
return zsl;
}
zskiplistNode *zslCreateNode(int level, double score, robj *obj) {
// 分配空間
zskiplistNode *zn = zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));
// 設置屬性
zn->score = score;
zn->obj = obj;
return zn;
}
ZRANGE 取top N到 top M之間的成員
爲什麼redis選擇跳躍表而不是平衡二叉樹來實現有序集合
平衡二叉樹中有代表性的如 AVL樹,紅黑樹。
跳躍表相比於紅黑樹這種數據結構,衆所周知的優點就是,crud性能可以媲美紅黑樹,
數據結構簡單,實現難度大大小於紅黑樹。
那麼除了跳躍表簡單好實現以外,還有什麼別的特殊的原因讓redis選擇了跳躍表而不是紅黑樹?
對於這個問題的解答,如果redis大佬已經回答過的話,那直接看他怎麼回答是最合適的了😂
原文鏈接
redis的有序集合上經常有區間遍歷操作。通過跳躍表加速查找到區間頭,然後在鏈表上進行區間遍歷,性能和平衡二叉樹差不多。
疑問? 平衡二叉樹似乎也能按中序遍歷的順序,將各個節點連接起來,形成一條有序的鏈表,似乎也可行。
但是這種爲了遍歷而串成的鏈表,似乎沒有跳躍表的鏈表來的自然 🤔
ZRANGE 與跳躍表相關的源碼邏輯
源碼邏輯大概如下
- 一段操作,檢查參數,修正參數,算出start, end
- 通過跳躍表加速查找到 start節點的位置
- 從start 節點開始,遍歷鏈表
源碼如下👇
void zrangeCommand(redisClient *c) {
zrangeGenericCommand(c,0);
}
void zrangeGenericCommand(redisClient *c, int reverse) {
robj *key = c->argv[1];
robj *zobj;
int withscores = 0;
long start;
long end;
int llen;
int rangelen;
// 取出 start 和 end 參數
if ((getLongFromObjectOrReply(c, c->argv[2], &start, NULL) != REDIS_OK) ||
(getLongFromObjectOrReply(c, c->argv[3], &end, NULL) != REDIS_OK)) return;
// 確定是否顯示分值
if (c->argc == 5 && !strcasecmp(c->argv[4]->ptr,"withscores")) {
withscores = 1;
} else if (c->argc >= 5) {
addReply(c,shared.syntaxerr);
return;
}
// 取出有序集合對象
if ((zobj = lookupKeyReadOrReply(c,key,shared.emptymultibulk)) == NULL
|| checkType(c,zobj,REDIS_ZSET)) return;
/* Sanitize indexes. */
// 將負數索引轉換爲正數索引
llen = zsetLength(zobj);
if (start < 0) start = llen+start;
if (end < 0) end = llen+end;
if (start < 0) start = 0;
/* Invariant: start >= 0, so this test will be true when end < 0.
* The range is empty when start > end or start >= length. */
// 過濾/調整索引
if (start > end || start >= llen) {
addReply(c,shared.emptymultibulk);
return;
}
if (end >= llen) end = llen-1;
rangelen = (end-start)+1;
/* Return the result in form of a multi-bulk reply */
addReplyMultiBulkLen(c, withscores ? (rangelen*2) : rangelen);
if (zobj->encoding == REDIS_ENCODING_ZIPLIST) {
...
} else if (zobj->encoding == REDIS_ENCODING_SKIPLIST) {
zset *zs = zobj->ptr;
zskiplist *zsl = zs->zsl;
zskiplistNode *ln;
robj *ele;
/* Check if starting point is trivial, before doing log(N) lookup. */
// 迭代的方向
if (reverse) {
ln = zsl->tail;
if (start > 0)
ln = zslGetElementByRank(zsl,llen-start);
} else {
ln = zsl->header->level[0].forward;
if (start > 0)
ln = zslGetElementByRank(zsl,start+1);
}
// 取出元素
while(rangelen--) {
redisAssertWithInfo(c,zobj,ln != NULL);
ele = ln->obj;
addReplyBulk(c,ele);
if (withscores)
addReplyDouble(c,ln->score);
ln = reverse ? ln->backward : ln->level[0].forward;
}
} else {
redisPanic("Unknown sorted set encoding");
}
}
小結
- 有序集合中,成員是唯一的,但分數可以重複
- 有序集合中按分數從小到大,當分數相同按成員字典順序從小到大排列
- 有序集合中有一個雙向鏈表,所以可以雙向遍歷
- 有序集合適合用來做排行榜之類的功能,當然如果單個有序集合成員數過多,佔用的內存也會很大。
- 有序集合在3.0版本中,最高32層