0
点赞
收藏
分享

微信扫一扫

Redis 3.0源码分析-数据结构与对象 skiplist intset ziplist


跳跃表 skiplist

跳跃表由redis.h/zskiplistNode和redis.h/zskiplist两个结构定义,其中zskiplistNode结构用于表示跳跃表节点,而zskiplist结构用于保存跳跃表节点的相关信息。

Redis 3.0源码分析-数据结构与对象 skiplist intset ziplist_跳跃表


Redis 3.0源码分析-数据结构与对象 skiplist intset ziplist_跳跃表_02

Redis 3.0源码分析-数据结构与对象 skiplist intset ziplist_Redis_03


节点的分值是一个double类型的浮点数,跳跃表中的所有节点都按分值从小到大来排序。分值相同的节点将按照成员对象在字典序中的大小来进行排序,成员对象较小的节点会排在前面(靠近表头的方向),而成员对象较大的节点则会排在后面(靠近表尾的方向)。

跨度用于记录两个节点之间的距离:两个节点之间的跨度越大,它们相距得就越远;指向NULL的所有前进指针的跨度都为0,因为它们没有连向任何节点。

Redis 3.0源码分析-数据结构与对象 skiplist intset ziplist_Redis_04


下面的函数处于t_zset.c文件中

创建一个层数为level的跳跃表节点

/*
* 创建一个层数为 level 的跳跃表节点,
* 并将节点的成员对象设置为 obj ,分值设置为 score 。
*
* 返回值为新创建的跳跃表节点
*
* T = O(1)
*/
zskiplistNode *zslCreateNode(int level, double score, robj *obj) {

// 分配空间
zskiplistNode *zn = zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));

// 设置属性
zn->score = score;
zn->obj = obj;

return zn;
}

创建并返回一个新的跳跃表

/*
* 创建并返回一个新的跳跃表
*
* T = O(1)
*/
zskiplist *zslCreate(void) {
int j;
zskiplist *zsl;

// 分配空间
zsl = zmalloc(sizeof(*zsl));

// 设置高度和起始层数
zsl->level = 1;
zsl->length = 0;

// 初始化表头节点
// T = O(1)
zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);
for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {
zsl->header->level[j].forward = NULL; //设置各层前进指针为NULL
zsl->header->level[j].span = 0; //设置跨度为0
}
zsl->header->backward = NULL; //设置该节点的后退指针为NULL

// 设置表尾
zsl->tail = NULL;

return zsl;
}

zset的实现:参见下一篇文章

Redis 3.0源码分析-数据结构与对象 skiplist intset ziplist_数组_05

整数集合

关于intset的文件intset.h和intset.c

Redis 3.0源码分析-数据结构与对象 skiplist intset ziplist_Redis_06


可以保存类型int16_t、int32_t或者int64_t的整数值

Redis 3.0源码分析-数据结构与对象 skiplist intset ziplist_数组_07


contents数组是整数集合的底层实现:整数集合的每个元素都是contents数组的一个数组项(item),各个项在数组中按值得大小从小到大有序地排列,并且数组中不包含任何重复项。虽然contents属性声明为int8_t类型的数组,但实际上contents数组并不保存任何int8_t类型的值,contents数组的真正类型取决于encoding属性的值。

升级:当我们将一个新元素添加到整数集合里面,并且新元素的类型比整数集合现有所有元素的类型都要长时,整数集合需要进行升级,然后才能将新元素添加到整数集合里面。
升级整数集合并添加新元素共分三步进行:1.根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间。2.将底层数组现有的所有元素都转换成与新元素相同的类型,并将类型转换后的元素放置到正确的位上,而且在放置元素的过程中,需要继续维持底层数组的有序性质不变。 3.将新元素添加到底层数组里面。

