redis基礎數據結構 之 ziplist壓縮鏈表
給新觀衆老爺的開場
大家好,我是弟弟!
最近讀了一遍 黃健宏大佬的 <<Redis 設計與實現>>,對Redis 3.0版本有了一些認識,該書作者有一版添加了註釋的 redis 3.0源碼
👉官方redis的github傳送門。
👉黃健宏大佬添加了註釋的 redis 3.0源碼傳送門
網上說Redis代碼寫得很好,爲了加深印象和學習redis大佬的代碼寫作藝術,瞭解工作中使用的redis 命令背後的源碼邏輯,便有了從redis命令角度學習redis源碼的想法。
(全文提到的redis服務器,都指在 mac os 上啓動的一個默認配置的單機redis服務器)
ziplist是什麼?
redis的鏈表數據類型,在底層有兩種數據結構實現.
一種是上一篇講到list數據結構,
另外一種就是 壓縮鏈表 ziplist
。
ziplist的結構較爲複雜,產生了一系列相關的稍顯複雜的代碼。
但從功能層面看 ziplist,list 都能實現鏈表的功能。
那讓我們來看看ziplist具體是什麼
弟弟:“從名字上看,壓縮列表,就是一個被壓縮了的列表”
觀衆老爺: “…,要你有何用,你不說我們也能看出來。😡”
弟弟:"…不好意思,打擾了🙃️"
弟弟: “redis的壓縮列表,存儲在連續的內存空間中,沒有代碼層面的數據結構定義,依賴設計好的二進制格式來進行操作。
”
觀衆老爺:“你這講得不夠專業啊,也不形象。”
弟弟:“😭,好的瞭解。
那讓我們來 結合 redis裏的英文註釋 和 黃健宏大佬的中文註釋 來看看是怎麼講的”
(ps: 該註釋出現在 ziplist.c 文件中)
ziplist的設計目的
👇
* The ziplist is a specially encoded dually linked list that is designed
* to be very memory efficient.
* Ziplist 是爲了儘可能地節約內存而設計的特殊編碼雙端鏈表。
ziplist的作用
👇
* It stores both strings and integer values,
* where integers are encoded as actual integers instead of a series of
* characters.
* Ziplist 可以儲存字符串值和整數值,
* 其中,整數值被保存爲實際的整數,而不是字符數組。
ziplist的小瑕疵
👇
* It allows push and pop operations on either side of the list
* in O(1) time. However, because every operation requires a reallocation of
* the memory used by the ziplist, the actual complexity is related to the
* amount of memory used by the ziplist.
* Ziplist 允許在列表的兩端進行 O(1) 複雜度的 push 和 pop 操作。
* 但是,因爲這些操作都需要對整個 ziplist 進行內存重分配,
* 所以實際的複雜度和 ziplist 佔用的內存大小有關。
ziplist 的空間佈局
ziplist的空間佈局,按低地址向高地址的方向 如下
- 開頭用
四個字節來存放ziplist的字節數大小
- 接着用
四個字節來存放ziplist尾節點的偏移字節數
- 然後是
2個字節表示該ziplist裏放了多少個節點
- 接着就是
<節點>,<節點>,<節點>... 0到n個
- 最後是
一個字節的ziplist結束標誌 255
👇看一遍註釋
* ZIPLIST OVERALL LAYOUT:
* Ziplist 的整體佈局:
* The general layout of the ziplist is as follows:
* 以下是 ziplist 的一般佈局:
* <zlbytes><zltail><zllen><entry><entry><zlend>
zlbytes的含義
* <zlbytes> is an unsigned integer to hold the number of bytes that the
* ziplist occupies. This value needs to be stored to be able to resize the
* entire structure without the need to traverse it first.
* <zlbytes> 是一個無符號整數,保存着 ziplist 使用的內存數量。
* 通過這個值,程序可以直接對 ziplist 的內存大小進行調整,
* 而無須爲了計算 ziplist 的內存大小而遍歷整個列表。
zltail的含義
* <zltail> is the offset to the last entry in the list. This allows a pop
* operation on the far side of the list without the need for full traversal.
* <zltail> 保存着到達列表中最後一個節點的偏移量。
* 這個偏移量使得對錶尾的 pop 操作可以在無須遍歷整個列表的情況下進行。
zllen的含義
* <zllen> is the number of entries.When this value is larger than 2**16-2,
* we need to traverse the entire list to know how many items it holds.
* <zllen> 保存着列表中的節點數量。
* 當 zllen 保存的值大於 2**16-2 時,(大於等於 65535 時)
* 程序需要遍歷整個列表才能知道列表實際包含了多少個節點。
zlend的含義
* <zlend> is a single byte special value, equal to 255, which indicates the
* end of the list.
* <zlend> 的長度爲 1 字節,值爲 255 ,標識列表的末尾。
觀衆老爺:“wo*, 這個ziplist的格式有點複雜啊”
弟弟:“是啊,有點複雜,還有ziplist的節點格式沒說呢 ,
ziplist的節點格式 及 相關代碼 是造成ziplist複雜的真正元兇🙃️”
ziplist的節點格式
ziplist節點
的空間佈局,分爲兩部分部分。
- header
1.1 描述前一個節點佔用的空間大小
1.2 描述本節點的值的類型,以及值的長度 - 值本身
詳細結構,按低地址向高地址的方向 如下
-
1or5個字節
用於描述前面一個節點的 字節數大小
。
若第1個字節 小於#define ZIP_BIGLEN 254
,則該字節直接表示前一個節點的字節數大小
否則爲5個字節,第2,3,4,5字節一起構成一個 32爲整數表示前一節點的大小 -
緊接着的
1or2or5 字節
一共表示了三類信息
1-當前節點值類型
2-描述當前節點值的長度所需要的空間大小
3-當前節點值的長度
這也太能省了,通過提高結構的複雜程度弄暈計算機來節省存儲空間 🙃️2.1 第一個字節叫
encoding
如果 encoding 小於#define ZIP_STR_MASK 0xc0
(二進制 11000000),
那該節點存的是字符串類型。
那麼 描述該字符串長度所需要的存儲空間大小的類型 就是 encoding & ZIP_STR_MASK
取值分別以下三種,`/** 字符串編碼類型 #define ZIP_STR_MASK 0xc0 //0xc0 11000000 #define ZIP_STR_06B (0 << 6) // 00000000 #define ZIP_STR_14B (1 << 6) // 01000000 #define ZIP_STR_32B (2 << 6) // 10000000 `
6B,14B,32B的意思是,描述字符串長度所需要的空間分別是6位/14位/32位,
且描述字符串的長度,是按大端序存放的,且前6位存放在了encoding裏,額外的空間緊接着encoding
所以描述這三類信息, 字符串類型、存儲字符串長度的空間大小,以及字符串的長度 一共需要 1/2/5個字節
如果一下就看懂了,那說明你真是個聰明的觀衆老爺呢😊
🙃️如果看暈了,沒關係,我們對着整數類型再來一遍😊如果 encoding 大於等於
#define ZIP_STR_MASK 0xc0
(二進制 11000000)
那該節點的值是一個整數
那麼 描述該字符串長度所需要的存儲空間大小 就是 encoding本身 👇/* * 整數編碼類型 * */ * #define ZIP_INT_16B (0xc0 | 0 << 4) // 11000000 * #define ZIP_INT_32B (0xc0 | 1 << 4) // 11010000 * #define ZIP_INT_64B (0xc0 | 2 << 4) // 11100000 * #define ZIP_INT_24B (0xc0 | 3 << 4) // 11110000 * #define ZIP_INT_8B 0xfe // 0xFE 11111110
8B/16B/24B/32B/64B 對應的 值的長度 分別是 (8位/16位/24位/32位/64位)
所以整數類型與字符串類型不同的地方在於
ziplist整數類型節點,僅需要一個字節,就可以描述存儲的值的是整數類型,和值佔用的空間字節數
整數還有一個隱藏類型,如果是整數[0,12], 則將值直接保存到了encoding中😏
encoding取值在下面兩個值之間(包含),那麼encoding - ZIP_INT_IMM_MIN - 1 的結果就直接是整數值,自然地該節點不需要再存儲一份值。
#define ZIP_INT_IMM_MIN 0xf1 /* 11110001 */
#define ZIP_INT_IMM_MAX 0xfd /* 11111101 */
-
第三部分就是節點的值本身啦,在3.0版本中值被本身是沒有經過壓縮處理的。
瞭解了ziplist節點的結構,可以知道,ziplist是可以通過頭尾指針雙向遍歷的
下面是 ziplist節點格式的 官方描述👇
* ZIPLIST ENTRIES:
* ZIPLIST 節點:
* Every entry in the ziplist is prefixed by a header that contains two pieces
* of information. First, the length of the previous entry is stored to be
* able to traverse the list from back to front. Second, the encoding with an
* optional string length of the entry itself is stored.
* 每個 ziplist 節點的前面都帶有一個 header ,這個 header 包含兩部分信息:
* 1)前置節點的長度,在程序從後向前遍歷時使用。
* 2)當前節點所保存的值的類型和長度。
*
* The length of the previous entry is encoded in the following way:
* If this length is smaller than 254 bytes, it will only consume a single
* byte that takes the length as value. When the length is greater than or
* equal to 254, it will consume 5 bytes. The first byte is set to 254 to
* indicate a larger value is following. The remaining 4 bytes take the
* length of the previous entry as value.
* 編碼前置節點的長度的方法如下:
* 1) 如果前置節點的長度小於 254 字節,那麼程序將使用 1 個字節來保存這個長度值。
* 2) 如果前置節點的長度大於等於 254 字節,那麼程序將使用 5 個字節來保存這個長度值:
* a) 第 1 個字節的值將被設爲 254 ,用於標識這是一個 5 字節長的長度值。
* b) 之後的 4 個字節則用於保存前置節點的實際長度。
*
* The other header field of the entry itself depends on the contents of the
* entry. When the entry is a string, the first 2 bits of this header will hold
* the type of encoding used to store the length of the string, followed by the
* actual length of the string. When the entry is an integer the first 2 bits
* are both set to 1. The following 2 bits are used to specify what kind of
* integer will be stored after this header. An overview of the different
* types and encodings is as follows:
* header 另一部分的內容和節點所保存的值有關。
* 1) 如果節點保存的是字符串值,
* 那麼這部分 header 的頭 2 個位將保存編碼字符串長度所使用的類型,
* 而之後跟着的內容則是字符串的實際長度。
* |00pppppp| - 1 byte
* String value with length less than or equal to 63 bytes (6 bits).
* 字符串的長度小於或等於 63 字節。
*
* |01pppppp|qqqqqqqq| - 2 bytes
* String value with length less than or equal to 16383 bytes (14 bits).
* 字符串的長度小於或等於 16383 字節。
* |10______|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt| - 5 bytes
* String value with length greater than or equal to 16384 bytes.
* 字符串的長度大於或等於 16384 字節。
*
* 2) 如果節點保存的是整數值,
* 那麼這部分 header 的頭 2 位都將被設置爲 1 ,
* 而之後跟着的 2 位則用於標識節點所保存的整數的類型。
*
* |11000000| - 1 byte
* Integer encoded as int16_t (2 bytes).
* 節點的值爲 int16_t 類型的整數,長度爲 2 字節。
* |11010000| - 1 byte
* Integer encoded as int32_t (4 bytes).
* 節點的值爲 int32_t 類型的整數,長度爲 4 字節。
* |11100000| - 1 byte
* Integer encoded as int64_t (8 bytes).
* 節點的值爲 int64_t 類型的整數,長度爲 8 字節。
* |11110000| - 1 byte
* Integer encoded as 24 bit signed (3 bytes).
* 節點的值爲 24 位(3 字節)長的整數。
* |11111110| - 1 byte
* Integer encoded as 8 bit signed (1 byte).
* 節點的值爲 8 位(1 字節)長的整數。
* |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.
* 節點的值爲介於 0 至 12 之間的無符號整數。
* 因爲 0000 和 1111 都不能使用,所以位的實際值將是 1 至 13 。
* 程序在取得這 4 個位的值之後,還需要減去 1 ,才能計算出正確的值。
* 比如說,如果位的值爲 0001 = 1 ,那麼程序返回的值將是 1 - 1 = 0 。
* |11111111| - End of ziplist.
* ziplist 的結尾標識
*
* All the integers are represented in little endian byte order.
*
* 所有整數都表示爲小端字節序。
ziplist insert!
redis是有 key/value 數據庫的說法的。
既然ziplist這麼複雜,那我們就簡單一點,站在crud的角度擼它一下。
在創建ziplist 並 插入一個節點之前。
我們來看一些ziplist相關的源碼,回憶一下ziplist的結構
ziplist上的基本操作
- 查看一個ziplist的大小 (佔用的字節數)
// 定位到 ziplist 的 bytes 屬性,該屬性記錄了整個 ziplist 所佔用的內存字節數
// 用於取出 bytes 屬性的現有值,或者爲 bytes 屬性賦予新值
#define ZIPLIST_BYTES(zl) (*((uint32_t *)(zl)))
- 查看ziplist的尾節點的偏移量
// 定位到 ziplist 的 offset 屬性,該屬性記錄了到達表尾節點的偏移量
// 用於取出 offset 屬性的現有值,或者爲 offset 屬性賦予新值
#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t *)((zl) + sizeof(uint32_t))))
- 查看ziplist的節點個數
// 定位到 ziplist 的 length 屬性,該屬性記錄了 ziplist 包含的節點數量
// 用於取出 length 屬性的現有值,或者爲 length 屬性賦予新值
#define ZIPLIST_LENGTH(zl) (*((uint16_t *)((zl) + sizeof(uint32_t) * 2)))
- 查看 ziplist 表頭的大小
// 返回 ziplist 表頭的大小
#define ZIPLIST_HEADER_SIZE (sizeof(uint32_t) * 2 + sizeof(uint16_t))
- 查看 ziplist 的第一個節點
// 返回指向 ziplist 第一個節點(的起始位置)的指針
#define ZIPLIST_ENTRY_HEAD(zl) ((zl) + ZIPLIST_HEADER_SIZE)
- 查看 ziplist 最後一個節點
// 返回指向 ziplist 最後一個節點(的起始位置)的指針
#define ZIPLIST_ENTRY_TAIL(zl) ((zl) + intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)))
- 查看 ziplist 的最後一個字節(結束標誌)
// 返回指向 ziplist 末端 ZIP_END (的起始位置)的指針
#define ZIPLIST_ENTRY_END(zl) ((zl) + intrev32ifbe(ZIPLIST_BYTES(zl)) - 1)
8.再來看下 黃健宏大佬給的ziplist結構示意圖👇
`/*
空白 ziplist 示例圖
area |<---- ziplist header ---->|<-- end -->|
size 4 bytes 4 bytes 2 bytes 1 byte
+---------+--------+-------+-----------+
component | zlbytes | zltail | zllen | zlend |
| | | | |
value | 1011 | 1010 | 0 | 1111 1111 |
+---------+--------+-------+-----------+
^
|
ZIPLIST_ENTRY_HEAD
&
address ZIPLIST_ENTRY_TAIL
&
ZIPLIST_ENTRY_END
非空 ziplist 示例圖
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
*/`
ziplist 節點上的基本操作
再正式insert前,再來看幾個 ziplist節點的 操作
- header中的前置節點的基本操作
1.1 已知ziplist節點 指針ptr,求描述前置節點長度所需要的空間大小
/* Decode the number of bytes required to store the length of the previous
* element, from the perspective of the entry pointed to by 'ptr'.
* 解碼 ptr 指針,
* 取出編碼前置節點長度所需的字節數,並將它保存到 prevlensize 變量中。
*/
#define ZIP_BIGLEN 254
#define ZIP_DECODE_PREVLENSIZE(ptr, prevlensize) \
do \
{ \
if ((ptr)[0] < ZIP_BIGLEN) \
{ \
(prevlensize) = 1; \
} \
else \
{ \
(prevlensize) = 5; \
} \
} while (0);
1.2 已知ziplist節點的 指針ptr,以及描述前置節點長度所需要的空間大小, 求前置節點長度
/* Decode the length of the previous element, from the perspective of the entry
* pointed to by 'ptr'.
* 解碼 ptr 指針,
* 取出編碼前置節點長度所需的字節數,
* 並將這個字節數保存到 prevlensize 中。
* 然後根據 prevlensize ,從 ptr 中取出前置節點的長度值,
* 並將這個長度值保存到 prevlen 變量中。
* T = O(1)
*/
#define ZIP_DECODE_PREVLEN(ptr, prevlensize, prevlen) \
do \
{ \
/* 先計算被編碼長度值的字節數 */ \
ZIP_DECODE_PREVLENSIZE(ptr, prevlensize); \
/* 再根據編碼字節數來取出長度值 */ \
if ((prevlensize) == 1) \
{ \
(prevlen) = (ptr)[0]; \
} \
else if ((prevlensize) == 5) \
{ \
assert(sizeof((prevlensize)) == 4); \
memcpy(&(prevlen), ((char *)(ptr)) + 1, 4); \
memrev32ifbe(&prevlen);//如果是大端序,則轉成小端序 \
} \
} while (0);
- header中本節點值的類型與長度的基本操作
2.1 已知 ptr (ziplist節點指針 + 存放前置節點信息佔用的空間大小), 求節點值的編碼類型
/* Extract the encoding from the byte pointed by 'ptr' and set it into
* 'encoding'.
* 從 ptr 中取出節點值的編碼類型,並將它保存到 encoding 變量中。
*/
#define ZIP_STR_MASK 0xc0 //0xc0 11000000 1個字節
#define ZIP_ENTRY_ENCODING(ptr, encoding) \
do \
{ \
(encoding) = (ptr[0]); \
if ((encoding) < ZIP_STR_MASK) \
(encoding) &= ZIP_STR_MASK; \
} while (0)
2.2 已知 ptr (ziplist節點指針 + 存放前置節點信息佔用的空間大小), 和節點值的編碼類型
求存放節點值長度的空間大小,以及節點值的長度
/* Decode the length encoded in 'ptr'. The 'encoding' variable will hold the
* entries encoding, the 'lensize' variable will hold the number of bytes
* required to encode the entries length, and the 'len' variable will hold the
* entries length.
* 解碼 ptr 指針,取出列表節點的相關信息,並將它們保存在以下變量中:
* - encoding 保存節點值的編碼類型。
* - lensize 保存編碼節點長度所需的字節數。
* - len 保存節點的長度。
*/
#define ZIP_DECODE_LENGTH(ptr, encoding, lensize, len) \
do \
{ \
/* 取出值的編碼類型 */ \
ZIP_ENTRY_ENCODING((ptr), (encoding)); \
/* 字符串編碼 */ \
if ((encoding) < ZIP_STR_MASK) \
{ \
if ((encoding) == ZIP_STR_06B) \
{ \
(lensize) = 1; \
(len) = (ptr)[0] & 0x3f; /* 編碼長度1字節長 00111111*/ \
} \
else if ((encoding) == ZIP_STR_14B) \
{ \
(lensize) = 2; /* 編碼長度2字節長 */ \
(len) = (((ptr)[0] & 0x3f) << 8) | (ptr)[1]; \
} \
else if (encoding == ZIP_STR_32B) \
{ \
(lensize) = 5; /* 編碼長度5字節長,實際長度是後4個字節 */ \
(len) = ((ptr)[1] << 24) | \
((ptr)[2] << 16) | \
((ptr)[3] << 8) | \
((ptr)[4]); \
} \
else \
{ \
assert(NULL); \
} \
/* 整數編碼 */ \
} \
else \
{ \
(lensize) = 1; \
(len) = zipIntSize(encoding); \
} \
} while (0);
static unsigned int zipIntSize(unsigned char encoding)
{
switch (encoding)
{
case ZIP_INT_8B:
return 1;
case ZIP_INT_16B:
return 2;
case ZIP_INT_24B:
return 3;
case ZIP_INT_32B:
return 4;
case ZIP_INT_64B:
return 8;
default:
return 0; /* 4 bit immediate */
}
assert(NULL);
return 0;
}
2.3 已知p指向一個ziplist節點,求ziplist節點的大小就很簡單了👇
/* Return the total number of bytes used by the entry pointed to by 'p'.
* 返回指針 p 所指向的節點佔用的字節數總和。
*/
static unsigned int zipRawEntryLength(unsigned char *p)
{
unsigned int prevlensize, encoding, lensize, len;
// 取出編碼前置節點的長度所需的字節數
ZIP_DECODE_PREVLENSIZE(p, prevlensize);
// 取出當前節點值的編碼類型,編碼節點值長度所需的字節數,以及節點值的長度
ZIP_DECODE_LENGTH(p + prevlensize, encoding, lensize, len);
// 計算節點佔用的字節數總和
return prevlensize + lensize + len;
}
好了,到這裏我們知道了 ziplist節點的結構、ziplist節點header的讀取方法。
我們可以接着看整個ziplist節點是怎麼讀取的了。
因爲ziplist結構是緊湊型的一整塊兒數據,所以爲了臨時讀取ziplist節點,弄了一個zlentry結構出來。
`/*
* 保存 ziplist 節點信息的結構
*/
typedef struct zlentry
{
// prevrawlen :前置節點的長度
// prevrawlensize :編碼 prevrawlen 所需的字節大小
unsigned int prevrawlensize, prevrawlen;
// len :當前節點值的長度
// lensize :編碼 len 所需的字節大小
unsigned int lensize, len;
// 當前節點 header 的大小
// 等於 prevrawlensize + lensize
unsigned int headersize;
// 當前節點值所使用的編碼類型
unsigned char encoding;
// 指向當前節點的指針
unsigned char *p;
} zlentry;
已知p指向一個ziplist節點,讀取ziplist節點👇
/* Return a struct with all information about an entry.
*
* 將 p 所指向的列表節點的信息全部保存到 zlentry 中,並返回該 zlentry 。
*
* T = O(1)
*/
static zlentry zipEntry(unsigned char *p)
{
zlentry e;
// e.prevrawlensize 保存着編碼前一個節點的長度所需的字節數
// e.prevrawlen 保存着前一個節點的長度
// T = O(1)
ZIP_DECODE_PREVLEN(p, e.prevrawlensize, e.prevrawlen);
// p + e.prevrawlensize 將指針移動到列表節點本身
// e.encoding 保存着節點值的編碼類型
// e.lensize 保存着編碼節點值長度所需的字節數
// e.len 保存着節點值的長度
// T = O(1)
ZIP_DECODE_LENGTH(p + e.prevrawlensize, e.encoding, e.lensize, e.len);
// 計算頭結點的字節數
e.headersize = e.prevrawlensize + e.lensize;
// 記錄指針
e.p = p;
return e;
}
ziplist節點插入流程
因爲ziplist是一個鏈表,給一個鏈表節點後插入一個新的鏈表節點,是一個比較好理解的事情。
但是ziplist是在連續空間存放的
插入ziplist節點,跟在數組中插入一個元素有點類似。
需要將ziplist後面部分空間整體向後移動,給新ziplist節點騰出空間。
但ziplist畢竟不是數組,它有一些特點…
在位置p插入一個新的ziplist節點的流程
- 計算新節點的大小 (header+值), 記爲 reqlen
- 計算後置節點header中(新節點後面的一個節點),存放新加入節點需要的額外空間 nextdiff
nextdiff = 新值-舊值 - 對ziplist進行空間分配 在原大小的基礎上 加上 新節點大小 以及 nextdiff (0 or 4字節)
- 將p + nextdiff 到 原ziplist結束標記之間的值,整體移動到 從p+reqlen開始的空間
- 修正ziplist結構中,各個元素的值。比如 尾節點偏移量,元素個數等
因加入一個ziplist節點可能會影響到後置節點header中存放前置節點長度的空間大小和值
後置節點header中存放前置節點長度的空間可能變大(該空間只變大,不縮小)
將有可能對後續所有節點產生連鎖影響
於是, 一輪 對後續所有節點header中存放前置節點長度的空間與值 的修正將被觸發…
好吧,不得不說ziplist是有點小複雜。
因其結構稍顯複雜,代碼邏輯有點小繞,
本弟弟也是看了好久的ziplist節點的insert代碼才搞清楚是怎麼回事。
插入ziplist節點完整詳細的流程可以 看👉黃健宏大佬添加了註釋的 redis 3.0源碼 中的
ziplist.c/ziplistInsert 函數。 😂
ziplist搞這麼複雜,有用嗎?
既然redis作者說了,搞這個ziplist是爲了節省空間的。
ziplist、list 在3.0版本 對值都沒有進行壓縮。
所以節省的空間 是ziplist、list結構本身所佔用的空間
。
那麼我們就來看一下ziplist,list在相同個數以及相同大小的值的情況下,結構佔用的空間大小。
ziplist結構相比list結構的空間節省比例
從lpush命令對應的源碼可以知道
當創建一個新的鏈表時,默認創建一個ziplist。
當ziplist中的元素個數超過512個,或者元素值長度超過64字節時,(默認配置,可通過配置文件修改)
redis將ziplist轉換成list來存儲,
(因爲ziplist的結構特性,使得元素個數過大,或者元素值過大時,帶來額外的內存操作影響性能)
結論
-
512個元素 長度爲 1到63個字節 的情況下,節省空間比例高達
91.6%
ziplist結構本身佔用1035
字節空間,list結構本身佔用12336
字節,ziplist的1035字節 = 4+4+2+2*512+1 (回憶一下ziplist的結構)
list 的 12336字節= 48 + 24*512 (回憶一下list的結構)
(ps: 64位系統上,任意一個指針變量佔用的空間是8個字節)
小結
- 鏈表的特性有一點像go語言裏的channel,一頭寫入,一頭讀取
- 可以當成隊列/棧用
- 做一些跟時間線相關的實時列表,
比如最近10位閱讀本文的觀衆老爺,或者
遊戲裏的最近充值VIP的100位RMB玩家。😂 - 可以做一些非實時的具有分頁需求的列表,
比如直播平臺的人氣/打賞 TOP100. 小時榜,日榜,周榜。 - 消息隊列的話,還是用專業的好一點。🙃️
- 另外,似乎在3.2版本之後,
在鏈表創建的時候,默認創建的是快速鏈表
是的 ziplist跟list合體進化成了quicklist,似乎數據還能壓縮呢