zset 似于 Java 的 SortedSet 和 HashMap 的结合体,一方面它是一个 set,保证了内部 value 的唯一性,另一方面它可以给每个 value 赋予一个 score,代表这个 value 的排序权重。

zset是把 score当作索引吗_链表

1.常用命令

ZADD key score member [[score member]…]		// 往有序集合key中加入带分值元素
ZCARD key									// 返回有序集合key中元素个数
ZREM key member [member …]					// 从有序集合key中删除元素

ZINCRBY key increment member				// 为有序集合key中元素member的分值加上increment 
ZSCORE key member 							// 返回有序集合key中元素member的分值
ZRANGE key start stop [WITHSCORE] 		    // 正序获取有序集合key从start下标到stop下标的元素(withscores 参数可以附带获取元素的 score)
ZREVRANGE key start stop [WITHSCORE]		// 倒序获取有序集合key从start下标到stop下标的元素(withscores 参数可以附带获取元素的 score)

ZUNIONSTORE destkey numkeys key [key ...] 	// 并集计算,deskey(合并后的结果集、无重复),numkeys(要合并的集合的个数),key(要合并的集合)
ZINTERSTORE destkey numkeys key [key …]		// 交集计算

2.应用示例

1.实现排行榜


zset是把 score当作索引吗_链表_02

ZINCRBY  hotNews:20201104  这就是开放不止步的中国   						// 点击新闻,当前新闻score+1

ZREVRANGE  hotNews:20201104  0  10  									// 展示当日排行前十

ZUNIONSTORE  hotNews:week 7 hotNews:20201104 ... hotNews:20201110   	// 合并七日的结果到 hotNews 这个Set中
ZREVRANGE   hotNews:week 0 7				 							// 在 hotNews 中取七日搜索榜单前七个

2.其余功能

ZSet 还可以用来存粉丝列表,value 值是粉丝的用户 ID,score是关注时间。我们可以对粉丝列表按关注时间进行排序。

ZSet 也可以用来存储学生的成绩,value 值是学生的 ID,score是他的考试成绩。我们可以对成绩按分数进行排序就可以得到他的名次。

3.存储原理

Zset 底层同样采用了两种方式来实现,分别是 ZipList 和 SkipList。当同时满足以下两个条件时,采用 ZipList 实现;反之采用 SkipList 实现。

  • Zset 中保存的元素个数小于 128。(通过修改 zset-max-ziplist-entries 配置来修改)
  • Zset 中保存的所有元素长度小于 64byte。(通过修改 zset-max-ziplist-values 配置来修改)

ZipList 方式

压缩列表是 redis 为了节约内存而开发的,是由一系列的特殊编码的连续内存块组成的双向链表。一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值,值的类型和长度由节点的encoding属性决定。

它不存储指向上一个链表节点和指向下一个链表节点的指针,而是存储上一个节点长度和当前节点长度,也就是和说与数组的区别在于数组的每个元素大小相同,而 ziplist 的每个节点的大小不是固定(保存->计算地址)。

ziplist 通过牺牲部分读写性能,来换取高效的内存空间利用率,是一种时间换空间的思想。只用在字段个数少,字段值小的场景里面。

来看 ziplist 的整体结构:

zset是把 score当作索引吗_zset是把 score当作索引吗_03

  • zlbytes:表示当前 list 的存储元素的总长度
  • zllen:表示当前 list 存储的元素的个数
  • zltail:表示当前 list 的头结点的地址,通过 zltail 就是可以实现 list 的遍历
  • zlend:表示当前 list 的结束标识

下面看具体的元素 zlentry 是怎么定义的:

typedef struct zlentry {
	
	/* 上一个链表节点占用的长度 */
	unsigned int prevrawlensize; 
	/* 存储上一个链表节点的长度数值所需要的字节数 */
	unsigned int prevrawlen; 
	/* 存储当前链表节点长度数值所需要的字节数 */
	unsigned int lensize; 
	/* 当前链表节点占用的长度 */
	unsigned int len; 
	/* 当前链表节点的头部大小(prevrawlensize + lensize),即非数据域的大小 */
	unsigned int headersize; 
	/* 编码方式 */
	unsigned char encoding; 
	/* 压缩链表以字符串的形式保存,该指针指向当前节点起始位置 */
	unsigned char *p; 
	
} zlentry;

ZipList 的优缺点比较

  • 优点:内存地址连续,省去了每个元素的头尾节点指针占用的内存
  • 缺点:对于删除和插入操作比较可能会触发连锁更新反应,比如在 list 中间插入删除一个元素时,在插入或删除位置后面的元素可能都需要发生相应的移动操作

对于 Zset 不同的是,其存储是以键值对的方式依次排列,键存储的是实际 value,值存储的是 value 对应的分值。

zset是把 score当作索引吗_List_04

SkipList 方式

跳表实际就是:多级有序链表,这样我们就可以抽出索引节点,从而降低查询的复杂度。

zset是把 score当作索引吗_有序集合_05

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

typedef struct zset {
    dict *dict;
	zskiplist *zsl;
} zset

typedef struct zskiplistNode {

    /* zset 的元素 */
	sds ele; 
	/* 分值 */
    double score; 
    /* 后退指针 */
    struct zskiplistNode *backward; 
    
    struct zskiplistLevel {
    	/* 前进指针,对应 level 的下一个节点 */
        struct zskiplistNode *forward; 
        /* 从当前节点到下一个节点的跨度(跨越的节点数) */
        unsigned long span; 
    } level[]; /* 层 */
    
} zskiplistNode;

typedef struct zskiplist {

	/* 指向跳跃表的头结点和尾节点 */
    struct zskiplistNode *header, *tail; 
    /* 跳跃表的节点数 */
    unsigned long length; 
    /* 最大的层数 */
    int level; 
    
} zskiplist;