/* Upgrades the intset to a larger encoding and inserts the given integer. 
*
* 根据值 value 所使用的编码方式,对整数集合的编码进行升级,
* 并将值 value 添加到升级后的整数集合中。
*
* 返回值:添加新元素之后的整数集合
*
* T = O(N)
*/
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {

// 当前的编码方式
uint8_t curenc = intrev32ifbe(is->encoding);

// 新值所需的编码方式
uint8_t newenc = _intsetValueEncoding(value);

// 当前集合的元素数量
int length = intrev32ifbe(is->length);

// 根据 value 的值,决定是将它添加到底层数组的最前端还是最后端
// 注意,因为 value 的编码比集合原有的其他元素的编码都要大
// 所以 value 要么大于集合中的所有元素,要么小于集合中的所有元素
// 因此,value 只能添加到底层数组的最前端或最后端
int prepend = value < 0 ? 1 : 0;

/* First set new encoding and resize */
// 更新集合的编码方式
is->encoding = intrev32ifbe(newenc);
// 根据新编码对集合(的底层数组)进行空间调整
// T = O(N)
is = intsetResize(is,intrev32ifbe(is->length)+1);

/* Upgrade back-to-front so we don't overwrite values.
* Note that the "prepend" variable is used to make sure we have an empty
* space at either the beginning or the end of the intset. */
// 根据集合原来的编码方式,从底层数组中取出集合元素
// 然后再将元素以新编码的方式添加到集合中
// 当完成了这个步骤之后,集合中所有原有的元素就完成了从旧编码到新编码的转换
// 因为新分配的空间都放在数组的后端,所以程序先从后端向前端移动元素
// 举个例子,假设原来有 curenc 编码的三个元素,它们在数组中排列如下:
// | x | y | z |
// 当程序对数组进行重分配之后,数组就被扩容了(符号 ? 表示未使用的内存):
// | x | y | z | ? | ? | ? |
// 这时程序从数组后端开始,重新插入元素:
// | x | y | z | ? | z | ? |
// | x | y | y | z | ? |
// | x | y | z | ? |
// 最后,程序可以将新元素添加到最后 ? 号标示的位置中:
// | x | y | z | new |
// 上面演示的是新元素比原来的所有元素都大的情况,也即是 prepend == 0
// 当新元素比原来的所有元素都小时(prepend == 1),调整的过程如下:
// | x | y | z | ? | ? | ? |
// | x | y | z | ? | ? | z |
// | x | y | z | ? | y | z |
// | x | y | x | y | z |
// 当添加新值时,原本的 | x | y | 的数据将被新值代替
// | new | x | y | z |
// T = O(N)
while(length--)
_intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));

/* Set the value at the beginning or the end. */
// 设置新值,根据 prepend 的值来决定是添加到数组头还是数组尾
if (prepend)
_intsetSet(is,0,value);
else
_intsetSet(is,intrev32ifbe(is->length),value);

// 更新整数集合的元素数量
is->length = intrev32ifbe(intrev32ifbe(is->length)+1);

return is;
}

Redis 3.0源码分析-数据结构与对象 skiplist intset ziplist_数组_08

/* Create an empty intset. 
*
* 创建并返回一个新的空整数集合
*
* T = O(1)
*/
intset *intsetNew(void) {

// 为整数集合结构分配空间
intset *is = zmalloc(sizeof(intset));

// 设置初始编码
is->encoding = intrev32ifbe(INTSET_ENC_INT16);

// 初始化元素数量
is->length = 0;

return is;
}

压缩列表

压缩列表(ziplist)是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较较短的字符串,Redis会使用压缩列表来列表键的底层实现。

一个压缩列表可以包含任意多个节点(entry),每个节点保存一个字节数组或者一个整数值。

Redis 3.0源码分析-数据结构与对象 skiplist intset ziplist_编码方式_09


Redis 3.0源码分析-数据结构与对象 skiplist intset ziplist_数组_10


从代码中可以看出ZIPLIST_HEADER_SIZE宏定义为(sizeof(uint32_t)*2+sizeof(uint16_t))就是zlbytes、zltail和zllen的大小。

Redis 3.0源码分析-数据结构与对象 skiplist intset ziplist_Redis_11

Redis 3.0源码分析-数据结构与对象 skiplist intset ziplist_Redis_12

