4.哈希表
哈希表是一种保存键值对(key-value)的数据结构。
在Redis中,Hash 对象的实现方式是:listpack(原为ziplist)、和哈希表。哈希表优点在于,它能以 O(1) 的复杂度快速查询数据,并且在Redis中,哈希表是通过链式哈希来解决哈希冲突的,又由于链式哈希的特性,所以哈希表还需要升级操作。
4.1 结构
值得注意的是,在这一版本的Redis中,已经弃用dictht这一结构体,而原本的used被加入在了dict结构体中,原本的size和sizemask直接改为了宏定义,可以通过宏定义直接计算得到,而原本dict中的ht指针直接改为了dictEntry** ,直接指向了哈希节点!
我们依然可以通过旧版本的结构图来更好的理解这个结构的逻辑构成,具体的修改细节我会体现在代码解析中。
(图片出处:CSDN @留兰香)
dict以及(逻辑上的)dictht:
typedef struct dictType {
uint64_t (*hashFunction)(const void *key);
void *(*keyDup)(dict *d, const void *key);
void *(*valDup)(dict *d, const void *obj);
int (*keyCompare)(dict *d, const void *key1, const void *key2);
void (*keyDestructor)(dict *d, void *key);
void (*valDestructor)(dict *d, void *obj);
int (*expandAllowed)(size_t moreMem, double usedRatio);
/* 允许dictEntry携带额外的caller-defined元数据。默认初始化dictEntry时,额外的内存是0。
这应该是最新版Redis加入的一条允许每个Entry自带一个meta-data的配置 */
size_t (*dictEntryMetadataBytes)(dict *d);
} dictType;
//注意,这里是通过2个宏定义代替原来dictht结构体中的size和sizemask
//并且,sizemask被固定为size-1了,因为好像确实没有场景需要自定义哈希因子?
#define DICTHT_SIZE(exp) ((exp) == -1 ? 0 : (unsigned long)1<<(exp))
#define DICTHT_SIZE_MASK(exp) ((exp) == -1 ? 0 : (DICTHT_SIZE(exp))-1)
struct dict {
dictType *type;
//至于下面 table和used为何是[2],是因为rehash,下一节讲
//注意,这里把原来指向dictht的指针,直接变为了指向ht_table的指针
dictEntry **ht_table[2];
//注意,这里是把原来定义在dictht中的used变量,通过数组的形式定义在了dict中
unsigned long ht_used[2];
long rehashidx; /* 当rehashidx == -1 时,说明rehash没有在进行中 */
/* 尽量把小的变量放在结构体的尾部,这样可以通过结构体填充来节约内存!
这里想不明白的话可以再看一下 1.SDS 中<内存对齐>部分 */
int16_t pauserehash; /* 如果pauserehash > 0 说明 rehash是停止的
如果pauserehash < 0 说明此时编码错误了 */
signed char ht_size_exp[2]; /* 用来表示size的幂数,也就是说size一定是2的n次方
且 size = 1<<exp */
};
dictEnrty:
typedef struct dictEntry {
void *key;
// 这里value值采用了64字节的共用体
union {
void *val; //如果是其他类型,那么就用4个字节保存一个指针指向该类型数据
uint64_t u64;//如果是64位无符号数,直接保存在共用体中
int64_t s64;//如果是64位有符号数,直接保存在共用体中
double d; //如果是(64位)双精度浮点数,直接保存在共用体中
} v;
struct dictEntry *next; /* 同一个哈希桶中的下一个entry */
void *metadata[]; /* dictType中提到的dictEntryMetadataBytes()返回的字节数的大小。
并且,这个指针最开始指向指针对齐的地址 */
} dictEntry;
创建dict
创建字典主要的操作是, 分配字典内存, 并且对字典类型和私有数据进行赋值, 注意, 此时字典的hash表还没初始化的, 其他统计数据都是0
/* Create a new hash table */
dict *dictCreate(dictType *type)
{
dict *d = zmalloc(sizeof(*d));
_dictInit(d,type);//见下方
return d;
}
/* Initialize the hash table */
int _dictInit(dict *d, dictType *type)
{
_dictReset(d, 0);
_dictReset(d, 1);
d->type = type;
d->rehashidx = -1;
d->pauserehash = 0;
return DICT_OK;
}
4.2 rehash
在看结构的源码时,细心的人肯定注意到了为啥我们在dict头部中要保存2个哈希表?这是因为进行 rehash 的时候,需要把旧表拷贝到更大的新表中来。
随着数据逐步增多,触发了 rehash 操作,这个过程分为三步:
-
给「哈希表 2」 分配空间,一般会比「哈希表 1」 大 2 倍;
-
将「哈希表 1 」的数据迁移到「哈希表 2」 中;
-
迁移完成后,「哈希表 1 」的空间会被释放,并把「哈希表 2」 设置为「哈希表 1」,然后在「哈希表 2」 新创建一个空白的哈希表,为下次 rehash 做准备。
(图片出处:小林Coding)
渐进式rehash
那看起来不是很简单么,rehash就只是拷贝一下就完事儿了?当然不是,由于哈希表的数据量可能会非常大,在拷贝的时候可能会对Redis造成了阻塞,这是不可接受的,所以采用渐进式rehash!将数据的迁移的工作不再是一次性迁移完成,而是分多次迁移。
渐进式 rehash 步骤如下:
- 给「哈希表 2」 分配空间;
- 在 rehash 进行期间,每次哈希表元素进行新增、删除、查找或者更新操作时,Redis 除了会执行对应的操作之外,还会顺序将「哈希表 1 」中索引位置上一个槽中的所有 key-value 迁移到「哈希表 2」 上,可以主要关注函数 _dictRehashStep() 的操作。
- 随着处理客户端发起的哈希表操作请求数量越多,最终在某个时间嗲呢,会把「哈希表 1 」的所有 key-value 迁移到「哈希表 2」,从而完成 rehash 操作。
这样就巧妙地把一次性大量数据迁移工作的开销,分摊到了多次处理请求的过程中,避免了一次性 rehash 的耗时操作。
在进行渐进式 rehash 的过程中,会有两个哈希表,所以在渐进式 rehash 进行期间,哈希表元素的删除、查找、更新等操作都会在这两个哈希表进行。
比如,查找一个 key 的值的话,先会在「哈希表 1」 里面进行查找,如果没找到,就会继续到哈希表 2 里面进行找到。
另外,在渐进式 rehash 进行期间,新增一个 key-value 时,会被保存到「哈希表 2 」里面,而「哈希表 1」 则不再进行任何添加操作,这样保证了「哈希表 1 」的 key-value 数量只会减少,随着 rehash 操作的完成,最终「哈希表 1 」就会变成空表。
int dictAdd(dict *d, void *key, void *val){
dictEntry *entry = dictAddRaw(d,key,NULL);//详解在下面
if (!entry) return DICT_ERR;
dictSetVal(d, entry, val);
return DICT_OK;
}
/* 这个函数仅添加一个新的dictEntry,并不会设置它的值,这是为了让用户自己去填充值
此函数还直接公开给用户API,主要是让用户存储非指针类型的哈希值 */
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
long index;
dictEntry *entry;
int htidx;
// dictIsRehashing(d) => ((d)->rehashidx != -1) 也就是判断是否正在进行rehash
// 如果字典在rehash, 则在处理请求前先搬一部分的key
if (dictIsRehashing(d)) _dictRehashStep(d); //详解在下面
//_dictKeyIndex() => 获取 key 对应的槽索引, 如果key已经存在, 则返回 -1
if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
return NULL; //如果key已经存在, 则返回 NULL
//获取hash表, 如果正在rehash, 则用新的hash表, 否则用旧的hash表
htidx = dictIsRehashing(d) ? 1 : 0;
//上述所说的额外存储mata-data数据的大小
size_t metasize = dictMetadataSize(d);
//分配内存
entry = zmalloc(sizeof(*entry) + metasize);
if (metasize > 0) {
memset(dictMetadata(entry), 0, metasize);
}
//把新entry加入到链表头部
entry->next = d->ht_table[htidx][index];
d->ht_table[htidx][index] = entry;
//计数+1
d->ht_used[htidx]++;
/* Set the hash entry fields. */
dictSetKey(d, entry, key);
return entry;
}
/* 此函数只执行一个rehash的步骤,并且只有在hash尚未暂停时才执行。
当我们的迭代器处于rehash过程中时,我们不能处理这两个哈希表,否则可能会丢失或复制某些元素。 */
static void _dictRehashStep(dict *d) {
//所以先判断是否的暂停的 ,并且只执行一个step
if (d->pauserehash == 0) dictRehash(d,1); //详解在下面
}
/* 递增rehash的N个步骤。如果仍有key要从旧哈希表移动到新哈希表,则返回1,否则返回0。
注意,rehash步骤包括将一个存储桶(在我们使用链接时可能有多个键)从旧哈希表移动到新哈希表,
但是由于哈希表的一部分可能由空空格组成,因此不能保证此函数将rehash哪怕是单个存储桶,
因为它最多访问N*10个空存储桶,否则,它所做的工作量将被解除绑定,函数可能会阻塞很长时间*/
int dictRehash(dict *d, int n) {
int empty_visits = n*10; /* 遍历空桶的最大数量 */
//如果字典不是rehash状态, 直接返回迁移失败
if (!dictIsRehashing(d)) return 0;
//如果n不为空, 且hash表没迁移完
while(n-- && d->ht_used[0] != 0) {
dictEntry *de, *nextde;
/* 注意,rehashidx不会溢出,我们确信会有更多元素,因为ht[0].used!= 0 */
assert(DICTHT_SIZE(d->ht_size_exp[0]) > (unsigned long)d->rehashidx);
//如果table[0]要迁移的索引指向了null, 则表示此桶为空. 这个循环是为了去掉空桶
while(d->ht_table[0][d->rehashidx] == NULL) {
d->rehashidx++;
//如果已经超过规定处理的最大空桶数量, 则直接返回迁移成功
if (--empty_visits == 0) return 1;
}
//获取要迁移的桶中链表首节点
de = d->ht_table[0][d->rehashidx];
/* Move all the keys in this bucket from the old to the new hash HT */
while(de) {
uint64_t h;
nextde = de->next;
//重新计算当前节点在新的hash表中的槽的索引
h = dictHashKey(d, de->key) & DICTHT_SIZE_MASK(d->ht_size_exp[1]);
//插入到链表中
de->next = d->ht_table[1][h];
d->ht_table[1][h] = de;
//旧hash表数量减少新hash表数量增加
d->ht_used[0]--;
d->ht_used[1]++;
//移动到下一个待迁移的节点
de = nextde;
}
d->ht_table[0][d->rehashidx] = NULL;
//待迁移的桶的index++
d->rehashidx++;
}
/* Check if we already rehashed the whole table... */
if (d->ht_used[0] == 0) {
//回收旧hash表的数组
zfree(d->ht_table[0]);
/* 把原来的ht[1] 变为ht[0] */
d->ht_table[0] = d->ht_table[1];
d->ht_used[0] = d->ht_used[1];
d->ht_size_exp[0] = d->ht_size_exp[1];
//然后重置ht[1]
_dictReset(d, 1);
d->rehashidx = -1;
return 0;
}
/* More to rehash... */
return 1;
}
触发条件
rehash 的触发条件跟负载因子(load factor) 有关系:
触发 rehash 操作的条件,主要有两个:
- 当负载因子大于等于 1 ,并且 Redis 没有在执行 bgsave 命令或者 bgrewiteaof 命令,也就是没有执行 RDB 快照或没有进行 AOF 重写的时候,就会进行 rehash 操作。
- 当负载因子大于等于 5 时,此时说明哈希冲突非常严重了,不管有没有有在执行 RDB 快照或 AOF 重写,都会强制进行 rehash 操作。
5.整数集合
整数集合是 Set 对象的底层实现之一。当一个 Set 对象只包含整数值元素,并且元素数量不大时,就会使用整数集这个数据结构作为底层实现。
5.1结构
直接上代码:
typedef struct intset {
//编码方式 4字节
uint32_t encoding;
//集合包含的元素数量 4字节
uint32_t length;
//保存元素的数组 4字节
int8_t contents[];
} intset;
这里contents数组是 int8_t 类型的,其实这里只是给这个数组指针定义为1个字节而已,配合encoding中保存的字节数作为步长,是一样可以挨个访问每个数的。不过不知道为啥这里不用int16_t ,按理说最小的encoding是16呀?
encoding的编码
/* Note that these encodings are ordered, so:
* INTSET_ENC_INT16 < INTSET_ENC_INT32 < INTSET_ENC_INT64. */
#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))
不同类型的 contents 数组,意味着数组的大小也会不同。
新建一个intset
/* Create an empty intset. */
intset *intsetNew(void) {
intset *is = zmalloc(sizeof(intset));
is->encoding = intrev32ifbe(INTSET_ENC_INT16);
is->length = 0;
return is;
}
5.2 intset升级
从上面初始化一个intset的代码可以看到,刚开始的时候encoding编码对应的每个数字16位占2个字节,但是当我们添加了一个int16_t或者int32_t类项的数据时,就需要操作升级,也就是把每个数字占的字节数扩大到新添加的那个数字所需字节数大小。并且需要注意的是,为了确保修改对应数字的内存地址不会覆盖原本的数据,应当从后向前修改第n个元素的对应内存地址,且intset并不会降级! 。
例如,我们在一个encoding为2个字节,并且已存{1,2,3}的intset中添加65535:
(图片出处:小林Coding)
插入一个数并升级
/* Insert an integer in the intset */
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
uint8_t valenc = _intsetValueEncoding(value); //见下
uint32_t pos;
if (success) *success = 1;
/* 如有必要,升级编码。如果我们需要升级,
我们知道这个值应该被附加(如果>0)或前置(如果<0),因为它不在现有值的范围内。 */
if (valenc > intrev32ifbe(is->encoding)) { //此时就需要升级,因为需要的空间大于现在encoding的空间
/* This always succeeds, so we don't need to curry *success. */
return intsetUpgradeAndAdd(is,value); //见下
} else {//此时不需要升级
/* Abort if the value is already present in the set.
* This call will populate "pos" with the right position to insert
* the value when it cannot be found. */
if (intsetSearch(is,value,&pos)) { //注意!这里不光是看是否存在这个元素,还把查找到的这个元素应该有的位置保存在pos中
//如果数组已经存在集合中直接返回,success为0表示已存在
if (success) *success = 0;
return is;
}
// 要加一个元素,所以给intset扩容一个数的大小
is = intsetResize(is,intrev32ifbe(is->length)+1);
//如果要插入的这个元素的位置是在原intset中间,那么就挨个把从pos开始的元素向后挪一个元素大小
//具体挪动多少个字节,是根据is.encoding保存的信息决定的
if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
}
//然后再在pos的位置插入这个元素
_intsetSet(is,pos,value);
// is 的长度+1
is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
return is;
}
/* 返回这个数需要用多大字节的encoding去保存 */
static uint8_t _intsetValueEncoding(int64_t v) {
if (v < INT32_MIN || v > INT32_MAX)
return INTSET_ENC_INT64;
else if (v < INT16_MIN || v > INT16_MAX)
return INTSET_ENC_INT32;
else
return INTSET_ENC_INT16;
}
/* 升级intset ,并且插入这个更大的数 */
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
//当前编码格式
uint8_t curenc = intrev32ifbe(is->encoding);
//判断当前新元素的编码,属于16位、32位还是64位
uint8_t newenc = _intsetValueEncoding(value);
int length = intrev32ifbe(is->length);
int prepend = value < 0 ? 1 : 0;
/* First set new encoding and resize */
is->encoding = intrev32ifbe(newenc);
is = intsetResize(is,intrev32ifbe(is->length)+1); //见下
/* 升级回前台,这样我们就不会覆盖值。
注意 prepend 用于确保在intset的开头或结尾处有一个空格。 */
while(length--) // 序号从后往前
_intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc)); //见下
/* 如果value<0 那么prepend就是true,且比当前set中所有数都要小,所以从头插入,
反之是正数,那一定是比当前所有数大的,所以插到尾部 */
if (prepend)
_intsetSet(is,0,value);
else
_intsetSet(is,intrev32ifbe(is->length),value);
// 把intset长度+1
is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
return is;
}
/* Resize the intset */
static intset *intsetResize(intset *is, uint32_t len) {
// size = 个数 * 每个的大小
uint64_t size = (uint64_t)len*intrev32ifbe(is->encoding);
assert(size <= SIZE_MAX - sizeof(intset));
//分配空间,头+数据
is = zrealloc(is,sizeof(intset)+size);
return is;
}
/* Set the value at pos, using the configured encoding. */
static void _intsetSet(intset *is, int pos, int64_t value) {
uint32_t encoding = intrev32ifbe(is->encoding);
if (encoding == INTSET_ENC_INT64) {
((int64_t*)is->contents)[pos] = value;
memrev64ifbe(((int64_t*)is->contents)+pos);
} else if (encoding == INTSET_ENC_INT32) {
((int32_t*)is->contents)[pos] = value;
memrev32ifbe(((int32_t*)is->contents)+pos);
} else {
((int16_t*)is->contents)[pos] = value;
memrev16ifbe(((int16_t*)is->contents)+pos);
}
}
5.3 查询
在intset中,由于是顺序存储,所以默认查询是使用二分查找。
/* 搜索 value 的位置。
找到时返回1,并将pos设置为intset中值的位置。
找不到时返回0,并将pos设置为可以插入value的位置。 */
static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) {
int min = 0, max = intrev32ifbe(is->length)-1, mid = -1;
int64_t cur = -1;
/* The value can never be found when the set is empty */
if (intrev32ifbe(is->length) == 0) {
if (pos) *pos = 0;
return 0;
} else {
/* Check for the case where we know we cannot find the value,
* but do know the insert position. */
//说人话就是看是不是最大值或者最小值,如果是就直接范围头部或者尾部
if (value > _intsetGet(is,max)) {
if (pos) *pos = intrev32ifbe(is->length);
return 0;
} else if (value < _intsetGet(is,0)) {
if (pos) *pos = 0;
return 0;
}
}
//重点,这里用了二分查找
while(max >= min) {
mid = ((unsigned int)min + (unsigned int)max) >> 1;
cur = _intsetGet(is,mid);
if (value > cur) {
min = mid+1;
} else if (value < cur) {
max = mid-1;
} else {
break;
}
}
if (value == cur) {
if (pos) *pos = mid;
return 1;
} else {
if (pos) *pos = min;
return 0;
}
}
6.跳表
6.1 结构
Redis 只有在 Zset 对象的底层实现用到了跳表,跳表的优势是能支持平均 O(logN) 复杂度的节点查找。
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
sds ele; //字符串
double score; //权值
struct zskiplistNode *backward; //前节点
struct zskiplistLevel {
struct zskiplistNode *forward; //这一层的下一个节点
unsigned long span; //跨度
} level[];
} zskiplistNode;
//跳表头结点
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level; //跳表层级数
} zskiplist;
/*Zset 对象是唯一一个同时使用了两个数据结构来实现的 Redis 对象,这两个数据结构一个是跳表,
这样的好处是既能进行高效的范围查询,也能进行高效单点查询。 */
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
6.2 跳表中查找一个节点 (出处:小林Coding )
查找一个跳表节点的过程时,跳表会从头节点的最高层开始,逐一遍历每一层。在遍历某一层的跳表节点时,会用跳表节点中的 SDS 类型的元素和元素的权重来进行判断,共有两个判断条件:
- 如果当前节点的权重「小于」要查找的权重时,跳表就会访问该层上的下一个节点。
- 如果当前节点的权重「等于」要查找的权重时,并且当前节点的 SDS 类型数据「小于」要查找的数据时,跳表就会访问该层上的下一个节点。
如果上面两个条件都不满足,或者下一个节点为空时,跳表就会使用目前遍历到的节点的 level 数组里的下一层指针,然后沿着下一层指针继续查找,这就相当于跳到了下一层接着查找。
举个例子,下图有个 3 层级的跳表。
如果要查找「元素:abcd,权重:4」的节点,查找的过程是这样的:
- 先从头节点的最高层开始,L2 指向了「元素:abc,权重:3」节点,这个节点的权重比要查找节点的小,所以要访问该层上的下一个节点;
- 但是该层上的下一个节点是空节点,于是就会跳到「元素:abc,权重:3」节点的下一层去找,也就是 leve[1];
- 「元素:abc,权重:3」节点的 leve[1] 的下一个指针指向了「元素:abcde,权重:4」的节点,然后将其和要查找的节点比较。虽然「元素:abcde,权重:4」的节点的权重和要查找的权重相同,但是当前节点的 SDS 类型数据「大于」要查找的数据,所以会继续跳到「元素:abc,权重:3」节点的下一层去找,也就是 leve[0];
- 「元素:abc,权重:3」节点的 leve[0] 的下一个指针指向了「元素:abcd,权重:4」的节点,该节点正是要查找的节点,查询结束。
6.3 跳表的层数是如何设置的
跳表的相邻两层的节点数量的比例会影响跳表的查询性能。如果设置的不合理,那么就会退化为链表,复杂度就成为了O(n)。所以跳表每层的数量比最理想时应该为2:1,这样时间复杂度理论上说就是0(log n)。
我们第一时间肯定会想到持续去维护一个固定的第n个节点应该是m层的策略,但是只要删除或者添加节点就会使表结构有很大的变化,造成很大的开销。而Redis 则采用一种巧妙的方法是,跳表在创建节点的时候,随机生成每个节点的层数,并没有严格维持相邻两层的节点数量比例为 2 : 1 ,而是使他们的期望是2:1,那么期望来说查询的时间复杂度也就是0(log n)。
具体的做法我们直接上代码:
创建一个快表,并带头结点
#define ZSKIPLIST_MAXLEVEL 32 /* Should be enough for 2^64 elements */
#define ZSKIPLIST_P 0.25 /* 这是每个节点需要加一层的概率,因为理论的最优是每有4个h层节点,就应该有1个h+1层节点 */
/* Create a new skiplist. */
zskiplist *zslCreate(void) {
int j;
zskiplist *zsl;
//分配zskiplist的内存空间
zsl = zmalloc(sizeof(*zsl));
//默认只有1层,长度为0
zsl->level = 1;
zsl->length = 0;
//注意!!!! 跳表是有一个头结点的!! 创建一个32层的节点,权值0,SDS字符串为NULL
zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);//见下
//把建好的每层下一指针指向null
for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {
zsl->header->level[j].forward = NULL;
zsl->header->level[j].span = 0;
}
//前节点也指向null
zsl->header->backward = NULL;
//尾部节点也是NULL
zsl->tail = NULL;
return zsl;
}
/* 创建指定level的skiplist节点。SDS字符串ele在调用后由节点引用。 */
zskiplistNode *zslCreateNode(int level, double score, sds ele) {
//分配内存空间为 1个 zskiplistNode 的大小 + level层 * zskiplistLevel 的大小
zskiplistNode *zn = zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));
zn->score = score;
zn->ele = ele;
return zn;
}
插入一个新的节点
总体思路大概分为3步:
- 根据目前传入的score找到插入位置x,这个过程会保存各层x的前一个位置节点。
- 根据随机函数获取level,并且生成新节点。
- 修改各个指针的指向,将创建的新节点插入。
/* 在skiplist中插入一个新节点。
假设该元素不存在(由调用方强制执行)。
skiplist获得传递的SDS字符串ele的所有权。 */
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x; // ZSKIPLIST_MAXLEVEL = 32
unsigned long rank[ZSKIPLIST_MAXLEVEL];
int i, level;
serverAssert(!isnan(score));
x = zsl->header;
// 从上层向下层遍历
for (i = zsl->level-1; i >= 0; i--) {
rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
/* 保存插入的这个节点x 的前一个节点位置,
方便插入后把这层对应的指针指过来 */
while (x->level[i].forward &&
(x->level[i].forward->score < score ||
(x->level[i].forward->score == score &&
sdscmp(x->level[i].forward->ele,ele) < 0)))
{
rank[i] += x->level[i].span;
x = x->level[i].forward;
}
update[i] = x;
}
/* 我们假设元素不在内部,因为我们允许重复的分数,所以永远不应该重新插入相同的元素,
原因是zslInsert()的调用方应该在哈希表(zset既有哈希表又有zskiplist)中测试元素是否已经在内部。 */
level = zslRandomLevel(); //这既是随机层数的函数,见下
if (level > zsl->level) {//新的节点的层数已经大于了这个快表的最大层数了
for (i = zsl->level; i < level; i++) {//把多的这部分层都初始化一下
rank[i] = 0;
update[i] = zsl->header;//x的前节点当然是头结点了
update[i]->level[i].span = zsl->length;// x 的跨度也就是从头到尾的长度
}
zsl->level = level;
}
//新建节点,层数就是刚刚随机的层数
x = zslCreateNode(level,score,ele);
for (i = 0; i < level; i++) {//这里从第一层开始遍历
//x节点每层的前节点就是update中记录的
x->level[i].forward = update[i]->level[i].forward;
update[i]->level[i].forward = x;
/* 在这里插入x节点的时候,要更新被updata[i]覆盖的范围 */
x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
update[i]->level[i].span = (rank[0] - rank[i]) + 1;
}
/* 把没有触及到的层的span +1 */
for (i = level; i < zsl->level; i++) {
update[i]->level[i].span++;
}
x->backward = (update[0] == zsl->header) ? NULL : update[0];
if (x->level[0].forward)
x->level[0].forward->backward = x;
else
zsl->tail = x;
zsl->length++;
return x;
}
/* 返回我们将要创建的新skiplist节点的随机层数。
此函数的返回值∈[1,32],具有类似幂律的分布,其中返回更高级别的可能性较小。 */
int zslRandomLevel(void) {
static const int threshold = ZSKIPLIST_P*RAND_MAX; //ZSKIPLIST_P = 0.25 所以门限是1/4的随机数最大值
int level = 1;
//随机数如果落在了概率为1/4的区域,那么层数+1
while (random() < threshold)
level += 1;
//当然,不能超过最大值
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
这样的做法,相当于每增加一层的概率不超过 25%,层数越高,概率越低。