從源碼看redis的list結構

rpush用來往list的隊尾加入值java

> rpush mylist "a" "b"
(integer) 2
複製代碼

使用lrange能夠查看插入的值node

> lrange mylist 0 2
1) "a"
2) "b"
複製代碼

linsert能夠在指定的元素以前或者以後插入值git

> linsert mylist before "m" "l"
-1
> linsert mylist before "d" "e"
5
> lrange mylist 0 -1
1) "e"
2) "d"
3) "c"
4) "a"
5) "b"
複製代碼

指定的元素不存在則不會插入github

rpop能夠對應彈出隊尾的值redis

> lrange mylist 0 -1
1) "e"
2) "d"
3) "c"
4) "a"
5) "b"
6) "a"
7) "b"
8) "c"
> rpop mylist
"c"
複製代碼

rpush命令執行追蹤

rpush的入口在 rpushCommand算法

Code.SLICE.source("robj *lobj = lookupKeyWrite(c->db,c->argv[1]);\n" +
        "\n" +
        " if (lobj && lobj->type != OBJ_LIST) {\n" +
        " addReply(c,shared.wrongtypeerr);\n" +
        " return;\n" +
        " }")
        .interpretation("查找以前是否是有過同名的key,若是有,可是key的編碼方式不是 OBJ_LIST直接報錯返回");
Code.SLICE.source("for (j = 2; j < c->argc; j++) ")
        .interpretation("遍歷全部的value,一個個的插入");
Code.SLICE.source("if (!lobj) {\n" +
        " lobj = createQuicklistObject();\n" +
        " quicklistSetOptions(lobj->ptr, server.list_max_ziplist_size,\n" +
        " server.list_compress_depth);\n" +
        " dbAdd(c->db,c->argv[1],lobj);\n" +
        " }\n" +
        " listTypePush(lobj,c->argv[j],where);\n" +
        " pushed++;")
        .interpretation("若是以前沒有存在如出一轍的key,從新建立一個,它的類型是 quicklist,而後存起來,再執行插入");
複製代碼

執行插入,和一個數據結構相關,就是quicklist,quicklist的每個節點爲quicklistNodebash

doubly linked list

一個常規的redis雙向列表形式以下數據結構

[0] <-> [1] <-> [2] <-> ... <-> [N]
複製代碼
  • 每個節點的listNode包含3個指針:prev/next/value(3個指針的長度爲24字節)。- 每一個數據指向一個 redisObject 對象,它包括32bit的元數據,1個int的引用,1個指向內容的指針(總共16字節)
  • 在redisObject裏面的值是sds,它包括兩個int的字段和string內容(總共 4字節+contents)

也就是說,每一個節點,至少包含40個字節的元數據內容,還有其它的一些內部爲了計算的分配,那麼若是隻往內部 插入 10個字符的string,顯然元素據的空間超過了存儲的內容,這顯得有些浪費運維

ziplist

redis使用ziplist來解決存儲小量數據 常規雙向鏈表 的問題。它的結構以下函數

[total size][tail offset][cached element count][entry 0]...[entry N][END]
複製代碼

一個空的ziplist只佔據了11 bytes

[size=4 bytes][tail offset=4 bytes][count=2 bytes][END=1 byte]
複製代碼

對於每個entry來講,它的結構爲

[length of previous entry][length of this entry][contents]
複製代碼
  1. 前一個entry的長度用來保證能夠作逆向遍歷。
  2. ziplist使用變長的編碼,若是存儲小的內容,偏移也更小

可是這種方式也帶來了問題

  1. 每次插入元素須要將後面的元素後移,同時插入意味着須要從新分配內存
  2. 刪除元素的時候,全部元素要往前移

這意味着ziplist最好保持必定的大小來作到空間和時間的最有效利用

quicklist

一個quicklist的結構大體以下

[ziplist 0] <-> [ziplist 1] <-> ... <-> [ziplist N]
複製代碼

