Redis-存储底层数据结构-ziplist
ziplist.c的开头注释
这一段注释大概解析了一下ziplist基本构成。
1 |
|
以上我们大致能够了解到ziplist的存储结构。
看看code
创建一个ziplist
1 |
|
zlbytes
赋值为(sizeof(uint32_t)*2+sizeof(uint16_t))
+(sizeof(uint8_t))
, intrev32ifbe是用来保持小端序的。zltail
赋值为ZIPLIST_HEADER_SIZE
。zllen
赋值为0zlend
赋值为ZIP_END
, 也就是255。
也就是给ziplist赋了的个初值。
insert entry
这个insert的逻辑偏复杂,我们一段段代码的来分析。首先插入方法ziplistInsert
最终调用的是__ziplistInsert
。所以我们主要来看看__ziplistInsert
。
1 |
|
我们分析一下最开始的逻辑。
- 如果
p[0]
不是尾节点,则需要赋值将p[0]
的prevlen
和prevlensize
赋值给__ziplistInsert中的prevlensize, prevlen
,具体逻辑。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// 这里我们可以看到如开头的注释所说,prevlensize是一个字节的话,prevlen直接读`(ptr)[0]`,否则读`ptr`的1-5个字节,同时保持大端序。
#define ZIP_DECODE_PREVLENSIZE(ptr, prevlensize) do { \
(prevlensize) = 1; \
if ((ptr)[0] < ZIP_BIG_PREVLEN) { \
} else { \
(prevlensize) = 5; \
} \
} while(0);
#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((prevlen)) == 4); \
memcpy(&(prevlen), ((char*)(ptr)) + 1, 4); \
memrev32ifbe(&prevlen); \
} \
} while(0); - 如果
p[0]
就是结尾的话,else看着像进行了一些特殊处理。如果ziplist为空的话,ptail
就是尾节点, 如果不为空的话,则进入if分支,此时p就成为了s的前置节点。也就是需要求p的<prevlen> <encoding> <entry-data>
的字节长度。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46#define ZIPLIST_ENTRY_TAIL(zl) ((zl)+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)))
/* Return the total number of bytes used by the entry pointed to by 'p'. */
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;
}
#define ZIP_DECODE_PREVLENSIZE(ptr, prevlensize) do { \
if ((ptr)[0] < ZIP_BIG_PREVLEN) { \
(prevlensize) = 1; \
} else { \
(prevlensize) = 5; \
} \
} while(0);
#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; \
} else if ((encoding) == ZIP_STR_14B) { \
(lensize) = 2; \
(len) = (((ptr)[0] & 0x3f) << 8) | (ptr)[1]; \
} else if ((encoding) == ZIP_STR_32B) { \
(lensize) = 5; \
(len) = ((ptr)[1] << 24) | \
((ptr)[2] << 16) | \
((ptr)[3] << 8) | \
((ptr)[4]); \
} else { \
panic("Invalid string encoding 0x%02X", (encoding)); \
} \
} else { \
(lensize) = 1; \
(len) = zipIntSize(encoding); \
} \
} while(0);
#define ZIP_ENTRY_ENCODING(ptr, encoding) do { \
(encoding) = (ptr[0]); \
if ((encoding) < ZIP_STR_MASK) (encoding) &= ZIP_STR_MASK; \
} while(0) prevlensize
用来保存p的prevlen的长度,这个逻辑主要在ZIP_DECODE_PREVLENSIZE
中,其实就是p的第一个字节是小于254,则是一个字节存储,否则五个字节存储。- 接着就是
ZIP_DECODE_LENGTH
其首先通过ZIP_ENTRY_ENCODING
来确定encoding是否小于ZIP_STR_MASK(11000000)
, 如果是则encoding就用来保存ptr的编码类型。 - 回到
ZIP_DECODE_LENGTH
,如果encoding < ZIP_STR_MASK代表是字符编码类型,则可以看是属于三种字符编码中的哪一种类型了。lensize保存<encoding>
长度, len保存<entry-data>
长度。- 如果是
ZIP_STR_06B
,则lensize = 1, len=prt[0] & 00111111,也就是第一个字节的后6为表示字符长度。 - 如果是
ZIP_STR_14B
,则lensize = 2, len=(prt[0] & 00111111)<<8 | prt[1], 也就是前两个字节的后14位表示长度, - 如果是
ZIP_STR_32B
,则lensize = 5,len则是后四个字节,按照大端存储读取即可。 - 其他就是int类型了,lensize=1,具体保存信息的长度则通过zipIntSize获得。这个看最开始的注解就懂了,记得别忘了
立即 4 位整数
。
- 如果是
- 最后就返回
prevlensize
+lensize
+len
则是 insert节点s
的prevlen。
接下来进入第二段逻辑。
1 |
|
- 这里先尝试了对s进行了转整型的编码,首先是如果其长度
(entrylen >= 32 || entrylen == 0)
,就无法进行整型编码,不应该大于20就不行了码?- 调用string2ll, 将string转为int,值保存value内。接着就是将encoding赋值为各种编码类型。v=value,同时返回1。
- 返回主逻辑的if分支后,将
replen
赋值为保存实际数据的长度,zipIntSize
在前面已经解释过。 - 否则就是一个字符串,
reqlen=slen
即可。到这里,reqlen等于的是保存实际数据所需要的长度,即<entry-data>
所需要的字节数。
- 接着是求
prevlen
所需的字节数,这个逻辑较为简单,不做过多解释。 - 随后是求保存
encoding
所需字节数,len初值是赋为1,int类型的encoding都是1,接着就是看他是属于那种类型的string,并将实际encoding保存在buf中,因为在p为null的时候,buf是没用用的,所以我注释掉了这一部分。 - 到这里,s需要的字节数已经求出来了。但是还是需要判断一下,如果p不是尾节点的话,s就插到p前面了,有可能是p的prevlensize是要扩大的。具体逻辑在
zipPrevLenByteDiff
, 看p存储len作为prevlen字节数是否够。所以nextdiff也是需要的字节。
其后开始进入拓容逻辑。
1 |
|
- 首先是获取p相对zl的偏移量。
- 接着重新对zl分配内存,curlen是原来的长度,reqlen是s需要的长度,diff是p需要扩大的长度。
- 接着重新获取p的位置。
1 |
|
如果是
P[0] != ZIP_END
,就要将p-zltail
这一段移动到新内存。- 这个forcelarge看着像是修复了一个bug,不过目前没有找到相关issue。所以这段逻辑我们只能先跳过。
- 接着就是对
zltail
的赋值,需要加上一个reqlen
。 - 接下来的这段了逻辑比较复杂,首先是将p的一些信息存到
tail
中,接着是看p是不是最后一个entry,如果不是的,ZIPLIST_TAIL_OFFSET
需要加上diff。1
2
3
4
5
6
7
8/* Return a struct with all information about an entry. */
void zipEntry(unsigned char *p, zlentry *e) {
ZIP_DECODE_PREVLEN(p, e->prevrawlensize, e->prevrawlen);
ZIP_DECODE_LENGTH(p + e->prevrawlensize, e->encoding, e->lensize, e->len);
e->headersize = e->prevrawlensize + e->lensize;
e->p = p;
}
如果
p[0] = ZIP_END
,则仅需要ZIPLIST_TAIL_OFFSET=p-zl,此时s就是最后一个entry
。
接下来的这段逻辑比较重要,因为ziplist受限于连锁更新,所带来的稳定性较差,所以后面页产生了一些替代品。
1 |
|
稍后来看看这个__ziplistCascadeUpdate
方法。先看看最后的逻辑。
- 最后就是写入这个entry, p此时仍然指向原位置。
- 所以先写入prevlen。
- 再写入encoding。
- 最后在把s的
entry data
copy进来。 - 最长长度+1即可。
1
2
3
4
5
6
7
8
9p += zipStorePrevEntryLength(p,prevlen);
p += zipStoreEntryEncoding(p,encoding,slen);
if (ZIP_IS_STR(encoding)) {
memcpy(p,s,slen);
} else {
zipSaveInteger(p,value,encoding);
}
ZIPLIST_INCR_LENGTH(zl,1);
return zl;
连锁更新的问题
其主要逻辑集中在__ziplistCascadeUpdate
。
1 |
|
首先我们来看看方法的注解。
插入entry
时,我们需要将下一entry
的 prevlen 字段设置为等于插入entry
的长度。可能会出现此长度无法用 1 个字节编码的情况,因此下一个entry
需要增大prevlen
以容纳 5 个字节编码的 。这可以免费完成,因为这仅在已插入entry
时发生(这会导致 realloc 和 memmove)。但是,编码 prevlen 可能还需要此条目也增大。当存在大小接近ZIP_BIG_PREVLEN 的连续条目时,此效果可能会在整个 ziplist 中级联,因此我们需要检查是否可以在每个连续条目中编码 prevlen。
请注意,此效果也可能反向发生,其中编码 prevlen 字段所需的字节数可能会减少。故意忽略此影响,因为它可能导致“抖动”效应,即链式 prevlen 字段首先增大,然后在连续插入后再次缩小。相反,允许字段保持大于必要的大小,因为较大的 prevlen字段意味着 ziplist 无论如何都会保存较大的条目。
指针“p”指向第一个不需要更新的条目,即连续字段可能需要更新。
接下来我们来看看函数。主要就是跑一个循环,我们直接进入循环开始看。
首先是获取存储p需要的字节长度
rawlen
和保存rawlen
所需要的字节数。1
2
3
4
5
6
7
8
9
10
11zipEntry(p, &cur);
rawlen = cur.headersize + cur.len;
rawlensize = zipStorePrevEntryLength(NULL,rawlen);
void zipEntry(unsigned char *p, zlentry *e) {
ZIP_DECODE_PREVLEN(p, e->prevrawlensize, e->prevrawlen);
ZIP_DECODE_LENGTH(p + e->prevrawlensize, e->encoding, e->lensize, e->len);
e->headersize = e->prevrawlensize + e->lensize;
e->p = p;
}如果没有一一个节点,直接return。然后就可以通过zipentry函数解析下一个entry。还需要特判的是如果
next.prevrawlen == rawlen
则代表prevlen没有发生变化,无序处理。1
2
3
4
5
6/* Abort if there is no next entry. */
if (p[rawlen] == ZIP_END) break;
zipEntry(p+rawlen, &next);
/* Abort when "prevlen" has not changed. */
if (next.prevrawlen == rawlen) break;接着就是进行连锁更新的处理,首先是prevlen变大了。
- 首先是先计算了一些有用的字段。
np
和noffset
分别代表当前节点的位置和当前节点的偏移量。 - 接着看如果当前节点不是最后一个节点,
zltail
需要+extra
,即np额外需要的字节数。 - 接着就是相当于将np向后移动extra个字节,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26/* The "prevlen" field of "next" needs more bytes to hold
* the raw length of "cur". */
offset = p-zl;
extra = rawlensize-next.prevrawlensize;
zl = ziplistResize(zl,curlen+extra);
p = zl+offset;
/* Current pointer and offset for next element. */
np = p+rawlen;
noffset = np-zl;
/* Update tail offset when next element is not the tail element. */
if ((zl+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))) != np) {
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+extra);
}
/* Move the tail to the back. */
memmove(np+rawlensize,
np+next.prevrawlensize,
curlen-noffset-next.prevrawlensize-1);
zipStorePrevEntryLength(np,rawlen);
/* Advance the cursor */
p += rawlen;
curlen += extra;
- 首先是先计算了一些有用的字段。
另外一种情况就是prevlen变小了。这种情况空间不发生变化,所以仅存储一下即可。需要注意的长度本来为5就不收缩了。也就是第一个if的特判。
以上就大概是ziplist的解析。后续看看redis针对ziplist的优化。