有序集合是Redis對象系統中的一部分,其底層採用跳錶和壓縮列表兩種形式儲存,在上一篇介紹了跳錶實現,就趁熱打鐵看一下有序集合的跳錶實現
本篇主要涉及的是有序集合添加資料的命令,後面會看到,在命令的底層實現中,實際上還是調用跳錶的介面 儲存結構
有序集合的定義在server.h檔案中,不過除了跳錶以外,有序集合又儲存了一個字典,這個字典的作用是用來尋找某個資料對應的分值。根據跳錶的實現可知,跳錶內部是採用分值排序的,通過分值尋找資料還行,但是如果要通過資料尋找分值,就顯得力不從心了,所以Redis又維護了一個字典,用來完成通過資料尋找分值的任務
//server.h/* 有序集合 */typedef struct zset { dict *dict; /* 儲存<資料,分值>索引值對,用來通過資料尋找分值 */ zskiplist *zsl; /* 跳錶,儲存<分值,資料>,內部通過分值排序 */} zset;
有序集合操作
添加資料
通過命令ZADD,可以實現向資料庫中添加有序集合,該命令是由zaddGenericCommand函數實現的。函數中會先解析命令選項,命令參數,然後根據底層是由跳錶實現還是由壓縮列表實現執行不同的操作,都是調用二者的介面
/* 向有序集合中添加資料 */void zaddGenericCommand(client *c, int flags) { 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; int scoreidx = 0; ... scoreidx = 2; /* 擷取參數選項,參數選項緊接在鍵的後面 */ ... /* 將參數選項轉換為數值變數 */ ... /* argc中儲存所有參數個數,scoreidx儲存第一個資料的分值位置 * argc - scoreidx計算所有的分值,資料個數 */ elements = c->argc-scoreidx; /* 由於分值和資料是成對出現的,這裡判斷輸入的個數是否合法 */ if (elements % 2) { addReply(c,shared.syntaxerr); return; } /* 除以2計算不同<分數,資料>對的個數 */ elements /= 2; /* 核查幾個選項 */ ... /* 為分值分配記憶體 */ scores = zmalloc(sizeof(double)*elements); for (j = 0; j < elements; j++) { /* 將字串類型轉成double */ if (getDoubleFromObjectOrReply(c,c->argv[scoreidx+j*2],&scores[j],NULL) != C_OK) goto cleanup; } /* 在資料庫中尋找是否存在鍵key,返回鍵對應的值 */ zobj = lookupKeyWrite(c->db,key); if (zobj == NULL) { /* 如果不存在,建立值 */ if (xx) goto reply_to_client; /* No key + XX option: nothing to do. */ /* 根據參數配置選擇底層採用壓縮字典還是跳錶 */ /* 如果資料長度大於規定值,則採用跳錶,否則選擇壓縮列表 */ if (server.zset_max_ziplist_entries == 0 || server.zset_max_ziplist_value < sdslen(c->argv[scoreidx+1]->ptr)) { /* 建立跳錶編碼的有序集合 */ zobj = createZsetObject(); } else { /* 建立壓縮列表編碼的有序集合 */ zobj = createZsetZiplistObject(); } /* 將索引值對添加到資料庫中,這裡值是空的 */ dbAdd(c->db,key,zobj); } else { /* 存在鍵key,判斷原先的值是否是用有序集合儲存的 */ if (zobj->type != OBJ_ZSET) { addReply(c,shared.wrongtypeerr); goto cleanup; } } /* 對於每個<分數,資料>對,將其添加到zobj中 */ for (j = 0; j < elements; j++) { /* 第j個分值 */ score = scores[j]; /* 採用壓縮列表的api執行添加操作 */ if (zobj->encoding == OBJ_ENCODING_ZIPLIST) { ... } else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) { /* 採用跳錶的api添加資料 */ zset *zs = zobj->ptr; zskiplistNode *znode; dictEntry *de; /* 嘗試將資料轉換成合適的編碼以節省記憶體 */ ele = c->argv[scoreidx+1+j*2] = tryObjectEncoding(c->argv[scoreidx+1+j*2]); /* 採用跳錶實現的有序集合中儲存了一個字典,鍵是資料,值是分值 */ de = dictFind(zs->dict,ele); /* 有序集合中存在要添加的資料 */ if (de != NULL) { if (nx) continue; /* 擷取鍵節點的資料和分值 */ curobj = dictGetKey(de); curscore = *(double*)dictGetVal(de); /* incr選項是如果存在該資料,則和它對應的分值相加 */ if (incr) { score += curscore; ... } /* 更新資料,分值 */ if (score != curscore) { /* 先刪除,再添加 */ serverAssertWithInfo(c,curobj,zslDelete(zs->zsl,curscore,curobj)); /* 跳錶插入操作 */ znode = zslInsert(zs->zsl,score,curobj); /* 增加資料的引用計數 */ incrRefCount(curobj); /* 更新字典中的分值 */ dictGetVal(de) = &znode->score; server.dirty++; updated++; } processed++; } else if (!xx) { /* 不存在要添加的資料,直接插入 */ znode = zslInsert(zs->zsl,score,ele); incrRefCount(ele); /* Inserted in skiplist. */ serverAssertWithInfo(c,NULL,dictAdd(zs->dict,ele,&znode->score) == DICT_OK); incrRefCount(ele); /* Added to dictionary. */ server.dirty++; added++; processed++; } } else { serverPanic("Unknown sorted set encoding"); } } ...}
為了不讓函數太長,這裡刪除了一些關於命令選項的判斷和執行,不過還是很長… 擷取排名
在跳錶中,看到跳錶可以快速計算<分值,資料>的排名。排名是由ZRANK命令完成的,底層由zrankGnericCommand函數實現
//t_zset.c/* 返回某個鍵下的指定資料的排名 */void zrankGenericCommand(client *c, int reverse) { /* 第一個參數是鍵 */ robj *key = c->argv[1]; /* 第二個參數是值 */ robj *ele = c->argv[2]; robj *zobj; unsigned long llen; unsigned long rank; /* 在資料庫中尋找鍵key是否存在,如果存在,再判斷值是否是由有序集合儲存的 */ if ((zobj = lookupKeyReadOrReply(c,key,shared.nullbulk)) == NULL || checkType(c,zobj,OBJ_ZSET)) return; /* 擷取有序集合中資料個數 */ llen = zsetLength(zobj); serverAssertWithInfo(c,ele,sdsEncodedObject(ele)); /* 根據底層實現不同選擇不同的介面 */ if (zobj->encoding == OBJ_ENCODING_ZIPLIST) { ... } else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) { /* 如果底層採用跳錶實現,則調用跳錶介面 */ /* 擷取鍵key對應的有序集合 */ zset *zs = zobj->ptr; zskiplist *zsl = zs->zsl; dictEntry *de; double score; ele = c->argv[2]; /* 由於跳錶通過資料尋找分值比較慢 * 所以Redis採用字典儲存<資料,分值>對,可通過資料快速找到對應分值 */ de = dictFind(zs->dict,ele); if (de != NULL) { /* 如果有序集合中存在要尋找的資料,則擷取資料的分值 */ score = *(double*)dictGetVal(de); /* 調用跳錶介面計算分值score的排名 */ rank = zslGetRank(zsl,score,ele); serverAssertWithInfo(c,ele,rank); /* 根據選項不同計算是正向排名還是逆向排名 */ if (reverse) addReplyLongLong(c,llen-rank); else addReplyLongLong(c,rank-1); } else { addReply(c,shared.nullbulk); } } else { serverPanic("Unknown sorted set encoding"); }}
計算資料個數
zsetLength函數用於計算有序集合中資料個數,同樣是調用跳錶或者壓縮列表的介面
//t_zset.c/* 計算有序集合中資料個數 */unsigned int zsetLength(robj *zobj) { int length = -1; /* 根據底層實現不同調用不同介面 */ if (zobj->encoding == OBJ_ENCODING_ZIPLIST) { ... } else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) { /* 跳錶中直接儲存了資料個數 */ length = ((zset*)zobj->ptr)->zsl->length; } else { serverPanic("Unknown sorted set encoding"); } return length;}
小結
有序集合中儲存的資料都是有序的,在對象系統中底層可以由跳錶和有序列表實現,而且跳錶實現還是比較簡單的,而壓縮列表實現會相對難理解一些