經過 list-max-ziplist-entries 來控制每一個節點的 ziplist的數目,超過限定則新建一個 quicklistnode。 優點

  1. 任何長度的list都能有效的利用內存
  2. 仍然是O(1)獲取head和tail
  3. 刪除某個區域的list效率提高
  4. 維持了原有的RDB和AOF格式
  5. 若是限制每一個ziplist只保留1個entry,它就轉換成了原始的linked list但卻有更好的內存利用率

這種方式也帶來了額外的操做

  1. 在quicklist的中間插入元素,可能須要拆開原有的ziplist並建立額外的quicklistNOde
  2. 從quicklist中刪除元素,須要把多個ziplist進行合併
  3. 全部的插入意味着須要從新分配ziplist
  4. 在頭部插入須要把原有的ziplist實體後移

quicklist的結構以下

Code.SLICE.source("typedef struct quicklist {" +
        " quicklistNode *head; /*頭結點*/" +
        " quicklistNode *tail; /*尾結點*/" +
        " unsigned long count; /* 全部ziplists中的全部entry的個數 */\n" +
        " unsigned long len; /* quicklistNodes節點的個數 */\n" +
        " int fill : 16; /* ziplist大小設置,存放配置 list-max-ziplist-size */\n" +
        " unsigned int compress : 16; /* 節點壓縮深度設置,存放配置 list_compress_depth */\n" +
        "} quicklist;")
        .interpretation("head和tail兩個函數指針最多8字節,count和len屬於無符號long最多8字節,最後兩字段共32bits,總共40字節")
        .interpretation("list-max-ziplist-size 取正數按照個數來限制ziplist的大小,好比5表示每一個quicklist節點ziplist最多包含5個數據項,最大爲 1 << 15" +
                "-1表示每一個quicklist節點上的ziplist大小不能超過 4kb,-2(默認值)表示不能超過 8kb依次類推,最大爲 -5,不能超過 64kb")
        .interpretation("list_compress_depth 0表示不壓縮,1表示quicklist兩端各有1個節點不壓縮,其他壓縮,2表示quicklist兩端各有2個節點不壓縮,其他壓縮,依次類推,最大爲 1 << 16");
//...
Code.SLICE.source("typedef struct quicklistNode {\n" +
        " struct quicklistNode *prev; /*當前節點的前一個結點*/" +
        " struct quicklistNode *next; /*當前節點的下一個結點*/" +
        " unsigned char *zl; /*數據指針。若是當前節點沒有被壓縮,它指向的是一個ziplist,不然是 quicklistLZF*/" +
        " unsigned int sz; /* zl所指向的 ziplist 的總大小,計算被壓縮了,指向的也是壓縮前的大小*/\n" +
        " unsigned int count : 16; /* ziplist中數據項的個數 */\n" +
        " unsigned int encoding : 2; /* RAW==1(沒有壓縮) or LZF==2(壓縮了) */\n" +
        " unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */\n" +
        " unsigned int recompress : 1; /* 識別這個數據以前是否是壓縮過,好比再檢查數據的過程當中是要解壓縮的事後須要還原*/\n" +
        " unsigned int attempted_compress : 1; /* node can't compress; too small */\n" +
        " unsigned int extra : 10; /* 擴展字段,目前沒有用*/\n" +
        "} quicklistNode;")
        .interpretation("從前向和後項來看,quickList 自己就是一個 雙向鏈表")
        .interpretation("1:結構自身的大小 prev、next、zl 各8字節,sz無符號 int 爲4字節,其他按照後面的bit算一共32bits共4字節,總共32字節");
複製代碼

quicklistnode自己還能夠根據節點離head/tail的距離作壓縮,達到更高的空間節約

結論

list在底層會使用quicklist的結構來存儲,每個quicklistNode的節點都會存儲一個可配置的ziplist大小量,若是有多個quicklistNode,它會根據配置的壓縮深度,來使用lzf算法進行壓縮

附錄

rpush源碼追蹤
quicklist與其它list實現方式的對比以及性能測試說明 matt.sh
Redis內部數據結構詳解(5)——quicklist 張鐵蕾
Redis內部數據結構詳解(4)——ziplist 張鐵蕾 redis設計與實現 redis開發與運維

相關文章
相關標籤/搜索