redis使用兩種數(shù)據(jù)結(jié)構(gòu)保存鏈表,分別是ziplist與linkedlist,內(nèi)存占用及常用操作效率各不相同。
本文嘗試簡(jiǎn)要說明他們之間的區(qū)別。
眾所周知,redis中的list有兩種編碼結(jié)構(gòu),ziplist和linkedlist。兩種編碼結(jié)構(gòu)的切換由下面的配置信息決定:
redis 127.0.0.1:6379> config get list*
1) "list-max-ziplist-entries"
2) "512"
3) "list-max-ziplist-value"
4) "64"
以上兩個(gè)配置是默認(rèn)的配置。
針對(duì)以上的配置,當(dāng)列表對(duì)象保存的所有字符串元素的長(zhǎng)度都小于64字節(jié),并且列表對(duì)象保存的元素?cái)?shù)量小于512時(shí),list使用ziplist編碼;不能滿足這兩種情況就是用linkedlist編碼。
ziplist的特點(diǎn)是節(jié)省內(nèi)存,linkedlist是一個(gè)雙向列表,特點(diǎn)就是插入速度快,但是占內(nèi)存。
測(cè)試
正式開始我今天主要想發(fā)表的東西,雖不是什么了不起的東西,但是是我認(rèn)認(rèn)真真測(cè)試出來的結(jié)果,留個(gè)紀(jì)念吧。
測(cè)試方式:
a. 一個(gè)key,分別對(duì)其進(jìn)行rpush、lrange、ltrim三種操作;
b. rpush數(shù)據(jù)為80W個(gè)整型,每插入10W條記錄記錄一次此時(shí)的平均插入速率;
c. 每隔10W條記錄進(jìn)行一次lrange,查看占用時(shí)間;
d. 全部數(shù)據(jù)更新成功后,開始測(cè)試ltrim;
e. 分兩種編碼結(jié)構(gòu)進(jìn)行測(cè)試,作對(duì)比;
以下是測(cè)試結(jié)果:
關(guān)于ziplist和linkedlist的內(nèi)存占用,80W的數(shù)據(jù),ziplist占用內(nèi)存不到5M,而linked占用內(nèi)存為37M+,內(nèi)存占用相差7倍多。但是執(zhí)行速度方面,linkedlist有明顯的優(yōu)勢(shì),在80w級(jí)別的數(shù)據(jù)相差63左右。
通過上面的測(cè)試,我們已經(jīng)知道ziplist在空間利用上有優(yōu)勢(shì),linkedlist在執(zhí)行效率上有優(yōu)勢(shì),具體選擇什么類型,需結(jié)合使用場(chǎng)景而定。
數(shù)據(jù)結(jié)構(gòu)
鏈表
每個(gè)鏈表節(jié)點(diǎn)使用一個(gè) adlist.h/listNode 結(jié)構(gòu)來表示:
typedef struct listNode {
// 前置節(jié)點(diǎn)
struct listNode *prev;
// 后置節(jié)點(diǎn)
struct listNode *next;
// 節(jié)點(diǎn)的值
void *value;
} listNode;
多個(gè) listNode 可以通過 prev 和 next 指針組成雙端鏈表,如圖 3-1 所示。
雖然僅僅使用多個(gè) listNode 結(jié)構(gòu)就可以組成鏈表, 但使用 adlist.h/list 來持有鏈表的話, 操作起來會(huì)更方便:
typedef struct list {
// 表頭節(jié)點(diǎn)
listNode *head;
// 表尾節(jié)點(diǎn)
listNode *tail;
// 鏈表所包含的節(jié)點(diǎn)數(shù)量
unsigned long len;
// 節(jié)點(diǎn)值復(fù)制函數(shù)
void *(*dup)(void *ptr);
// 節(jié)點(diǎn)值釋放函數(shù)
void (*free)(void *ptr);
// 節(jié)點(diǎn)值對(duì)比函數(shù)
int (*match)(void *ptr, void *key);
} list;
list 結(jié)構(gòu)為鏈表提供了表頭指針 head 、表尾指針 tail , 以及鏈表長(zhǎng)度計(jì)數(shù)器 len , 而 dup 、 free 和 match 成員則是用于實(shí)現(xiàn)多態(tài)鏈表所需的類型特定函數(shù):
- dup 函數(shù)用于復(fù)制鏈表節(jié)點(diǎn)所保存的值;
- free 函數(shù)用于釋放鏈表節(jié)點(diǎn)所保存的值;
- match 函數(shù)則用于對(duì)比鏈表節(jié)點(diǎn)所保存的值和另一個(gè)輸入值是否相等。
圖 3-2 是由一個(gè) list 結(jié)構(gòu)和三個(gè) listNode 結(jié)構(gòu)組成的鏈表:
Redis 的鏈表實(shí)現(xiàn)的特性可以總結(jié)如下:
- 雙端: 鏈表節(jié)點(diǎn)帶有 prev 和 next 指針, 獲取某個(gè)節(jié)點(diǎn)的前置節(jié)點(diǎn)和后置節(jié)點(diǎn)的復(fù)雜度都是 O(1) 。
- 無環(huán): 表頭節(jié)點(diǎn)的 prev 指針和表尾節(jié)點(diǎn)的 next 指針都指向 NULL , 對(duì)鏈表的訪問以 NULL 為終點(diǎn)。
- 帶表頭指針和表尾指針: 通過 list 結(jié)構(gòu)的 head 指針和 tail 指針, 程序獲取鏈表的表頭節(jié)點(diǎn)和表尾節(jié)點(diǎn)的復(fù)雜度為 O(1) 。
- 帶鏈表長(zhǎng)度計(jì)數(shù)器: 程序使用 list 結(jié)構(gòu)的 len 屬性來對(duì) list 持有的鏈表節(jié)點(diǎn)進(jìn)行計(jì)數(shù), 程序獲取鏈表中節(jié)點(diǎn)數(shù)量的復(fù)雜度為 O(1) 。
- 多態(tài): 鏈表節(jié)點(diǎn)使用 void* 指針來保存節(jié)點(diǎn)值, 并且可以通過 list 結(jié)構(gòu)的 dup 、 free 、 match 三個(gè)屬性為節(jié)點(diǎn)值設(shè)置類型特定函數(shù), 所以鏈表可以用于保存各種不同類型的值。
壓縮列表
壓縮列表(ziplist)是列表鍵和哈希鍵的底層實(shí)現(xiàn)之一。
當(dāng)一個(gè)列表鍵只包含少量列表項(xiàng), 并且每個(gè)列表項(xiàng)要么就是小整數(shù)值, 要么就是長(zhǎng)度比較短的字符串, 那么 Redis 就會(huì)使用壓縮列表來做列表鍵的底層實(shí)現(xiàn)。
比如說, 執(zhí)行以下命令將創(chuàng)建一個(gè)壓縮列表實(shí)現(xiàn)的列表鍵:
redis> RPUSH lst 1 3 5 10086 "hello" "world"
(integer) 6
redis> OBJECT ENCODING lst
"ziplist"
壓縮列表的構(gòu)成
壓縮列表是 Redis 為了節(jié)約內(nèi)存而開發(fā)的, 由一系列特殊編碼的連續(xù)內(nèi)存塊組成的順序型(sequential)數(shù)據(jù)結(jié)構(gòu)。
一個(gè)壓縮列表可以包含任意多個(gè)節(jié)點(diǎn)(entry), 每個(gè)節(jié)點(diǎn)可以保存一個(gè)字節(jié)數(shù)組或者一個(gè)整數(shù)值。
圖 7-1 展示了壓縮列表的各個(gè)組成部分, 表 7-1 則記錄了各個(gè)組成部分的類型、長(zhǎng)度、以及用途。
屬性 | 類型 | 長(zhǎng)度 | 用途 |
---|---|---|---|
zlbytes | uint32_t | 4 字節(jié) | 記錄整個(gè)壓縮列表占用的內(nèi)存字節(jié)數(shù):在對(duì)壓縮列表進(jìn)行內(nèi)存重分配, 或者計(jì)算 zlend 的位置時(shí)使用。 |
zltail | uint32_t | 4 字節(jié) | 記錄壓縮列表表尾節(jié)點(diǎn)距離壓縮列表的起始地址有多少字節(jié): 通過這個(gè)偏移量,程序無須遍歷整個(gè)壓縮列表就可以確定表尾節(jié)點(diǎn)的地址。 |
zllen | uint16_t | 2 字節(jié) | 記錄了壓縮列表包含的節(jié)點(diǎn)數(shù)量: 當(dāng)這個(gè)屬性的值小于 UINT16_MAX (65535)時(shí), 這個(gè)屬性的值就是壓縮列表包含節(jié)點(diǎn)的數(shù)量; 當(dāng)這個(gè)值等于 UINT16_MAX 時(shí), 節(jié)點(diǎn)的真實(shí)數(shù)量需要遍歷整個(gè)壓縮列表才能計(jì)算得出。 |
entryX | 列表節(jié)點(diǎn) | 不定 | 壓縮列表包含的各個(gè)節(jié)點(diǎn),節(jié)點(diǎn)的長(zhǎng)度由節(jié)點(diǎn)保存的內(nèi)容決定。 |
zlend | uint8_t | 1字節(jié) | 特殊值 0xFF (十進(jìn)制 255 ),用于標(biāo)記壓縮列表的末端。 |
圖 7-2 展示了一個(gè)壓縮列表示例:
- 列表 zlbytes 屬性的值為 0x50 (十進(jìn)制 80), 表示壓縮列表的總長(zhǎng)為 80 字節(jié)。
- 列表 zltail 屬性的值為 0x3c (十進(jìn)制 60), 這表示如果我們有一個(gè)指向壓縮列表起始地址的指針 p , 那么只要用指針 p 加上偏移量 60 , 就可以計(jì)算出表尾節(jié)點(diǎn) entry3 的地址。
- 列表 zllen 屬性的值為 0x3 (十進(jìn)制 3), 表示壓縮列表包含三個(gè)節(jié)點(diǎn)。
圖 7-3 展示了另一個(gè)壓縮列表示例:
- 列表 zlbytes 屬性的值為 0xd2 (十進(jìn)制 210), 表示壓縮列表的總長(zhǎng)為 210 字節(jié)。
- 列表 zltail 屬性的值為 0xb3 (十進(jìn)制 179), 這表示如果我們有一個(gè)指向壓縮列表起始地址的指針 p , 那么只要用指針 p 加上偏移量 179 , 就可以計(jì)算出表尾節(jié)點(diǎn) entry5 的地址。
- 列表 zllen 屬性的值為 0x5 (十進(jìn)制 5), 表示壓縮列表包含五個(gè)節(jié)點(diǎn)。
壓縮列表節(jié)點(diǎn)的構(gòu)成
每個(gè)壓縮列表節(jié)點(diǎn)可以保存一個(gè)字節(jié)數(shù)組或者一個(gè)整數(shù)值, 其中, 字節(jié)數(shù)組可以是以下三種長(zhǎng)度的其中一種:
長(zhǎng)度小于等于 63 (2^{6}-1)字節(jié)的字節(jié)數(shù)組;
長(zhǎng)度小于等于 16383 (2^{14}-1) 字節(jié)的字節(jié)數(shù)組;
長(zhǎng)度小于等于 4294967295 (2^{32}-1)字節(jié)的字節(jié)數(shù)組;
而整數(shù)值則可以是以下六種長(zhǎng)度的其中一種:4 位長(zhǎng),介于 0 至 12 之間的無符號(hào)整數(shù);
1 字節(jié)長(zhǎng)的有符號(hào)整數(shù);
3 字節(jié)長(zhǎng)的有符號(hào)整數(shù);
int16_t 類型整數(shù);
int32_t 類型整數(shù);
int64_t 類型整數(shù)。
每個(gè)壓縮列表節(jié)點(diǎn)都由 previous_entry_length 、 encoding 、 content 三個(gè)部分組成, 如圖 7-4 所示。
接下來的內(nèi)容將分別介紹這三個(gè)組成部分。
previous_entry_length
節(jié)點(diǎn)的 previous_entry_length 屬性以字節(jié)為單位, 記錄了壓縮列表中前一個(gè)節(jié)點(diǎn)的長(zhǎng)度。
previous_entry_length 屬性的長(zhǎng)度可以是 1 字節(jié)或者 5 字節(jié):
- 如果前一節(jié)點(diǎn)的長(zhǎng)度小于 254 字節(jié), 那么 previous_entry_length 屬性的長(zhǎng)度為 1 字節(jié): 前一節(jié)點(diǎn)的長(zhǎng)度就保存在這一個(gè)字節(jié)里面。
- 如果前一節(jié)點(diǎn)的長(zhǎng)度大于等于 254 字節(jié), 那么 previous_entry_length 屬性的長(zhǎng)度為 5 字節(jié): 其中屬性的第一字節(jié)會(huì)被設(shè)置為 0xFE (十進(jìn)制值 254), 而之后的四個(gè)字節(jié)則用于保存前一節(jié)點(diǎn)的長(zhǎng)度。
圖 7-5 展示了一個(gè)包含一字節(jié)長(zhǎng) previous_entry_length 屬性的壓縮列表節(jié)點(diǎn), 屬性的值為 0x05 , 表示前一節(jié)點(diǎn)的長(zhǎng)度為 5 字節(jié)。
圖 7-6 展示了一個(gè)包含五字節(jié)長(zhǎng) previous_entry_length 屬性的壓縮節(jié)點(diǎn), 屬性的值為 0xFE00002766 , 其中值的最高位字節(jié) 0xFE 表示這是一個(gè)五字節(jié)長(zhǎng)的 previous_entry_length 屬性, 而之后的四字節(jié) 0x00002766 (十進(jìn)制值 10086 )才是前一節(jié)點(diǎn)的實(shí)際長(zhǎng)度。
因?yàn)楣?jié)點(diǎn)的 previous_entry_length 屬性記錄了前一個(gè)節(jié)點(diǎn)的長(zhǎng)度, 所以程序可以通過指針運(yùn)算, 根據(jù)當(dāng)前節(jié)點(diǎn)的起始地址來計(jì)算出前一個(gè)節(jié)點(diǎn)的起始地址。
舉個(gè)例子, 如果我們有一個(gè)指向當(dāng)前節(jié)點(diǎn)起始地址的指針 c , 那么我們只要用指針 c 減去當(dāng)前節(jié)點(diǎn) previous_entry_length 屬性的值, 就可以得出一個(gè)指向前一個(gè)節(jié)點(diǎn)起始地址的指針 p , 如圖 7-7 所示。
壓縮列表的從表尾向表頭遍歷操作就是使用這一原理實(shí)現(xiàn)的: 只要我們擁有了一個(gè)指向某個(gè)節(jié)點(diǎn)起始地址的指針, 那么通過這個(gè)指針以及這個(gè)節(jié)點(diǎn)的 previous_entry_length 屬性, 程序就可以一直向前一個(gè)節(jié)點(diǎn)回溯, 最終到達(dá)壓縮列表的表頭節(jié)點(diǎn)。
圖 7-8 展示了一個(gè)從表尾節(jié)點(diǎn)向表頭節(jié)點(diǎn)進(jìn)行遍歷的完整過程:
- 首先,我們擁有指向壓縮列表表尾節(jié)點(diǎn) entry4 起始地址的指針 p1 (指向表尾節(jié)點(diǎn)的指針可以通過指向壓縮列表起始地址的指針加上 zltail 屬性的值得出);
- 通過用 p1 減去 entry4 節(jié)點(diǎn) previous_entry_length 屬性的值, 我們得到一個(gè)指向 entry4 前一節(jié)點(diǎn) entry3 起始地址的指針 p2 ;
- 通過用 p2 減去 entry3 節(jié)點(diǎn) previous_entry_length 屬性的值, 我們得到一個(gè)指向 entry3 前一節(jié)點(diǎn) entry2 起始地址的指針 p3 ;
- 通過用 p3 減去 entry2 節(jié)點(diǎn) previous_entry_length 屬性的值, 我們得到一個(gè)指向 entry2 前一節(jié)點(diǎn) entry1 起始地址的指針 p4 , entry1 為壓縮列表的表頭節(jié)點(diǎn);
- 最終, 我們從表尾節(jié)點(diǎn)向表頭節(jié)點(diǎn)遍歷了整個(gè)列表。
encoding
節(jié)點(diǎn)的 encoding 屬性記錄了節(jié)點(diǎn)的 content 屬性所保存數(shù)據(jù)的類型以及長(zhǎng)度:
- 一字節(jié)、兩字節(jié)或者五字節(jié)長(zhǎng), 值的最高位為 00 、 01 或者 10 的是字節(jié)數(shù)組編碼: 這種編碼表示節(jié)點(diǎn)的 content 屬性保存著字節(jié)數(shù)組, 數(shù)組的長(zhǎng)度由編碼除去最高兩位之后的其他位記錄;
- 一字節(jié)長(zhǎng), 值的最高位以 11 開頭的是整數(shù)編碼: 這種編碼表示節(jié)點(diǎn)的 content 屬性保存著整數(shù)值, 整數(shù)值的類型和長(zhǎng)度由編碼除去最高兩位之后的其他位記錄;
表 7-2 記錄了所有可用的字節(jié)數(shù)組編碼, 而表 7-3 則記錄了所有可用的整數(shù)編碼。 表格中的下劃線 _ 表示留空, 而 b 、 x 等變量則代表實(shí)際的二進(jìn)制數(shù)據(jù), 為了方便閱讀, 多個(gè)字節(jié)之間用空格隔開。
表 7-2 字節(jié)數(shù)組編碼
編碼 | 編碼長(zhǎng)度 | content 屬性保存的值 |
---|---|---|
00bbbbbb | 1 字節(jié) | 長(zhǎng)度小于等于 63 字節(jié)的字節(jié)數(shù)組。 |
01bbbbbb xxxxxxxx | 2 字節(jié) | 長(zhǎng)度小于等于 16383 字節(jié)的字節(jié)數(shù)組。 |
10______ aaaaaaaa bbbbbbbb cccccccc dddddddd | 5 字節(jié) | 長(zhǎng)度小于等于 4294967295 的字節(jié)數(shù)組。 |
表 7-3 整數(shù)編碼
編碼 | 編碼長(zhǎng)度 | content 屬性保存的值 |
---|---|---|
11000000 | 1 字節(jié) | int16_t 類型的整數(shù)。 |
11010000 | 1 字節(jié) | int32_t 類型的整數(shù)。 |
11100000 | 1 字節(jié) | int64_t 類型的整數(shù)。 |
11110000 | 1 字節(jié) | 24 位有符號(hào)整數(shù)。 |
11111110 | 1 字節(jié) | 8 位有符號(hào)整數(shù)。 |
1111xxxx | 1 字節(jié) | 使用這一編碼的節(jié)點(diǎn)沒有相應(yīng)的 content 屬性, 因?yàn)榫幋a本身的 xxxx 四個(gè)位已經(jīng)保存了一個(gè)介于 0 和 12 之間的值, 所以它無須 content 屬性。 |
content
節(jié)點(diǎn)的 content 屬性負(fù)責(zé)保存節(jié)點(diǎn)的值, 節(jié)點(diǎn)值可以是一個(gè)字節(jié)數(shù)組或者整數(shù), 值的類型和長(zhǎng)度由節(jié)點(diǎn)的 encoding 屬性決定。
圖 7-9 展示了一個(gè)保存字節(jié)數(shù)組的節(jié)點(diǎn)示例:
- 編碼的最高兩位 00 表示節(jié)點(diǎn)保存的是一個(gè)字節(jié)數(shù)組;
- 編碼的后六位 001011 記錄了字節(jié)數(shù)組的長(zhǎng)度 11 ;
- content 屬性保存著節(jié)點(diǎn)的值 "hello world" 。
圖 7-10 展示了一個(gè)保存整數(shù)值的節(jié)點(diǎn)示例:
- 編碼 11000000 表示節(jié)點(diǎn)保存的是一個(gè) int16_t 類型的整數(shù)值;
- content 屬性保存著節(jié)點(diǎn)的值 10086 。