每个压缩列表节点的previous_entry_length属性以字节为单位,记录了压缩列表中前一个节点的长度。previous_entry_length属性的长度可以是1字节或者5字节。

  • 如果前一节点的长度小于254字节,那么previous_entry_length属性的长度为1字节:前一节点的长度就保存在这一个字节里面。
  • 如果前一节点的长度大于等于254字节,那么previous_entry_length属性的长度为5字节:其中属性的第一个字节会被设置为0xFE,而之后的四字节则用于保存前一节点的长度。

content可以保存一个字节数组或者一个整数值,其中数组可以是以下三种长度之一:

  • 长度小于等于63(26-1)字节的字节数组
  • 长度小于等于16383(214-1)字节的字节数组
  • 长度小于等于4294967295(232-1)字节的字节数组
  • Redis 3.0源码分析-数据结构与对象 skiplist intset ziplist_跳跃表_13

  • 而整数值则可以是以下六种长度之一:
  • 4位长,介于0至12之间的无符号整数
  • 1字节长的有符号整数
  • 3字节长的有符号整数
  • int16_t类型整数
  • int32_t类型整数
  • int64_t类型整数
  • Redis 3.0源码分析-数据结构与对象 skiplist intset ziplist_跳跃表_14

Redis 3.0源码分析-数据结构与对象 skiplist intset ziplist_跳跃表_15


提取p所指向的列表节点的信息并保存到zlentry中,并返回zlentry

Redis 3.0源码分析-数据结构与对象 skiplist intset ziplist_编码方式_16

连锁更新:当将一个新节点添加到某个节点之前的时候,如果原节点的 header 空间不足以保存新节点的长度,那么就需要对原节点的 header 空间进行扩展(从 1 字节扩展到 5 字节)。但是,当对原节点进行扩展之后,原节点的下一个节点的 prevlen 可能出现空间不足,这种情况在多个连续节点的长度都接近 ZIP_BIGLEN 时可能发生。反过来说,因为节点的长度变小而引起的连续缩小也是可能出现的,不过,为了避免扩展-缩小-扩展-缩小这样的情况反复出现(flapping,抖动)。

/* When an entry is inserted, we need to set the prevlen field of the next
* entry to equal the length of the inserted entry. It can occur that this
* length cannot be encoded in 1 byte and the next entry needs to be grow
* a bit larger to hold the 5-byte encoded prevlen. This can be done for free,
* because this only happens when an entry is already being inserted (which
* causes a realloc and memmove). However, encoding the prevlen may require
* that this entry is grown as well. This effect may cascade throughout
* the ziplist when there are consecutive entries with a size close to
* ZIP_BIGLEN, so we need to check that the prevlen can be encoded in every
* consecutive entry.
*
* 当将一个新节点添加到某个节点之前的时候,
* 如果原节点的 header 空间不足以保存新节点的长度,
* 那么就需要对原节点的 header 空间进行扩展(从 1 字节扩展到 5 字节)。
*
* 但是,当对原节点进行扩展之后,原节点的下一个节点的 prevlen 可能出现空间不足,
* 这种情况在多个连续节点的长度都接近 ZIP_BIGLEN 时可能发生。
*
* 这个函数就用于检查并修复后续节点的空间问题。
*
* Note that this effect can also happen in reverse, where the bytes required
* to encode the prevlen field can shrink. This effect is deliberately ignored,
* because it can cause a "flapping" effect where a chain prevlen fields is
* first grown and then shrunk again after consecutive inserts. Rather, the
* field is allowed to stay larger than necessary, because a large prevlen
* field implies the ziplist is holding large entries anyway.
*
* 反过来说,
* 因为节点的长度变小而引起的连续缩小也是可能出现的,
* 不过,为了避免扩展-缩小-扩展-缩小这样的情况反复出现(flapping,抖动),
* 我们不处理这种情况,而是任由 prevlen 比所需的长度更长。

* The pointer "p" points to the first entry that does NOT need to be
* updated, i.e. consecutive fields MAY need an update.
*
* 注意,程序的检查是针对 p 的后续节点,而不是 p 所指向的节点。
* 因为节点 p 在传入之前已经完成了所需的空间扩展工作。
*
* T = O(N^2)
*/
static unsigned char *__ziplistCascadeUpdate(unsigned char *zl, unsigned char *p) {
size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), rawlen, rawlensize;
size_t offset, noffset, extra;
unsigned char *np;
zlentry cur, next;

