添加ziplist (#15)

This commit is contained in:
LingZhaoHui 2022-12-03 21:57:03 +08:00 committed by GitHub
parent 9d96f2af2e
commit 1125276577
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 146 additions and 7 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
*.backup
*.html

View File

@ -1,11 +1,11 @@
## 简介
# 简介
压缩列表ziplist本质上就是一个字节数组是Redis为了节约内存而设计的一种线性数据结构可以包含多个元素每个元素可以是一个字节数组或一个整数。
Redis的有序集合、散列和列表都直接或者间接使用了压缩列表。当有序集合或散列表的元素个数比较少且元素都是短字符串时Redis便使用压缩列表作为其底层数据存储结构。列表使用快速链表quicklist数据结构存储而快速链表就是双向链表与压缩列表的组合。
ziplist 压缩列表是一个特殊编码的双端链表内存上连续为了尽可能节省内存而设计的。ziplist 可以存储字符串或者整数值其中整数被编码保存为实际的整数而不是字符数组。ziplist 支持 O(1) 的时间复杂度在列表的两端进行 push 和 pop 操作。然而因为这些操作都需要对整个 ziplist 进行内存重分配(因为是一块连续的内存),所以操作的实际复杂度和 ziplist 占用的内存大小有关。在 7.0 版本里ziplist 已经全面被 listpack 替换了(主要是因为连锁更新较影响性能)
## 压缩列表的存储结构
# 压缩列表的存储结构
Redis使用字节数组表示一个压缩列表压缩列表结构如下所示
@ -21,7 +21,7 @@ Redis使用字节数组表示一个压缩列表压缩列表结构如下所示
- entryX压缩列表存储的元素可以是字节数组或者整数长度不限。entry的编码结构将在后面详细介绍。
- zlend 是一个 8 位无符号整数1 byte是一个特殊的标志位来标记压缩列表的结尾0xFF(十进制表示为: 255)。其它正常节点不会有以这个字节开头的,在遍历 ziplist 的时候通过这个标记来判断是否遍历结束。
### 元素的存储结构
## 元素的存储结构
压缩列表元素的存储结构如下所示:
```
@ -83,7 +83,7 @@ Redis 常见的encoding
#define ZIP_INT_8B 0xfe
```
## 解码结构体
# 解码结构体
对于压缩列表中的任意元素获取前一个元素的长度、判断存储的数据类型、获取数据内容等都需要经过复杂的解码运算。解码后的结果应该被缓存起来为此定义了结构体zlentry用于表示解码后的压缩列表元素单纯的用来临时存储解码之后的元素信息。
```c
@ -120,7 +120,7 @@ static inline void zipEntry(unsigned char *p, zlentry *e) {
解码主要分为下面几个步骤:
### 解码前节点长度
## 解码前节点长度
根据 p 目前的指针,获取 entry 的 prevlen 的值;
- 如果prevlen一个字节编码对应字节 (ptr)[0] 的值就是 prevlen。
- 如果prevlen五个字节编码具体的 prevlen 是存储在后四个字节,后四个字节进行位运算获得实际的 prevlen
@ -149,9 +149,147 @@ static inline void zipEntry(unsigned char *p, zlentry *e) {
} while(0)
```
### 解码encoding
## 解码encoding
p+prevrawlensize 位置的第一个字节,获取 entry 当前的 encoding 属性,保存在 encoding 变量中时间复杂度 O(1)。
```c
#define ZIP_ENTRY_ENCODING(ptr, encoding) do { \
(encoding) = ((ptr)[0]); \
if ((encoding) < ZIP_STR_MASK) (encoding) &= ZIP_STR_MASK; \
} while(0)
```
## 解码长度
p+prevrawlensize 根据 encoding 获取 entry 的 len 相关属性。 `ptr[0]<11000000`说明是字节数组,前两个比特为字节数组编码类型
> 进制转换echo "ibase=16;obase=2;C0" | bc
>
```c
#define ZIP_DECODE_LENGTH(ptr, encoding, lensize, len) do { \
if ((encoding) < ZIP_STR_MASK) { \
if ((encoding) == ZIP_STR_06B) { \
(lensize) = 1; \
(len) = (ptr)[0] & 0x3f; \
} else if ((encoding) == ZIP_STR_14B) { \
(lensize) = 2; \
(len) = (((ptr)[0] & 0x3f) << 8) | (ptr)[1]; \
} else if ((encoding) == ZIP_STR_32B) { \
(lensize) = 5; \
(len) = ((uint32_t)(ptr)[1] << 24) | \
((uint32_t)(ptr)[2] << 16) | \
((uint32_t)(ptr)[3] << 8) | \
((uint32_t)(ptr)[4]); \
} else { \
(lensize) = 0; \
(len) = 0; \
} \
} else { \
(lensize) = 1; \
if ((encoding) == ZIP_INT_8B) (len) = 1; \
else if ((encoding) == ZIP_INT_16B) (len) = 2; \
else if ((encoding) == ZIP_INT_24B) (len) = 3; \
else if ((encoding) == ZIP_INT_32B) (len) = 4; \
else if ((encoding) == ZIP_INT_64B) (len) = 8; \
else if (encoding >= ZIP_INT_IMM_MIN && encoding <= ZIP_INT_IMM_MAX) \
(len) = 0; \
else \
(lensize) = (len) = 0; \
} \
} while(0)
```
# 基本操作
主要介绍压缩列表的基本操作,包括创建压缩列表,遍历元素,插入元素,删除元素,修改元素等。
## 创建压缩列表
创建一个空的压缩列表:只对 `lbytes、zltail、zllen、zlend`四个字段进行初始化。初始化过程如下:
- 计算空ziplist的长度并且申请内存`zlbytes`和`zltail`的类型是32位无符号整数`zllen`是16位无符号整数所以总长度为`zlbytes(4) + zltail(4) + zllen(2) = 10 bytes`
- 将总字节数写入内存。zl 既为 ziplist 的起始地址,其中值又负责记录 ziplist 的总字节长度zlbytes 编码存储固定 4 字节,也就代表了一个 ziplist 总字节最大为为 (2^32)-1 字节。
- 将到尾节点的偏移量写进内存,因为是刚初始化的 ziplist偏移量其实就是 HEADER_SIZE 值,此时它刚好指向 zlend因此能够以 O(1) 时间复杂度快速在尾部进行 push 或 pop 操作。
- 写入节点数量0
- 最后一个字节设置为 ZIP_END标识 ziplist 结尾。
实现代码如下:
```c
unsigned char *ziplistNew(void) {
unsigned int bytes = ZIPLIST_HEADER_SIZE+ZIPLIST_END_SIZE;
unsigned char *zl = zmalloc(bytes);
ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);
ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);
ZIPLIST_LENGTH(zl) = 0;
zl[bytes-1] = ZIP_END;
return zl;
}
```
## 插入元素
压缩列表实现函数如下,其中:
- zl压缩列表。
- p: 元素插入位置
- s: 插入元素内容
- slen: 元素数据长度。
```c
unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen)
```
插入元素可以简要分为3个步骤① 将元素内容编码;② 重新分配空间;③ 复制数据。
### 解码长度
### 编码
编码就是计算前节点的prelen字段encoding字段和content字段的内容。计算prelen的前提条件就是明确元素的插入位置。
元素的插入位置主要包含两种场景:
- 元素插入到中间位置。
- 元素插入到末尾。
#### 场景一:元素插入到中间位置
当插入到ziplist的中间节点时,解码插入节点`p`的prevlen函数`ZIP_DECODE_PREVLEN`)。
#### 场景二:元素插入到末尾
当插入到ziplist的尾部时通过zltail计算出ziplist的最后一个节点再计算prevlen。首先我们应当获取最后一个节点。
可以通过zltail获取最后一个节点的内容。zl偏移zltail的偏移量就可以获取最后一个节点的指针。
```c
#define ZIPLIST_ENTRY_TAIL(zl) ((zl)+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)))
```
取出最后一个节点的长度,作为新插入节点`p`的`prevlen`,最后一个节点的prevlen是节点headersize和将节点内容长度之和。
```c
static inline unsigned int zipRawEntryLengthSafe(unsigned char* zl, size_t zlbytes, unsigned char *p) {
zlentry e;
assert(zipEntrySafe(zl, zlbytes, p, &e, 0));
return e.headersize + e.len;
}
```
#### 元素编码
编码时尝试将输入字符串转为整数若可以转为整数则按照压缩列表整数类型编码存储reqlen根据encoding确定保存节点值需要的字节数
若不可以转为整数则按照字节数组方式存储reqlen为字符串的长度。
reqlen字段为存储当前元素需要的空间大小所以由prevlen占用空间、当前节点的encoding和length、当前节点值占用的空间三部分之和构成。
计算公式:`reqlen = prevlenSize + encodingSize + dataSize`
```c
if (zipTryEncoding(s,slen,&value,&encoding)) {
reqlen = zipIntSize(encoding);
} else {
reqlen = slen;
}
reqlen += zipStorePrevEntryLength(NULL,prevlen);
reqlen += zipStoreEntryEncoding(NULL,encoding,slen);
```
### 重新分配空间
当向ziplist中插入数据时空间变化下面几种可能
## 删除元素
## 遍历元素