有序集SortedSet算是redis中一個很有特色的資料結構,通過這篇文章來總結一下這塊知識點。
原文地址:www.jianshu.com/p/75ca5a359…
一、有序集SortedSet命令簡介
redis中的有序集,允許使用者使用指定值對放進去的元素進行排序,並且基於該已排序的集合提供了一系列豐富的操作集合的API。
舉例如下:
//新增元素,table1為有序集的名字,100為用於排序欄位(redis把它叫做score),a為我們要儲存的元素
127.0.0.1:6379> zadd table1 100 a
(integer) 1
127.0.0.1:6379> zadd table1 200 b
(integer) 1
127.0.0.1:6379> zadd table1 300 c
(integer) 1
//按照元素索引返回有序集中的元素,索引從0開始
127.0.0.1:6379> zrange table1 0 1
1) "a"
2) "b"
//按照元素排序範圍返回有序集中的元素,這裡用於排序的欄位在redis中叫做score
127.0.0.1:6379> zrangebyscore table1 150 400
1) "b"
2) "c"
//刪除元素
127.0.0.1:6379> zrem table1 b
(integer) 1複製程式碼
在有序集中,用於排序的值叫做score,實際儲存的值叫做member。
由於有序集中提供的API較多,這裡只舉了幾個常見的,具體可以參考redis文件。
關於有序集,我們有一個十分常見的使用場景就是使用者評論。在APP或者網站上釋出一條訊息,下面會有很多評論,通常展示是按照發布時間倒序排列,這個需求就可以使用有序集,以釋出評論的時間戳作為score,然後按照展示評論的數量倒序查詢有序集。
二、有序集SortedSet命令原始碼分析
老規矩,我們還是從server.c檔案中的命令表中找到相關命令的處理函式,然後一一分析。
依舊從新增元素開始,zaddCommand函式:
void zaddCommand(client *c) {
zaddGenericCommand(c,ZADD_NONE);
}複製程式碼
這裡可以看到流程轉向了zaddGenericCommand,並且傳入了一個模式標記。
關於SortedSet的操作模式這裡簡單說明一下,先來看一條完整的zadd命令:
zadd key [NX|XX] [CH] [INCR] score member [score member ...]複製程式碼
其中的可選項我們依次看下:
- NX表示如果元素存在,則不執行替換操作直接返回。
- XX表示只操作已存在的元素。
- CH表示返回修改(包括新增,更新)元素的數量,只能被ZADD命令使用。
- INCR表示在原來的score基礎上加上新的score,而不是替換。
上面程式碼片段中的ZADD_NONE表示普通操作。
接下來看下zaddGenericCommand函式的原始碼,很長,耐心一點點看:
void zaddGenericCommand(client *c, int flags) {
//一條錯誤提示資訊
static char *nanerr = "resulting score is not a number (NaN)";
//有序集名字
robj *key = c->argv[1];
robj *zobj;
sds ele;
double score = 0, *scores = NULL;
int j, elements;
int scoreidx = 0;
//記錄元素操作個數
int added = 0;
int updated = 0;
int processed = 0;
//查詢score的位置,預設score在位置2上,但由於有各種模式,所以需要判斷
scoreidx = 2;
while(scoreidx < c->argc) {
char *opt = c->argv[scoreidx]->ptr;
//判斷命令中是否設定了各種模式
if (!strcasecmp(opt,"nx")) flags |= ZADD_NX;
else if (!strcasecmp(opt,"xx")) flags |= ZADD_XX;
else if (!strcasecmp(opt,"ch")) flags |= ZADD_CH;
else if (!strcasecmp(opt,"incr")) flags |= ZADD_INCR;
else break;
scoreidx++;
}
//設定模式
int incr = (flags & ZADD_INCR) != 0;
int nx = (flags & ZADD_NX) != 0;
int xx = (flags & ZADD_XX) != 0;
int ch = (flags & ZADD_CH) != 0;
//通過上面的解析,scoreidx為真實的初始score的索引位置
//這裡客戶端引數數量減去scoreidx就是剩餘所有元素的數量
elements = c->argc - scoreidx;
//由於有序集中score,member成對出現,所以加一層判斷
if (elements % 2 || !elements) {
addReply(c,shared.syntaxerr);
return;
}
//這裡計算score,member有多少對
elements /= 2;
//引數合法性校驗
if (nx && xx) {
addReplyError(c,
"XX and NX options at the same time are not compatible");
return;
}
//引數合法性校驗
if (incr && elements > 1) {
addReplyError(c,
"INCR option supports a single increment-element pair");
return;
}
//這裡開始解析score,先初始化scores陣列
scores = zmalloc(sizeof(double)*elements);
for (j = 0; j < elements; j++) {
//填充陣列,這裡注意元素是成對出現,所以各個score之間要隔一個member
if (getDoubleFromObjectOrReply(c,c->argv[scoreidx+j*2],&scores[j],NULL)
!= C_OK) goto cleanup;
}
//這裡首先在client對應的db中查詢該key,即有序集
zobj = lookupKeyWrite(c->db,key);
if (zobj == NULL) {
//沒有指定有序集且模式為XX(只操作已存在的元素),直接返回
if (xx) goto reply_to_client;
//根據元素數量選擇不同的儲存結構初始化有序集
if (server.zset_max_ziplist_entries == 0 ||
server.zset_max_ziplist_value < sdslen(c->argv[scoreidx+1]->ptr))
{
//雜湊表 + 跳錶的組合模式
zobj = createZsetObject();
} else {
//ziplist(壓縮連結串列)模式
zobj = createZsetZiplistObject();
}
//加入db中
dbAdd(c->db,key,zobj);
} else {
//如果ZADD操作的集合型別不對,則返回
if (zobj->type != OBJ_ZSET) {
addReply(c,shared.wrongtypeerr);
goto cleanup;
}
}
//這裡開始往有序集中新增元素
for (j = 0; j < elements; j++) {
double newscore;
//取出client傳過來的score
score = scores[j];
int retflags = flags;
//取出與之對應的member
ele = c->argv[scoreidx+1+j*2]->ptr;
//向有序集中新增元素,引數依次是有序集,要新增的元素的score,要新增的元素,操作模式,新的score
int retval = zsetAdd(zobj, score, ele, &retflags, &newscore);
//新增失敗則返回
if (retval == 0) {
addReplyError(c,nanerr);
goto cleanup;
}
//記錄操作
if (retflags & ZADD_ADDED) added++;
if (retflags & ZADD_UPDATED) updated++;
if (!(retflags & ZADD_NOP)) processed++;
//設定新score值
score = newscore;
}
//操作記錄
server.dirty += (added+updated);
//返回邏輯
reply_to_client:
if (incr) {
if (processed)
addReplyDouble(c,score);
else
addReply(c,shared.nullbulk);
} else {
addReplyLongLong(c,ch ? added+updated : added);
}
//清理邏輯
cleanup:
zfree(scores);
if (added || updated) {
signalModifiedKey(c->db,key);
notifyKeyspaceEvent(NOTIFY_ZSET,
incr ? "zincr" : "zadd", key, c->db->id);
}
}複製程式碼
程式碼有點長,來張圖看一下儲存結構:
注:每個entry都是由score+member組成
有了上面的結構圖以後,可以想到刪除操作應該就是根據不同的儲存結構進行,如果是ziplist就執行連結串列刪除,如果是雜湊表+跳錶結構,那就要把兩個集合都進行刪除。真實邏輯是什麼呢?
我們來看下刪除函式zremCommand的原始碼,相對短一點:
void zremCommand(client *c) {
//獲取有序集名
robj *key = c->argv[1];
robj *zobj;
int deleted = 0, keyremoved = 0, j;
//做校驗
if ((zobj = lookupKeyWriteOrReply(c,key,shared.czero)) == NULL ||
checkType(c,zobj,OBJ_ZSET)) return;
for (j = 2; j < c->argc; j++) {
//一次刪除指定元素
if (zsetDel(zobj,c->argv[j]->ptr)) deleted++;
//如果有序集中全部元素都被刪除,則回收有序表
if (zsetLength(zobj) == 0) {
dbDelete(c->db,key);
keyremoved = 1;
break;
}
}
//同步操作
if (deleted) {
notifyKeyspaceEvent(NOTIFY_ZSET,"zrem",key,c->db->id);
if (keyremoved)
notifyKeyspaceEvent(NOTIFY_GENERIC,"del",key,c->db->id);
signalModifiedKey(c->db,key);
server.dirty += deleted;
}
//返回
addReplyLongLong(c,deleted);
}複製程式碼
看下具體的刪除操作原始碼:
//引數zobj為有序集,ele為要刪除的元素
int zsetDel(robj *zobj, sds ele) {
//與新增元素相同,根據不同的儲存結構執行不同的刪除邏輯
if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {
unsigned char *eptr;
//ziplist是一個簡單的連結串列刪除節點操作
if ((eptr = zzlFind(zobj->ptr,ele,NULL)) != NULL) {
zobj->ptr = zzlDelete(zobj->ptr,eptr);
return 1;
}
} else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {
zset *zs = zobj->ptr;
dictEntry *de;
double score;
de = dictUnlink(zs->dict,ele);
if (de != NULL) {
//查詢該元素的score
score = *(double*)dictGetVal(de);
//從雜湊表中刪除元素
dictFreeUnlinkedEntry(zs->dict,de);
//從跳錶中刪除元素
int retval = zslDelete(zs->zsl,score,ele,NULL);
serverAssert(retval);
//如果有需要則對雜湊表進行resize操作
if (htNeedsResize(zs->dict)) dictResize(zs->dict);
return 1;
}
} else {
serverPanic("Unknown sorted set encoding");
}
//沒有找到指定元素返回0
return 0;
}複製程式碼
最後看一個查詢函式zrangeCommand原始碼,也是很長,汗~~~,不過放心,有了上面的基礎,大致也能猜到查詢邏輯應該是什麼樣子的:
void zrangeCommand(client *c) {
//第二個引數,0表示順序,1表示倒序
zrangeGenericCommand(c,0);
}
void zrangeGenericCommand(client *c, int reverse) {
//有序集名
robj *key = c->argv[1];
robj *zobj;
int withscores = 0;
long start;
long end;
int llen;
int rangelen;
//引數校驗
if ((getLongFromObjectOrReply(c, c->argv[2], &start, NULL) != C_OK) ||
(getLongFromObjectOrReply(c, c->argv[3], &end, NULL) != C_OK)) return;
//根據引數附加資訊判斷是否需要返回score
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,OBJ_ZSET)) return;
//索引值重置
llen = zsetLength(zobj);
if (start < 0) start = llen+start;
if (end < 0) end = llen+end;
if (start < 0) start = 0;
//返回空集
if (start > end || start >= llen) {
addReply(c,shared.emptymultibulk);
return;
}
if (end >= llen) end = llen-1;
rangelen = (end-start)+1;
//返回給客戶端結果長度
addReplyMultiBulkLen(c, withscores ? (rangelen*2) : rangelen);
//同樣是根據有序集的不同結構執行不同的查詢邏輯
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 (reverse)
eptr = ziplistIndex(zl,-2-(2*start));
else
eptr = ziplistIndex(zl,2*start);
serverAssertWithInfo(c,zobj,eptr != NULL);
sptr = ziplistNext(zl,eptr);
while (rangelen--) {
serverAssertWithInfo(c,zobj,eptr != NULL && sptr != NULL);
//注意巢狀的ziplistGet方法就是把eptr索引的值讀出來儲存在後面三個引數中
serverAssertWithInfo(c,zobj,ziplistGet(eptr,&vstr,&vlen,&vlong));
//返回value
if (vstr == NULL)
addReplyBulkLongLong(c,vlong);
else
addReplyBulkCBuffer(c,vstr,vlen);
//如果需要則返回score
if (withscores)
addReplyDouble(c,zzlGetScore(sptr));
//倒序從後往前,正序從前往後
if (reverse)
zzlPrev(zl,&eptr,&sptr);
else
zzlNext(zl,&eptr,&sptr);
}
} else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {
zset *zs = zobj->ptr;
zskiplist *zsl = zs->zsl;
zskiplistNode *ln;
sds ele;
//找到起始節點
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--) {
serverAssertWithInfo(c,zobj,ln != NULL);
ele = ln->ele;
addReplyBulkCBuffer(c,ele,sdslen(ele));
if (withscores)
addReplyDouble(c,ln->score);
ln = reverse ? ln->backward : ln->level[0].forward;
}
} else {
serverPanic("Unknown sorted set encoding");
}
}複製程式碼
上面就是關於有序集SortedSet的新增,刪除,查詢的原始碼。可以看出SortedSet會根據存放元素的數量選擇ziplist或者雜湊表+跳錶兩種資料結構進行實現,之所以原始碼看上去很長,主要原因也就是要根據不同的資料結構進行不同的程式碼實現。只要掌握了這個核心思路,再看原始碼就不會太難。
三、有序集SortedSet命令總結
有序集的邏輯不難,就是程式碼有點長,涉及到ziplist,skiplist,dict三套資料結構,其中除了常規的dict之外,另外兩個資料結構內容都不少,準備專門寫文章進行總結,就不在這裡贅述了。本文主要目的是總結一下有序集SortedSet的實現原理。