开篇
zset和set类似,不过zset是有序的集合,排序原因是:zset的每个元素都会关联一个 double 类型的分数,redis 正是通过分数来为集合中的成员进行从小到大的排序
基础命令
- zadd key [score member]:添加一个或多个元素,或者更新已存在元素的分数
- zcard key : 获取集合元素的数量
- zcount key min max:返回指定区间分数的元素数量(分数)
- zlexcount key min max: 计算指定字典区间内元素数量(元素)
- zrange key start stop :返回指定区间的元素
myRedis:0>zadd key1 10 test1 20 test2 30 test3 40 test4
"4"
myRedis:0>zcard key1
"4"
myRedis:0>zcount key1 10 30
"3"
myRedis:0>zlexcount key1 [test2 [test4
"3"
myRedis:0>zrange key1 1 3
1) "test2"
2) "test3"
3) "test4"
- zscore key member: 返回指定member的分数
- zincrby key increment member:给指定的member增加increment
- zrem key [member]:移除一个或多个元素
myRedis:0>zscore key1 test1
"10"
myRedis:0>zincrby key1 50 test1
"60"
myRedis:0>zscore key1 test1
"60"
myRedis:0>zrem key1 test2
"1"
myRedis:0>zrange key1 0 -1
1) "test3"
2) "test4"
3) "test1"
- zremrangebyrank key start stop:移除给定的排名区间的所有元素
- zremrangebyscore key min max:移除给定的分数区间的所有元素
myRedis:0>zadd key1 55 test6 62 test7 44 test8 59 test9
"4"
myRedis:0>zrange key1 0 -1
1) "test3" // 30
2) "test4" // 40
3) "test8" // 44
4) "test6" // 55
5) "test9" // 59
6) "test1" // 60
7) "test7" // 62
myRedis:0>zremrangebyrank key1 0 1
"2"
myRedis:0>zrange key1 0 -1
1) "test8"
2) "test6"
3) "test9"
4) "test1"
5) "test7"
myRedis:0>zremrangebyscore key1 50 60
"3"
myRedis:0>zrange key1 0 -1
1) "test8"
2) "test7"
存储编码
Redis中存储zset有两种编码:ziplist和skiplist
ziplist
ziplist再链表的篇章中已经详细介绍:Redis-List篇
这里需要注意的是,采用ziplist作为底层编码时,每个entry都包含元素的成员member和元素的分值score,并且ziplist按照分值从小到大进行排序
skiplist
skiplist称为跳跃表,也简称跳表,在满足一定条件时,redis使用skiplist作为底层存储编码
skiplist查找图解
- 我们先看下普通的链表结构
普通链表查找只能遍历,链表不支持随机查找,不能使用二分法 - 可以模拟二分法原理,把3 7 11 提取出来
如果,我们需要查找的时9,可以在模拟二分法的情况下我们可以先查 3 7 11 先确定范围,确定9在7~11区间内,既可直接定位到元素的位置 - 如果数量比较多,也可以按照以上规则,抽取不同的数据来提高数据的层级,提高查找效率
- 把level的属性放到对应的元素上,可以看到如图结构
每一个level层级都有指向下一个相同level层级元素的指针
筛选规则:(查找元素7)
- 先查找第三层数据,确定区间范围 3~11
- 再查找第二层数据,确定范围 5~9
- 最后查找第一层数据,确定元素7的位置
level生成规则
level生成是随机的,并没有指定的规律,可以看下随机函数源码(t_zset.c):
创建新节点的时候根据幂次定律(power law)随机生成的一个介于1~32之间的数字
/* Returns a random level for the new skiplist node we are going to create.
* The return value of this function is between 1 and ZSKIPLIST_MAXLEVEL
* (both inclusive), with a powerlaw-alike distribution where higher
* levels are less likely to be returned. */
int zslRandomLevel(void) {
int level = 1;
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
源码分析
skiplist源码(server.h)
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
typedef struct zskiplist {
struct zskiplistNode *header, *tail; //跳跃表的头和尾节点 指针
unsigned long length; // 元素数量
int level; // 等级level
} zskiplist;
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
sds ele; //元素
double score; //分值
struct zskiplistNode *backward; //后退指针
struct zskiplistLevel {
struct zskiplistNode *forward; //前进指针
unsigned long span;//当前节点到下一个节点的节点数
} level[];
} zskiplistNode;
当Redis的zset使用了skiplist的编码时,数据结构为zset,zset是封装了dict和 zskiplist的结构
属性 | 含义 | 简介 |
---|---|---|
level | 层级数组 | 一个节点的元素可以拥有多个层,通过不同层级的指针来选择最快捷的路径提升访问速度 |
forward | 前进指针 | 指向链表尾部方向元素的指针 ,每一层都有一个 |
backward | 后退指针 | 指向链表头部方向元素的指针,只有一个 |
span | 跨度 | 记录了两个节点之间的距离,null表示跨度为0 |
ele | 元素 | SDS对象 |
score | 分值 | 节点的分值是一个double类型的浮点数,从小到大排序,不同的节点分值可以重复 |
skiplist存储图解
图解1:
图解2
备注:虽然图解看起来zskiplist和hashtable像是两套数据,只是为了画图解析方便,实际上他们是共享数据和分值,没有存储两份数据,也并没有浪费内存空间
skiplist的编码特殊性
skiplist(数据编码)的底层使用的是封装了dict(字典)和zskiplist(跳跃表结构)的zset结构
- 如果单独使用dict,查询性能很高,但是无序
- 如果单独使用zskiplist,顺序可以保证,但是查询虽然有level,但是查询效率仍然不如dict
- 如果同时使用dict和zskiplist,底层共享基础数据,就可以兼顾查询性能和顺序
编码转换规则
- 保存的元素个数小于128个(zset-max-ziplist-entries,可修改)
- 所有元素的总长度小于64字节(zset-max-ziplist-value,可修改)
同时满足以上两个条件,用ziplist编码存储,例:
myRedis:0>object encoding key1
"ziplist"
基础篇结束语
从Redis总纲到Zset,每个篇章中从基础命令到存储结构,把Redis基本内容都介绍了一遍,到此基础篇已经完结,接下来的要介绍的是Redis的事务,内存回收,持久化相关的内容