redis源碼分析(三):基本數(shù)據(jù)結(jié)構(gòu)

redis中使用的數(shù)據(jù)結(jié)構(gòu)有:

  • dict 字典,就是個哈希表,實現(xiàn)和HashMap類似,不做闡述;不同的是在哈希表resize()的時候是分步執(zhí)行的,后續(xù)篇幅再說明。

  • sds 很多項目都對自己的字符串進(jìn)行了封裝,作用類似于leveldb的slice。

  • linkedlist 雙端鏈表,迭代器的實現(xiàn)是通過鏈表的pre和next實現(xiàn)的,是個BidirctionalIterator。代碼中只實現(xiàn)了ForwardIterator的功能。

  • zipmap 已不再使用了

  • inset 一個緊致的有序整型數(shù)組。

  • ziplist 也是一個鏈表,但是它的內(nèi)存是連續(xù)的,在數(shù)據(jù)量小的時候使用,增長到一定的大小會轉(zhuǎn)化成list或者skiplist

  • skiplist 跳躍表,實現(xiàn)在t_zset.c中

本文只闡述inset,ziplist,skiplist三種數(shù)據(jù)結(jié)構(gòu)。

1.ziplist

直接借用代碼注釋上ziplist的存儲結(jié)構(gòu):

/*
area        |<---- ziplist header ---->|<----------- entries ------------->|<-end->|

size          4 bytes  4 bytes  2 bytes    ?        ?        ?        ?     1 byte
            +---------+--------+-------+--------+--------+--------+--------+-------+
component   | zlbytes | zltail | zllen | entry1 | entry2 |  ...   | entryN | zlend |
            +---------+--------+-------+--------+--------+--------+--------+-------+
                                       ^                          ^        ^
address                                |                          |        |
                                ZIPLIST_ENTRY_HEAD                |   ZIPLIST_ENTRY_END
                                                                  |
                                                        ZIPLIST_ENTRY_TAIL
  • zlbytes 記錄了整個ziplist占用的內(nèi)存大??;
  • zltail 記錄尾節(jié)點(diǎn)的相對于ziplist實例指針的偏移量;
  • zllen 是整個list的節(jié)點(diǎn)數(shù)量的無符號數(shù),占2個字節(jié),可以最大表示2**16-1,實際使用保留了0xffff ffff ,即大于等于這個數(shù)的時候,已經(jīng)不能通過此值來取得節(jié)點(diǎn)的數(shù)量了;
  • zlend 固定為0xff。
  • entry 的結(jié)構(gòu)并不是代碼中給的結(jié)構(gòu)體zlentry。而是如下結(jié)構(gòu):
size              1/5 bytes      1/2/5 bytes    ?   
            +---------------+---------------+-------+
component   | pre_entry_len | cur_entry_len | value |
            +---------------+---------------+-------+

這是個變長的結(jié)構(gòu),pre節(jié)點(diǎn)小于254則pre_entry_len占用1字節(jié),否則5字節(jié),為什么不是255字節(jié)呢?255會和zlend產(chǎn)生沖突;cur_entry_len中包含了編碼信息(以字符串編碼還是數(shù)字,數(shù)字編碼能減少內(nèi)存長度)和本節(jié)點(diǎn)的長度信息,數(shù)字格式編碼固定占用1字節(jié);字符串格式編碼數(shù)據(jù)小于127字節(jié)占1字節(jié),小于0x3fff 占用2字節(jié),否則5個字節(jié)。

因此ziplist有如下特點(diǎn):

  • 內(nèi)存連續(xù),數(shù)據(jù)緊湊。
  • 查找效率o(n)
  • 雙端查詢,可前后遍歷,類似于nodelist.

了解了數(shù)據(jù)結(jié)構(gòu),思考下如何對其增加一個節(jié)點(diǎn)p,p的大小為sp。

  • 1.ziplist長度發(fā)生變化,zlbytes要增加entry的內(nèi)存大?。?/li>
  • 2.zltail也要做相應(yīng)改變,如果插入的是尾節(jié)點(diǎn),則zltail等于ziplist到新節(jié)點(diǎn)p的距離,否則是原值加上新節(jié)點(diǎn)p的長度。
  • 3.內(nèi)存大小發(fā)生改變,需要realloc個更大的內(nèi)存。
  • 4.對于在新節(jié)點(diǎn)以后的節(jié)點(diǎn),后移sp個單位。接下來要更新p以后的節(jié)點(diǎn)內(nèi)容:
  • 4.1如果p的前一個節(jié)點(diǎn)的長度和p節(jié)點(diǎn)的長度一樣長,那么皆大歡喜。只需要更新p節(jié)點(diǎn)后一個節(jié)點(diǎn)的pre_entry_len值;
  • 4.2如果比p節(jié)點(diǎn)的長度大(5字節(jié)對1字節(jié)),也ok,存下來,浪費(fèi)4個字節(jié)內(nèi)存。
  • 4.3如果比p節(jié)點(diǎn)的長度小(1字節(jié)對5字節(jié)),意味著p以后的節(jié)點(diǎn)長度會發(fā)生變化,增加4個字節(jié)來存儲p的長度,zltail和ziplist也要同時加4;同時由于p->next節(jié)點(diǎn)長度發(fā)生變化,p->next->next也要去做一次判斷,需要重復(fù)步驟4,直到不滿足4.3的情況為止。

經(jīng)過以上步驟,就可以大概還原redis中的代碼實現(xiàn)了,也可以看出來ziplist的增刪代價是很大的,改變一個節(jié)點(diǎn)的復(fù)雜度最差是(n^2);一個節(jié)點(diǎn)改動,可能牽一發(fā)而動全身,所以ziplist是不適合大數(shù)據(jù)量的存儲的。

2.inset

inset 實際是一個整數(shù)數(shù)組,結(jié)構(gòu)如下:

typedef struct intset {
    
    uint32_t encoding;

    uint32_t length;

    int8_t contents[];

} intset;

其中encoding是編碼方式,有16位,32位,64位三種取值;length是inset集合中保存的元素個數(shù),contents為保存的數(shù)據(jù)。下面是inset的特點(diǎn)和實現(xiàn)方式.

  • 1.初始化的inset是16位編碼的,如果inset里面只保存了16位的數(shù)字,那么這個編碼方式將保持不變。

  • 2.inset是有序不重復(fù)的,從小到大排列,每次插入/刪除會觸發(fā)inset的大小調(diào)整,length+1。

  • 3.如果插入了比當(dāng)前編碼范圍更大的數(shù)字,會觸發(fā)intsetUpgradeAndAdd(),下面以16位-> 32位擴(kuò)展為例。

  • 3.1 重新調(diào)整contents大小,大小為原(size+1)*2,更新encoding的值為INTSET_ENC_INT32;

  • 3.2 新數(shù)字肯定是位于數(shù)組開頭或者結(jié)尾,如果是在結(jié)尾,將原數(shù)組的每個數(shù)從原位置pos移到pos2的位置,否則是(pos+1)2;

  • 3.3 插入新的數(shù)據(jù),intsetUpgradeAndAdd()過程結(jié)束

  • 4.刪除不改變inset的編碼方式,即如果原inset為32位,刪光了inset里的16位不能表示的數(shù)字以后,inset也不會恢復(fù)到16位,代價太大,即intsetUpgradeAndAdd()的過程是不可逆的。

  • 5.在inset中查找一個數(shù)采用二分查找。

根據(jù)這些特點(diǎn),寫出inset的實現(xiàn),想必不難,inset的特點(diǎn)是在存儲的數(shù)據(jù)為小值的時候,占用的內(nèi)存較小,但是只要插入了一個64位的數(shù)據(jù),那么inset就沒有優(yōu)勢了,插入和刪除的時間復(fù)雜度都是o(n),因此不適合做大數(shù)量的存儲。

3.skiplist

跳表是一個特殊的有序鏈表,盜一張網(wǎng)圖吧。

跳表結(jié)構(gòu)

結(jié)構(gòu)如下

typedef struct zskiplist {

    // 表頭節(jié)點(diǎn)和表尾節(jié)點(diǎn)
    struct zskiplistNode *header, *tail;

    // 表中節(jié)點(diǎn)的數(shù)量
    unsigned long length;

    // 表中層數(shù)最大的節(jié)點(diǎn)的層數(shù)
    int level;

} zskiplist;

節(jié)點(diǎn)的結(jié)構(gòu)

typedef struct zskiplistNode {

    // 成員對象
    robj *obj;

    // 分值
    double score;

    // 后退指針
    struct zskiplistNode *backward;

    // 層
    struct zskiplistLevel {

        // 前進(jìn)指針
        struct zskiplistNode *forward;

        // 跨度
        unsigned int span;

    } level[];

} zskiplistNode;

簡述下跳表的特點(diǎn)

  • 跳表是一個排序鏈表,redis中以score的大小做排序,增刪改查的效率均是o(logn),由于其實現(xiàn)更易于理解和實現(xiàn)而被廣泛使用。
  • 跳表是不穩(wěn)定的,即同樣是創(chuàng)建一個跳表,插入若干相同的值,內(nèi)部的數(shù)據(jù)結(jié)構(gòu)可能是不一致的。
  • 每個節(jié)點(diǎn)的大小不固定,不固定的原因是節(jié)點(diǎn)的擁有的forward指針個數(shù)不定,這個數(shù)值是在節(jié)點(diǎn)創(chuàng)建的時候隨機(jī)選取的一個正整數(shù),redis實現(xiàn)默認(rèn)最大層數(shù)是32。
  • redis中的跳表增加了一個指向前節(jié)點(diǎn)的backward來往前遍歷;一個span,用于計算forward節(jié)點(diǎn)和本節(jié)點(diǎn)中間的跨度,以方便zcount等命令來計算范圍內(nèi)score的數(shù)量。

如何插入一個數(shù)據(jù)?redis排序順序是從頭到尾,按照score從小到大。如果要增加一個值,首先需要確定插入后的順序,假設(shè)跳表為skl ,新節(jié)點(diǎn)為n。

    1. 找到每一層比新節(jié)點(diǎn)score小,而且距離最近的節(jié)點(diǎn),保存為update[]。
    1. 隨機(jī)產(chǎn)生新節(jié)點(diǎn)的層高,對每一層的forward節(jié)點(diǎn)復(fù)制,令n->level[i]->forward = update[i]->level[i]->forward,給新節(jié)點(diǎn)的所有forward指針賦值。令update[i]->forward = n使新節(jié)點(diǎn)和其所有前置節(jié)點(diǎn)關(guān)聯(lián);實際上和雙向鏈表操作類似。

如何查找一個節(jié)點(diǎn)p?

  • 場景1.根據(jù)score范圍查找內(nèi)容,對應(yīng)zrange命令。查找左界和右界,中間的范圍及所求。左界和右界算法類似,從head的頂層forward節(jié)點(diǎn)開始,如果比p的score小,那么讓head = forward 繼續(xù)此步驟.如果等于score那么返回。如果小于那么代表需要找的左界在head和forward之間,跳到下一層重復(fù)此步驟。

  • 場景2.根據(jù)內(nèi)容查找score,對應(yīng)zscore命令。redis不查找skiplist,而是額外使用了個hash表來達(dá)到o(1)的效率。

迭代器如何設(shè)計?

  • 可知n->level[0]->forward的步長是1,循環(huán)forward可以遍歷整個跳表。
    推薦跳表的leveldb版實現(xiàn)。
    綜上,redis的數(shù)據(jù)結(jié)構(gòu)分析告一段落。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容