zset 似于 Java 的 SortedSet 和 HashMap 的结合体,一方面它是一个 set,保证了内部 value 的唯一性,另一方面它可以给每个 value 赋予一个 score,代表这个 value 的排序权重。
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.实现排行榜
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 的整体结构:
- 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 对应的分值。
SkipList 方式
跳表实际就是:多级有序链表,这样我们就可以抽出索引节点,从而降低查询的复杂度。
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;