Redis源碼剖析(十二)有序集合跳錶實現

來源:互聯網
上載者:User

有序集合是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;}
小結

有序集合中儲存的資料都是有序的,在對象系統中底層可以由跳錶和有序列表實現,而且跳錶實現還是比較簡單的,而壓縮列表實現會相對難理解一些

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.