redis面试(四)ZSet数据结构

Sorted Set

有序集合ZSet,但是有序集合的英文明明是sorted sets。 那这个"Z"代表什么意思,这点官网没有解释,但是gitHub上有人问过,作者是这样回答的

c 复制代码
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,只不过我们只需要知道就可以了,在使用的时候不需要关心这些)

c 复制代码
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的时候,会变成跳表形式

c 复制代码
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都添加一个指向下一个节点的指针。

具体实现结构如下:

c 复制代码
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来取出分数就可以了,不需要在列表中查询。

这也是一种为了效率,把数据冗余一份的策略

相关推荐
奶糖趣多多2 小时前
Redis知识点
数据库·redis·缓存
CoderIsArt3 小时前
Redis的三种模式:主从模式,哨兵与集群模式
数据库·redis·缓存
wheeldown6 小时前
【数据结构】选择排序
数据结构·算法·排序算法
ketil277 小时前
Redis - String 字符串
数据库·redis·缓存
鱼跃鹰飞8 小时前
大厂面试真题-简单说说线程池接到新任务之后的操作流程
java·jvm·面试
王佑辉9 小时前
【redis】延迟双删策略
redis
生命几十年3万天9 小时前
redis时间优化
数据库·redis·缓存
躺不平的理查德10 小时前
数据结构-链表【chapter1】【c语言版】
c语言·开发语言·数据结构·链表·visual studio
阿洵Rain10 小时前
【C++】哈希
数据结构·c++·算法·list·哈希算法
Leo.yuan10 小时前
39页PDF | 华为数据架构建设交流材料(限免下载)
数据结构·华为