// T = O(N^2)
while (p[0] != ZIP_END) {

// 将 p 所指向的节点的信息保存到 cur 结构中
cur = zipEntry(p);
// 当前节点的长度
rawlen = cur.headersize + cur.len;
// 计算编码当前节点的长度所需的字节数
// T = O(1)
rawlensize = zipPrevEncodeLength(NULL,rawlen);

/* Abort if there is no next entry. */
// 如果已经没有后续空间需要更新了,跳出
if (p[rawlen] == ZIP_END) break;

// 取出后续节点的信息,保存到 next 结构中
// T = O(1)
next = zipEntry(p+rawlen);

/* Abort when "prevlen" has not changed. */
// 后续节点编码当前节点的空间已经足够,无须再进行任何处理,跳出
// 可以证明,只要遇到一个空间足够的节点,
// 那么这个节点之后的所有节点的空间都是足够的
if (next.prevrawlen == rawlen) break;

if (next.prevrawlensize < rawlensize) {

/* The "prevlen" field of "next" needs more bytes to hold
* the raw length of "cur". */
// 执行到这里,表示 next 空间的大小不足以编码 cur 的长度
// 所以程序需要对 next 节点的(header 部分)空间进行扩展

// 记录 p 的偏移量
offset = p-zl;
// 计算需要增加的节点数量
extra = rawlensize-next.prevrawlensize;
// 扩展 zl 的大小
// T = O(N)
zl = ziplistResize(zl,curlen+extra);
// 还原指针 p
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. */
// 当 next 节点不是表尾节点时,更新列表到表尾节点的偏移量
//
// 不用更新的情况(next 为表尾节点):
//
// | | next | ==> | | new next |
// ^ ^
// | |
// tail tail
//
// 需要更新的情况(next 不是表尾节点):
//
// | next | | ==> | new next | |
// ^ ^
// | |
// old tail old tail
//
// 更新之后:
//
// | new next | |
// ^
// |
// new tail
// T = O(1)
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. */
// 向后移动 cur 节点之后的数据,为 cur 的新 header 腾出空间
//
// 示例:
//
// | header | value | ==> | header | | value | ==> | header | value |
// |<-->|
// 为新 header 腾出的空间
// T = O(N)
memmove(np+rawlensize,
np+next.prevrawlensize,
curlen-noffset-next.prevrawlensize-1);
// 将新的前一节点长度值编码进新的 next 节点的 header
// T = O(1)
zipPrevEncodeLength(np,rawlen);

/* Advance the cursor */
// 移动指针,继续处理下个节点
p += rawlen;
curlen += extra;
} else {
if (next.prevrawlensize > rawlensize) {
/* This would result in shrinking, which we want to avoid.
* So, set "rawlen" in the available bytes. */
// 执行到这里,说明 next 节点编码前置节点的 header 空间有 5 字节
// 而编码 rawlen 只需要 1 字节
// 但是程序不会对 next 进行缩小,
// 所以这里只将 rawlen 写入 5 字节的 header 中就算了。
// T = O(1)
zipPrevEncodeLengthForceLarge(p+rawlen,rawlen);
} else {
// 运行到这里,
// 说明 cur 节点的长度正好可以编码到 next 节点的 header 中
// T = O(1)
zipPrevEncodeLength(p+rawlen,rawlen);
}

/* Stop here, as the raw length of "next" has not changed. */
break;
}
}

return zl;
}

函数的声明与实现存储在ziplist.c和ziplist.h中

Redis 3.0源码分析-数据结构与对象 skiplist intset ziplist_Redis_17


举报

相关推荐

0 条评论