前言:list即鏈表,它是一個能維持數據先后順序的列表,便於在表的兩端追加和刪除數據,中間位置的存取具有O(N)的時間復雜度,是一個雙向鏈表。
一、內部原理
redis內部實現代碼在quicklist.c(注釋:A doubly linked list of ziplists)中,它確實是一個雙向鏈表,並且是一個ziplist雙向列表。
ziplist是什么?
一個經過特殊編碼的的雙向鏈表,它的設計目的是為了提高存儲效率。ziplist可以用於存儲字符串或整數,其中整數是真正的二進制進行編碼的,而不是編碼成字符串序列。普通的雙向鏈表每一項都獨立的占用一塊內存,各項之間用地址指針連接起來。這中方式會帶來大量的內存碎片,而且地址指針也會占用額外的內存。而ziplist將列表的每一項存放在前后連續的地址空間內,一個大的ziplist整體占用一大塊內存,它是一個列表,但不是一個鏈表。ziplist為了在細節上節省內存,對於值的存儲采用了變長的編碼方式,對於大的整數,就多一些字節來存儲,對於小的少一些字節來存儲。
ziplist的數據結構如下:
<zlbytes><zltail><zllen><entry>...<entry><zlend>
含義:
<zlbytes>:32字節,表示ziplist占用的字符總數(本身占用4個字節)。
<zltail>: 32字節,表示ziplist表中最后一項(entry)在ziplist中的偏移字節數。<zltail>的存在,使得我們可以很方便地找到最后一項,從而可以在ziplist尾端快速地執行push或pop操作。
<zllen> : 16字節, 表示ziplist中數據項(entry)的個數。zllen字段因為只有16bit,所以可以表達的最大值為2^16-1。這里需要特別注意的是,如果ziplist中數據項個數超過了16bit能表達的最大值,ziplist仍然可以來表示。那怎么表示呢?這里做了這樣的規定:如果<zllen>小於等於2^16-2(也就是不等於2^16-1),那么<zllen>就表示ziplist中數據項的個數;否則,也就是<zllen>等於16bit全為1的情況,那么<zllen>就不表示數據項個數了,這時候要想知道ziplist中數據項總數,那么必須對ziplist從頭到尾遍歷各個數據項,才能計數出來。
<entry> : 表示真正存放數據的數據項,長度不定。一個數據項(entry)也有它自己的內部結構。
<zlend> : ziplist最后1個字節,是一個結束標記,值固定等於255。
entry的數據結構:
<prevrawlen><len><data>
<prevrawlen>: 表示前一個數據項占用的總字節數。這個字段的用處是為了讓ziplist能夠從后向前遍歷(從后一項的位置,只需向前偏移prevrawlen個字節,就找到了前一項)。這個字段采用變長編碼。
<prevrawlen>。它有兩種可能,或者是1個字節,或者是5個字節:
1. 如果前一個數據項占用字節數小於254,那么<prevrawlen>就只用一個字節來表示,這個字節的值就是前一個數據項的占用字節數。
2. 如果前一個數據項占用字節數大於等於254,那么<prevrawlen>就用5個字節來表示,其中第1個字節的值是254(作為這種情況的一個標記),而后面4個字節組成一個整型值,來真正存儲前一個數據項的占用字節數。
<len>: 表示當前數據項的數據長度(即<data>部分的長度)。也采用變長編碼。根據第一個字節的不同分為下面九種方式
|00pppppp| - 1 byte String value with length less than or equal to 63 bytes (6 bits).
|01pppppp|qqqqqqqq| - 2 bytes String value with length less than or equal to 16383 bytes (14 bits).
|10______|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt| - 5 bytes String value with length greater than or equal to 16384 bytes.
|11000000| - 1 byte Integer encoded as int16_t (2 bytes).
|11010000| - 1 byte Integer encoded as int32_t (4 bytes).
|11100000| - 1 byte Integer encoded as int64_t (8 bytes).
|11110000| - 1 byte Integer encoded as 24 bit signed (3 bytes).
|11111110| - 1 byte Integer encoded as 8 bit signed (1 byte).
|1111xxxx| - (with xxxx between 0000 and 1101) immediate 4 bit integer. Unsigned integer from 0 to 12. The encoded value is actually from 1 to 13 because 0000 and 1111 can not be used, so 1 should be subtracted from the encoded 4 bit value to obtain the right value.
|11111111| - End of ziplist.
quicklist是什么?
雙向鏈表都是有多個node組成,而quicklist的每個節點都是一個ziplist。ziplist本身也是一個能維持數據項先后順序的列表,而且內存是緊湊的,例如一個包含2個node的quicklist,如果每個節點的ziplist包含了四個數據項
那么對外表現就是8個數據項。quicklist的設計是一個空間和時間的折中,雙向鏈表便於在表的兩端進行push和pop操作,但是它的內存開銷很大。開銷如下
1.每個節點上除了要保存數據之外,還要額外的保存兩個指針。
2.各個節點是單獨的內存塊,地址不連續,節點多了容易產生內存碎片。
ziplist是一塊連續的內存,所以存儲效率很高。但是,它不利於修改操作,每次數據變動都會引發內存的realloc。一次realloc可能會導致大量的數據拷貝,進一步降低性能。
quicklist結合了雙向鏈表和ziplist的優點,但是同樣也存在一個問題,一個quicklist包含多長的ziplist合適呢?需要找到一個平衡點
1.ziplist太短,內存碎片越多。
2.ziplist太長,分配大塊連續內存空間的難度就越大。
如果保持ziplist的合理長度,取決於具體的應用場景。redis提供了默認配置
list-max-ziplist-size -2
參數的含義解釋,取正值時表示quicklist節點ziplist包含的數據項。取負值表示按照占用字節來限定quicklist節點ziplist的長度。
-5: 每個quicklist節點上的ziplist大小不能超過64 Kb。
-4: 每個quicklist節點上的ziplist大小不能超過32 Kb。
-3: 每個quicklist節點上的ziplist大小不能超過16 Kb。
-2: 每個quicklist節點上的ziplist大小不能超過8 Kb。(默認值)
-1: 每個quicklist節點上的ziplist大小不能超過4 Kb。
list設計最容易被訪問的是列表兩端的數據,中間的訪問頻率很低,如果符合這個場景,list還有一個配置,可以對中間節點進行壓縮(采用的LZF——一種無損壓縮算法),進一步節省內存。配置如下
list-compress-depth 0
含義:
0: 是個特殊值,表示都不壓縮。這是Redis的默認值。
1: 表示quicklist兩端各有1個節點不壓縮,中間的節點壓縮。
2: 表示quicklist兩端各有2個節點不壓縮,中間的節點壓縮。
以此類推
quicklist數據結構:
/* quicklistNode is a 32 byte struct describing a ziplist for a quicklist. * We use bit fields keep the quicklistNode at 32 bytes. * count: 16 bits, max 65536 (max zl bytes is 65k, so max count actually < 32k). * encoding: 2 bits, RAW=1, LZF=2. * container: 2 bits, NONE=1, ZIPLIST=2. * recompress: 1 bit, bool, true if node is temporarry decompressed for usage. * attempted_compress: 1 bit, boolean, used for verifying during testing. * extra: 12 bits, free for future use; pads out the remainder of 32 bits */ typedef struct quicklistNode { struct quicklistNode *prev; /*指向鏈表前一個節點的指針*/ struct quicklistNode *next; /*指向鏈表后一個節點的指針*/ unsigned char *zl;/*數據指針。如果當前節點的數據沒有壓縮,那么它指向一個ziplist結構;否則,它指向一個quicklistLZF結構。*/ unsigned int sz; /*表示zl指向的ziplist的總大小(包括zlbytes, zltail, zllen, zlend和各個數據項)。需要注意的是:如果ziplist被壓縮了,那么這個sz的值仍然是壓縮前的ziplist大小。/* unsigned int count : 16; /* 表示ziplist里面包含的數據項個數。 */ unsigned int encoding : 2; /* RAW==1(未壓縮) or LZF==2 (壓縮了並采用LZF壓縮算法)*/ unsigned int container : 2; /* 使用的容器 NONE==1 or ZIPLIST==2(默認值) */ unsigned int recompress : 1; /* 我們使用類似lindex這樣的命令查看了某一項本來壓縮的數據時,需要把數據暫時解壓,這時就設置recompress=1做一個標記,等有機會再把數據重新壓縮 */ unsigned int attempted_compress : 1; /* node can't compress; too small */ unsigned int extra : 10; /* 其他擴展字段(未使用) */ } quicklistNode; /* quicklistLZF is a 4+N byte struct holding 'sz' followed by 'compressed'. * 'sz' is byte length of 'compressed' field. * 'compressed' is LZF data with total (compressed) length 'sz' * NOTE: uncompressed length is stored in quicklistNode->sz. * When quicklistNode->zl is compressed, node->zl points to a quicklistLZF */ typedef struct quicklistLZF { unsigned int sz; /* 表示壓縮后的ziplist大小*/ char compressed[]; /*是個柔性數組(flexible array member),存放壓縮后的ziplist字節數組/* } quicklistLZF; /* quicklist is a 32 byte struct (on 64-bit systems) describing a quicklist. * 'count' is the number of total entries. * 'len' is the number of quicklist nodes. * 'compress' is: -1 if compression disabled, otherwise it's the number * of quicklistNodes to leave uncompressed at ends of quicklist. * 'fill' is the user-requested (or default) fill factor. */ typedef struct quicklist { quicklistNode *head; ?/*指向頭節點(左側第一個節點)的指針。*/ quicklistNode *tail; /*指向尾節點(右側第一個節點)的指針。*/ unsigned long count; /* quicklist節點的個數 */ unsigned int len; /* number of quicklistNodes */ int fill : 16; /* ziplist大小設置,存放list-max-ziplist-size參數的值 */ unsigned int compress : 16; /* 節點壓縮深度設置,存放list-compress-depth參數的值 */ }
二:相關命令
lpush key value[value...] 將一個或多個value插入到列表的表頭,如果有多個 value 值,那么各個 value 值按從左到右的順序依次插入到表頭: 比如說,對空列表 mylist 執行命令 LPUSH mylist a b c,列表的值將是 c b a ,這等同於原子性地執行 LPUSH mylist a 、 LPUSH mylist b 和 LPUSH mylist c 三個命令。如果 key 不存在,一個空列表會被創建並執行 lpush 操作。當 key 存在但不是列表類型時,返回一個錯誤。
lpushx key value 將值 value 插入到列表 key 的表頭,若key不存在,不執行任何操作。
lpop key 移除並返回列表key的頭元素(后進先出),若key不存在返回nil。
blpop key[key...] timeout lpop的阻塞版本,若給定列表中沒有任何元素可供彈出時,鏈接會被blpop命令阻塞,直到等待超時(單位:秒)或發現可彈出元素時為止,若發現其中任何一個列表中有值則返回列表key和第一個元素的值。
rpush key value[value...] 將一個或多個值 value 插入到列表 key 的表尾(最右邊),如果有多個 value 值,那么各個 value 值按從左到右的順序依次插入到表尾:比如對一個空列表 mylist 執行 RPUSH mylist a b c ,得出的結果列表為 a b c ,等同於執行命令 RPUSH mylist a 、 RPUSH mylist b 、 RPUSH mylist c 。如果 key 不存在,一個空列表會被創建並執行 Rpush 操作。當 key 存在但不是列表類型時,返回一個錯誤。
rpushx key value 將值 value 插入到列表 key 的表尾,若key不存在,不執行任何操作。
rpop key 移除並返回列表的末尾,若key不存在則返回nil。
brpop key[key...] timeout 它是 rpop命令的阻塞版本,當給定列表內沒有任何元素可供彈出的時候,連接將被 brpop 命令阻塞,直到等待超時或發現可彈出元素為止。當給定多個 key 參數時,按參數 key 的先后順序依次檢查各個列表,彈出第一個非空列表的尾.
rpoplpush source destination 命令 rpoppush 在一個原子時間內,執行以下兩個動作:將列表 source 中的最后一個元素(尾元素)彈出,並返回給客戶端。將 source 彈出的元素插入到列表 destination ,作為 destination 列表的的頭元素。如果 source 不存在,值 nil 被返回,並且不執行其他動作。如果 source 和 destination 相同,則列表中的表尾元素被移動到表頭,並返回該元素,可以把這種特殊情況視作列表的旋轉(rotation)操作。
brpoplpush source destination brpoplpush是 rpoplpush的阻塞版本,當給定列表 source 不為空時, brpoplpush 的表現和 rpoplpush 一樣。當列表 source 為空時, brpoplpush 命令將阻塞連接,直到等待超時,或有另一個客戶端對 source 執行 lpush或 rpush 命令為止。超時參數 timeout 接受一個以秒為單位的數字作為值。超時參數設為 0 表示阻塞時間可以無限期延長(block indefinitely)
lset key index value 將列表 key 下標為 index 的元素的值設置為 value 。當 index 參數超出范圍,或對一個空列表( key 不存在)進行 lset時,返回一個錯誤。
linsert key before|after pivot value 將值 value 插入到列表 key 當中,位於值 pivot 之前或之后。當 pivot 不存在於列表 key 時,不執行任何操作。當 key 不存在時, key 被視為空列表,不執行任何操作。如果 key 不是列表類型,返回一個錯誤。
llen key 返回列表 key 的長度。如果 key 不存在,則 key 被解釋為一個空列表,返回 0 .如果 key 不是列表類型,返回一個錯誤。
lindex key index 返回列表 key 中,下標為 index 的元素。下標(index)參數 start 和 stop 都以 0 為底,也就是說,以 0 表示列表的第一個元素,以 1 表示列表的第二個元素,以此類推。你也可以使用負數下標,以 -1 表示列表的最后一個元素, -2 表示列表的倒數第二個元素,以此類推。如果 key 不是列表類型,返回一個錯誤。
lrange key start stop 返回列表 key 中指定區間內的元素,區間以偏移量 start 和 stop 指定。下標(index)參數 start 和 stop 都以 0 為底,也就是說,以 0 表示列表的第一個元素,以 1 表示列表的第二個元素,以此類推。你也可以使用負數下標,以 -1 表示列表的最后一個元素, -2 表示列表的倒數第二個元素,以此類推。
ltrim key start stop 對一個列表進行修剪(trim),就是說,讓列表只保留指定區間內的元素,不在指定區間之內的元素都將被刪除。下標(index)參數 start 和 stop 都以 0 為底,也就是說,以 0 表示列表的第一個元素,以 1 表示列表的第二個元素,以此類推。你也可以使用負數下標,以 -1 表示列表的最后一個元素, -2 表示列表的倒數第二個元素,以此類推。當 key 不是列表類型時,返回一個錯誤。
lrem key count value 移除列表中與value相等的元素,若count>0從左到右移除與count個與value相等的元素;若count<0從右向左移除count個與value相等的元素;若count==0移除所有與value相等的元素。