redis对外开放的api中包含了很多数据结构。比如list,set,hash,zset,string…然而实际上这只是封装后对外的结果。即使同样是list,在数据量不同的情况下,redis内部会使用不同的数据结构来实现。
这次讲的是压缩列表。属于redis内部的一种数据结构。设计它的初衷是什么?
redis的定位是缓存,这决定了它会有极大的QPS,如何提高它的性能呢,数据终归会落于磁盘上,数据量越大,检索数据的耗时就会越长。可以考虑将热点数据保留在缓存中,这是很容易想到的。然而内存是有限的,如何确保在内存中只存储有效的数据呢?
第一点涉及到它的内存淘汰策略。基于lru,lfu算法将非热点数据从内存中移除,提高热点数据在内存中的占比。
第二点通过压缩算法在有限的内存中存储尽量多的数据。lucene也是同样的思路,并且lucene的索引也做到尽可能小,以便直接加载到内存中,提升查询效率。
ziplist就是基于第二点设计的。
ziplist的数据结构是这样
…
第一个32位对应zlbytes存储的是ziplist的总大小
第二个32位对应zltail存储的是tailentry的偏移量 (zlend前的最后一个entry被称为tailentry)
第三个16位对应zllen存储的是entry数量
zlend表示一个结束标识,值为255
entry的数据结构是这样
prevlen记录的是上一个entry的长度。当上一个entry的长度小于254时才用一个字节来记录
当上一个entry的长度超过254时使用5个字节来记录(第一个字节是255表示prevlen需要5个字节来表示,之后的4个字节记录的是真正的长度)
encoding记录的是data的编码方式。当data本身是int类型判断int需要多少位来表示,encoding会被映射成对印的值。当data是string类型时,根据data的长度需要多少位来表示,转换成字节后作为encoding。
data记录的就是实际的数据。只针对int类型进行压缩.比如此时int值为-128~127,实际上只需要一个字节就能表示,就能节省3字节。
实际上ziplist无法对字符串进行压缩,只适合存储一些int类型的数据。当k/v占用的基础内存开销比较大的时候不适合使用这种结构。redis内部会自动选择合适的数据结构实现list
下面贴一下核心方法的注释
一些宏
/* Return total bytes a ziplist is composed of. */
#define ZIPLIST_BYTES(zl) (*((uint32_t*)(zl)))
/* Return the offset of the last item inside the ziplist. */
#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))
/* Return the length of a ziplist, or UINT16_MAX if the length cannot be
* determined without scanning the whole ziplist. */
#define ZIPLIST_LENGTH(zl) (*((uint16_t*)((zl)+sizeof(uint32_t)*2)))
/* The size of a ziplist header: two 32 bit integers for the total
* bytes count and last item offset. One 16 bit integer for the number
* of items field. */
#define ZIPLIST_HEADER_SIZE (sizeof(uint32_t)*2+sizeof(uint16_t))
/* Size of the "end of ziplist" entry. Just one byte. */
#define ZIPLIST_END_SIZE (sizeof(uint8_t))
/* Return the pointer to the first entry of a ziplist. */
#define ZIPLIST_ENTRY_HEAD(zl) ((zl)+ZIPLIST_HEADER_SIZE)
/* Return the pointer to the last entry of a ziplist, using the
* last entry offset inside the ziplist header. */
#define ZIPLIST_ENTRY_TAIL(zl) ((zl)+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)))
/* Return the pointer to the last byte of a ziplist, which is, the
* end of ziplist FF entry. */
#define ZIPLIST_ENTRY_END(zl) ((zl)+intrev32ifbe(ZIPLIST_BYTES(zl))-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((prevlen)) == 4); \
memcpy(&(prevlen), ((char*)(ptr)) + 1, 4); \
memrev32ifbe(&prevlen); \
} \
} while(0);
复制代码
ziplistNew 创建一个ziplist
unsigned char *ziplistNew(void) {
// 一开始分配的数组大小就是head+end
// head大小为 2个int32 + 1个int16
// end大小为 一个int8
unsigned int bytes = ZIPLIST_HEADER_SIZE+ZIPLIST_END_SIZE;
unsigned char *zl = zmalloc(bytes);
// head中第一个int32 记录的就是此时ziplist的总大小
ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);
// head中第二个int32 记录的是tail块的起始位置 因为此时内部还没有任何数据 所以header的末尾就是tail块的起始位置
ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);
// head中第一个int16记录此时ziplist中有效数据的总长度 也就是不包含head, end
ZIPLIST_LENGTH(zl) = 0;
// ziplist最后一个元素对应int8 存储的是一个固定值255
zl[bytes-1] = ZIP_END;
return zl;
}
复制代码
ziplistPush 将某个字符串插入到ziplist的头部或者尾部
unsigned char *ziplistPush(unsigned char *zl, unsigned char *s, unsigned int slen, int where) {
unsigned char *p;
// p 对应应当插入的位置 因为头部是固定不变的所以 需要定位到数组[0]+headsize的位置
// ZIPLIST_ENTRY_END 就是总长度-1 也就是对应到end的位置
p = (where == ZIPLIST_HEAD) ? ZIPLIST_ENTRY_HEAD(zl) : ZIPLIST_ENTRY_END(zl);
// 将s字符串插入到zl中
return __ziplistInsert(zl,p,s,slen);
}
复制代码
__ziplistInsert 插入实现
unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {
// 第一个元素是总大小 也就是包含head+end
size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)),
// 代表需要额外占用多少字节
reqlen;
unsigned int prevlensize, prevlen = 0;
size_t offset;
int nextdiff = 0;
unsigned char encoding = 0;
long long value = 123456789; /* initialized to avoid warning. Using a value
that is easy to see if for some reason
we use it uninitialized. */
zlentry tail;
/* Find out prevlen for the entry that is inserted. */
// 代表本次希望插入到头部 (且此时ziplist已经存在数据) 在下面会解析第一个entry的prevlen 不过第一个entry没有前prev prevlen长度应该是0
if (p[0] != ZIP_END) {
ZIP_DECODE_PREVLEN(p, prevlensize, prevlen);
} else {
// 代表本次是希望插入到末尾 或者想要插入头部但此前没有数据 这样逻辑是一样的 都是找到tail的位置
unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl);
// 代表此前有数据 如果没有数据head的尾部就是end同时也是tail的起始位置 3者重叠
if (ptail[0] != ZIP_END) {
// 这里是在计算tail的长度
prevlen = zipRawEntryLength(ptail);
}
}
/* See if the entry can be encoded
* 对本次即将插入的数据进行编码
* */
if (zipTryEncoding(s,slen,&value,&encoding)) {
/* 'encoding' is set to the appropriate integer encoding */
reqlen = zipIntSize(encoding);
} else {
/* 'encoding' is untouched, however zipStoreEntryEncoding will use the
* string length to figure out how to encode it.
* 本次长度超过了int 无法压缩 占用的字节数等于字符串的字节数
* */
reqlen = slen;
}
/* We need space for both the length of the previous entry and
* the length of the payload.
* 除了表示数据本身需要的长度外 还需要空间存储前一个entry的长度以及encoding
* 如果本次是第一次插入数据 prevlen为0这时reqlen会返回1
* */
reqlen += zipStorePrevEntryLength(NULL,prevlen);
reqlen += zipStoreEntryEncoding(NULL,encoding,slen);
/* When the insert position is not equal to the tail, we need to
* make sure that the next entry can hold this entry's length in
* its prevlen field. */
int forcelarge = 0;
// p[0] != ZIP_END 代表本次期望插入头部 这里是确保当前p用于存储上一个entry长度信息的prevlen有足够的空间表示本次新entry的长度信息
// 简化情况第一次p[0]就是end 此时diff是0
nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0;
// 目前头部entry前缀长度必然是0 也就是 zipPrevLenByteDiff(p,reqlen) 不会是负数 应该不会出现下面的情况
if (nextdiff == -4 && reqlen < 4) {
nextdiff = 0;
forcelarge = 1;
}
/* Store offset because a realloc may change the address of zl.
* 这里得到的是从zl起始位置到tailentry起始位置的偏移量(如果ziplist为空tailentry起始位置就是end)
* */
offset = p-zl;
// 首先要尝试对ziplist进行扩容 确保有足够的空间
zl = ziplistResize(zl,curlen+reqlen+nextdiff);
p = zl+offset;
/* Apply memory move when necessary and update tail offset.
* 本次插入头部且原本有数据
* */
if (p[0] != ZIP_END) {
/* Subtract one because of the ZIP_END bytes
* param1 dest param2 src param3 len
* 先简化情况 认为nextdiff为0 这里是把从firstentry开始的数据移动到了本次新entry插入后的位置
*
* 如果本次 nextdiff为4 (没有其他情况了)
* 以下操作等同于 memmove(p+reqlen+nextdiff,p,curlen-offset-1);
* */
memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff);
/* Encode this entry's raw length in the next entry. 如何出现??? */
if (forcelarge)
zipStorePrevEntryLengthLarge(p+reqlen,reqlen);
else
// 将新entry的长度信息填充到原first的首位
zipStorePrevEntryLength(p+reqlen,reqlen);
/* Update offset for tail
* tailoffset在原基础上增加reqlen(实际上可能还需要再移动nextdiff)
* */
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+reqlen);
/* When the tail contains more than one entry, we need to take
* "nextdiff" in account as well. Otherwise, a change in the
* size of prevlen doesn't have an effect on the *tail* offset.
* 读取原first的数据
* */
zipEntry(p+reqlen, &tail);
// 如果此时原first已经是最后一个entry了 tailoffset只需要增加reqlen 否则还要加上本次first为存储prevlen额外申请的nextdiff
if (p[reqlen+tail.headersize+tail.len] != ZIP_END) {
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff);
}
// 先简化情况 认为ziplist中没有entry 此时进入的是这个分支
} else {
/* This element will be the new tail. */
// 此时将tail_offset的位置标记为end 那么之后就应该会在end后插入新的entry作为tailentry
ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(p-zl);
}
/* When nextdiff != 0, the raw length of the next entry has changed, so
* we need to cascade the update throughout the ziplist
* 如果当前entry为存储prevlen申请了额外内存
* */
if (nextdiff != 0) {
offset = p-zl;
zl = __ziplistCascadeUpdate(zl,p+reqlen);
p = zl+offset;
}
// 从以下代码可以反推entry的结构 首先第一部分记录的是prevlen 第二部分对应encoding标记(针对数据是否可用int表示以及使用多少位表示 会设置不同的标记)
// 第三部分存储被压缩的数据 注意str是不支持压缩的
/* Write the entry 此时将数据填充到entry中 */
p += zipStorePrevEntryLength(p,prevlen);
// 将压缩标记写入到p(entry)中
p += zipStoreEntryEncoding(p,encoding,slen);
// 发现采用的压缩方式是str 也就是无法压缩 直接拷贝数据
if (ZIP_IS_STR(encoding)) {
memcpy(p,s,slen);
} else {
// 针对int类型通过压缩使得消耗的bit尽可能小
zipSaveInteger(p,value,encoding);
}
// 因为插入了一个新的entry 所以len+1 ziplist_len 代表当前压缩列表中总计有多少entry
ZIPLIST_INCR_LENGTH(zl,1);
return zl;
}
复制代码
zipRawEntryLength 返回当前entry的总长度
unsigned int zipRawEntryLength(unsigned char *p) {
unsigned int prevlensize, encoding, lensize, len;
// prevlen 可能占用1字节或者5字节 当prevlen超过254时 就会占用5字节
ZIP_DECODE_PREVLENSIZE(p, prevlensize);
// 这里是解析entry长度
ZIP_DECODE_LENGTH(p + prevlensize, encoding, lensize, len);
return prevlensize + lensize + len;
}
复制代码
zipTryEncoding 计算encoding的值
long long value;
if (entrylen >= 32 || entrylen == 0) return 0;
if (string2ll((char*)entry,entrylen,&value)) {
/* Great, the string can be encoded. Check what's the smallest
* of our encoding types that can hold this value.
* 如果值在一个特殊的范围内 使用ZIP_INT_IMM_MIN+value来表示 相当于在标记位内部直接存储了value
* 读取其他大小的数据时 必须先读取encoding之后再读取位数
* */
if (value >= 0 && value <= 12) {
*encoding = ZIP_INT_IMM_MIN+value;
// 代表能够用8bit表示
} else if (value >= INT8_MIN && value <= INT8_MAX) {
*encoding = ZIP_INT_8B;
} else if (value >= INT16_MIN && value <= INT16_MAX) {
*encoding = ZIP_INT_16B;
} else if (value >= INT24_MIN && value <= INT24_MAX) {
*encoding = ZIP_INT_24B;
} else if (value >= INT32_MIN && value <= INT32_MAX) {
*encoding = ZIP_INT_32B;
} else {
*encoding = ZIP_INT_64B;
}
*v = value;
return 1;
}
return 0;
}
复制代码
zipIntSize 根据encoding的值判断本次int需要多少字节存储
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;
}
// 代表value的范围在 0~12之间 所以value的值被直接存储在了encoding中
if (encoding >= ZIP_INT_IMM_MIN && encoding <= ZIP_INT_IMM_MAX)
return 0; /* 4 bit immediate */
panic("Invalid integer encoding 0x%02X", encoding);
return 0;
}
复制代码
zipStorePrevEntryLength 用prevlen表示传入的长度需要多少字节
unsigned int zipStorePrevEntryLength(unsigned char *p, unsigned int len) {
if (p == NULL) {
// 在前一个entry长度小于big长度(254)时 使用1来表示 否则为1+int的长度(4)
return (len < ZIP_BIG_PREVLEN) ? 1 : sizeof(len)+1;
} else {
// p不为空 代表将数据填充到p的后面 反推p(entry)的结构 第一部分是prevlen 也就是上一个entry的长度
if (len < ZIP_BIG_PREVLEN) {
p[0] = len;
return 1;
} else {
return zipStorePrevEntryLengthLarge(p,len);
}
}
}
复制代码
zipStoreEntryEncoding encoding需要多少字节来表示
unsigned int zipStoreEntryEncoding(unsigned char *p, unsigned char encoding, unsigned int rawlen) {
// len代表buf中占用了多少个槽
unsigned char len = 1, buf[5];
// 当encoding为0 也是进入这个分支
if (ZIP_IS_STR(encoding)) {
/* Although encoding is given it may not be set for strings,
* so we determine it here using the raw length.
* 4*16-1 = 63 可以用(2的6次-1)表示
* */
if (rawlen <= 0x3f) {
if (!p) return len;
// 这里2^6|rawlen 刚好可以用6位表示
buf[0] = ZIP_STR_06B | rawlen;
// 一个char最多表示 0xff 这里就代表需要占用buf的2个槽
} else if (rawlen <= 0x3fff) {
len += 1;
if (!p) return len;
buf[0] = ZIP_STR_14B | ((rawlen >> 8) & 0x3f);
buf[1] = rawlen & 0xff;
} else {
// 剩余的数据统一占用5个槽
len += 4;
if (!p) return len;
buf[0] = ZIP_STR_32B;
buf[1] = (rawlen >> 24) & 0xff;
buf[2] = (rawlen >> 16) & 0xff;
buf[3] = (rawlen >> 8) & 0xff;
buf[4] = rawlen & 0xff;
}
} else {
/* Implies integer encoding, so length is always 1.
* 代表本次数据可以用int来表示 encoding统一占用1字节
* */
if (!p) return len;
buf[0] = encoding;
}
/* Store this length at p. */
memcpy(p,buf,len);
return len;
}
复制代码
zipSaveInteger 采用压缩方式存储int
void zipSaveInteger(unsigned char *p, int64_t value, unsigned char encoding) {
int16_t i16;
int32_t i32;
int64_t i64;
// 使用一个char来表示 一个char能够表示8位 此时int的值不超过8位
if (encoding == ZIP_INT_8B) {
((int8_t*)p)[0] = (int8_t)value;
} else if (encoding == ZIP_INT_16B) {
i16 = value;
memcpy(p,&i16,sizeof(i16));
memrev16ifbe(p);
} else if (encoding == ZIP_INT_24B) {
i32 = value<<8;
memrev32ifbe(&i32);
// 这里要去掉一开始 <<8 的操作
memcpy(p,((uint8_t*)&i32)+1,sizeof(i32)-sizeof(uint8_t));
} else if (encoding == ZIP_INT_32B) {
i32 = value;
memcpy(p,&i32,sizeof(i32));
memrev32ifbe(p);
} else if (encoding == ZIP_INT_64B) {
i64 = value;
memcpy(p,&i64,sizeof(i64));
memrev64ifbe(p);
// 这种情况数据直接存储在encoding里了 所以不需要处理
} else if (encoding >= ZIP_INT_IMM_MIN && encoding <= ZIP_INT_IMM_MAX) {
/* Nothing to do, the value is stored in the encoding itself. */
} else {
assert(NULL);
}
}
复制代码