Sorted Set
有序集合ZSet,但是有序集合的英文明明是sorted sets。 那这个“Z”代表什么意思,这点官网没有解释,但是gitHub上有人问过,作者是这样回答的
Hello. Z is as in XYZ, so the idea is, sets with another dimension: the
order. It’s a far association… I know 😃
这句话的本意是:这里的Z就像XYZ中的Z一样,所以这个概念是,集合中还有另一个维度:顺序。这是一个遥远的关联…
在我理解来的话,这应该只是为了和原本的Sets集合做一个区分,Z代表的就是原本顺序之外的一个规则,就像XYZ中的Z一样,是二维平面之外,另一个维度的规则。
结构
在redis 7.0之前有两种编码:ziplist、skiplist
7.0之后是listpack、skiplist
主要区别就是ziplist和listpack
我们先来简单说一下两种格式:(如果了解跳表,可以直接跳转到 skiplist 看跳表的结构)
ziplist和listpack:都是一种压缩列表的实现,当保存的元素长度都小于64字节,同时数量小于128时,会使用该结构(可以认为就是有序列表 list )
与我们之前的理解list不同的地方是,他们占用的磁盘是连续的,没有节点之间的指针,而是将数据按照顺序一个个的排列。
那么查询的时候就要从第一个节点一个个的往后捋出来。虽然不会造成碎片空间,但这也是压缩列表的局限性。
ziplist
下面是这个ziplist列表的节点数据, 可以看到里面的属性 prevrawlensize,这个属性标记了前一个节点长度。
缺点就是 这个列表如果要更新第一个节点数据的话,可能会造成后面所有节点的长度数据全部更新。
(题外话:其实之前讲的redis的list结构中,每个节点Node里面都是一个ziplist,只不过我们只需要知道就可以了,在使用的时候不需要关心这些)
typedef struct zlentry {
unsigned int prevrawlensize; /* 用于编码前一个节点字节长度*/
unsigned int prevrawlen;
unsigned int lensize; /* 用于编码此节点类型/长度的字节。
例如,字符串有1、2或5个字节标题。
整数总是使用一个字节。
*/
unsigned int len; /* 用于表示节点实际的字节。
对于字符串,这只是字符串长度
而对于整数,它是1、2、3、4、8或
0,具体取决于数字范围。
*/
unsigned int headersize; /* prevrawlensize + lensize. */
unsigned char encoding; /* 设置为ZIP_STR_*或ZIP_INT_*,具体取决于节点编码。*/
unsigned char *p; /* 第一个节点的地址指针,prev-entry-len */
} zlentry;
listpack
listpack 列表最大的特点就是不再包含前一个节点的长度,那么在更新的时候就不会再造成连锁更新问题。
但是由于压缩列表本身的局限性,只能顺序查询,为了效率,在数据量超过64的时候,会变成跳表形式
typedef struct {
/* 当使用string时,它具有长度(slen)。 */
unsigned char *sval;
uint32_t slen;
/* 当使用integer时,“sval”为 NULL,lval 保存该值。*/
long long lval;
} listpackEntry;
跳表zskiplist
跳表就是ZSets 有序列表的主要结构模式
skiplist 中也是包含两种结构,但是要注意,这里的两种结构是同时存在的 字典(dict)和跳跃表(zskiplist)存储方式。
dict就不说了,在上一章的Hash中说过,可以认为她就是一个k-v结构的数据。 里面的key是存储的数据,value是数据的score分数。
zskiplist:是一个具有跳跃节点能力的链表,给每个节点附加了一个level层级的属性,这个level会指向后面的 某一个节点,通过这个level层级可以直接越过中间的节点,减少查询的时间。
为了比较容易理解,这里画了一个示例图
L1、L2、L3… 这些就是每个节点的层级,规定了最高的层级是32层。 每个节点查询的时候,就可以通过高层直接跳跃到后面;
如果发现分数过大的话,可以通过低一些的层级少跳跃一些节点。
往里面放数据的时候,会给这个数据+分数封装为一个节点,然后给这个节点随机一个1~32范围内level的层高。
然后从头开始查询,通过level跳跃过N个节点,直接将节点放到对应的位置,然后给给每一层的level都添加一个指向下一个节点的指针。
具体实现结构如下:
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
typedef struct zskiplistNode {
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned int span;
} level[];
struct zskiplistNode *backward;
double score;
robj *obj;
} zskiplistNode;
Sorted Set 为什么使用跳跃表,而不是红黑树?
主要有以下几个原因:
- 跳表的性能和红黑树差不多。
- 插入速度非常快速,因为不需要进行旋转等操作来维持平衡性
- 跳表更容易实现和调试。
跳表中的dict是什么用处?
通过上面的数据结构不难发下,跳表只适合单位查询,排序处理。但是不适合查询分数,以及判断成员是否存在这种操作。
那么这时候dict就派上用场了,之前说过,dict的结构是key-value键值对。
比如我们的数据是 周杰伦 100分、孙燕姿 99分、许嵩 98分
那在跳表zskiplist 中的数据是
(level:[], score:100分, value:周杰伦)
(level:[], score:99分, value:孙燕姿)
(level:[], score:98分, value:许嵩)
而在字典项中的数据是
(key:周杰伦, score:100分)
(key:孙燕姿,value:99分)
(key:许嵩, value:98分)
要查询某个数据是否存在,或者是查询分数的话,直接从dict的数据结构中通过key来取出分数就可以了,不需要在列表中查询。
这也是一种为了效率,把数据冗余一份的策略