同时满足以下条件时使用ziplist 编码:

元素数量小于128 个

所有member 的长度都小于64 字节

在ziplist 的内部,按照score 排序递增来存储。插入的时候要移动之后的数据。

对应redis.conf 参数:

zset-max-ziplist-entries 128

zset-max-ziplist-value 64

超过阈值之后,使用skiplist+dict 存储。

问题:什么是skiplist?

我们先来看一下有序链表:

redis zset ID增长 redis zset长度_redis zset ID增长

在这样一个链表中,如果我们要查找某个数据,那么需要从头开始逐个进行比较,直到找到包含数据的那个节点,或者找到第一个比给定数据大的节点为止(没找到)。也就是说,时间复杂度为O(n)。同样,当我们要插入新数据的时候,也要经历同样的查
找过程,从而确定插入位置。

而二分查找法只适用于有序数组,不适用于链表。

假如我们每相邻两个节点增加一个指针(或者理解为有三个元素进入了第二层),让指针指向下下个节点。

redis zset ID增长 redis zset长度_redis zset ID增长_02

这样所有新增加的指针连成了一个新的链表,但它包含的节点个数只有原来的一半(上图中是7, 19, 26)。在插入一个数据的时候,决定要放到那一层,取决于一个算法(在redis 中t_zset.c 有一个zslRandomLevel 这个方法)。

现在当我们想查找数据的时候,可以先沿着这个新链表进行查找。当碰到比待查数据大的节点时,再回到原来的链表中的下一层进行查找。比如,我们想查找23,查找的路径是沿着下图中标红的指针所指向的方向进行的:

redis zset ID增长 redis zset长度_链表_03

1. 23 首先和7 比较,再和19 比较,比它们都大,继续向后比较。

2. 但23 和26 比较的时候,比26 要小,因此回到下面的链表(原链表),与22比较。

3. 23 比22 要大,沿下面的指针继续向后和26 比较。23 比26 小,说明待查数据23 在原链表中不存在

在这个查找过程中,由于新增加的指针,我们不再需要与链表中每个节点逐个进行比较了。需要比较的节点数大概只有原来的一半。这就是跳跃表。

为什么不用AVL 树或者红黑树?因为skiplist 更加简洁。

源码:server.h

typedef struct zskiplistNode {
	sds ele; /* zset 的元素*/
	double score; /* 分值*/
	struct zskiplistNode *backward; /* 后退指针*/
	struct zskiplistLevel {
		struct zskiplistNode *forward; /* 前进指针,对应level 的下一个节点*/
		unsigned long span; /* 从当前节点到下一个节点的跨度(跨越的节点数) */
	} level[]; /* 层*/
} zskiplistNode;
typedef struct zskiplist {
	struct zskiplistNode *header, *tail; /* 指向跳跃表的头结点和尾节点*/
	unsigned long length; /* 跳跃表的节点数*/
	int level; /* 最大的层数*/
} zskiplist;

随机获取层数的函数:

源码:t_zset.c

int zslRandomLevel(void) {
	int level = 1;
	while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
	level += 1;
	return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}