Redis中的集合主要分为有序和无序的,而且都不重复。
集合常见命令
- 无序集合常见命令
// 添加一个元素
127.0.0.1:6379> sadd name "tom"
(integer) 1
// 重复添加,则添加失败
127.0.0.1:6379> sadd name "tom"
(integer) 0
// 成功添加多个元素,返回成功添加元素个数
127.0.0.1:6379> sadd name "jack" "peter"
(integer) 2
// jack已经存在,所以成功添加了Sam,返回1
127.0.0.1:6379> sadd name "jack" "Sam"
(integer) 1
// 获取所有元素,也能看出是无序的
127.0.0.1:6379> SMEMBERS name
1) "jack"
2) "peter"
3) "Sam"
4) "tom"
// 查询某个value是否存在
127.0.0.1:6379> SISMEMBER name tom
(integer) 1
127.0.0.1:6379> SISMEMBER name Lary
(integer) 0
// 统计元素个数
127.0.0.1:6379> scard name
(integer) 4
// 随机弹出一个元素
127.0.0.1:6379> spop name
"Sam"
// 删除集合中指定元素
127.0.0.1:6379> SREM name tom
(integer) 1
- 有序集合常见命令
// 添加一个height的key,value是tom,170.5表示这个value的权重用作排序
127.0.0.1:6379> zadd height 170.5 "tom"
(integer) 1
127.0.0.1:6379> zadd height 177.5 "jack"
(integer) 1
127.0.0.1:6379> zadd height 167.5 "Lary"
(integer) 1
// 统计height的元素数量
127.0.0.1:6379> zcard height
(integer) 3
// 统计执行权重区间的元素个数 范围是个双闭合区间
127.0.0.1:6379> zcount height 165.0 175
(integer) 2
127.0.0.1:6379> zcount height 167.5 177.5
(integer) 3
// 从表头开始遍历,返回指定范围内元素
127.0.0.1:6379> ZRANGE height 0 3
1) "Lary"
2) "tom"
3) "jack"
// 从表尾遍历 返回指定范围内元素,0,-1表示遍历全部
127.0.0.1:6379> ZREVRANGE height 0 -1
1) "jack"
2) "tom"
3) "Lary"
// 从头到尾查找指定元素,并且返回当前排名,
127.0.0.1:6379> ZRANK height tom
(integer) 1
127.0.0.1:6379> ZRANK height Lary
(integer) 0
127.0.0.1:6379> ZRANK height jack
(integer) 2
数据结构
由于无序集合使用字典作为底层实现,只不过将所有的字典值设置NULL,就不介绍了,主要看下有序集合的数据结构。 有序集合底层有两种数据结构,一个是压缩列表,另一个是跳跃列表。
压缩列表
有序集合如果采用压缩列表存储的话,每个entry都包含一个值和一个分值(权重)。按照分值从小打到大排序。
同时满足下面两种情况采用压缩列表数据结构:
- 集合中的的元素数量小于128个
- 集合中的保存的每个元素长度都小于64个字节
跳跃列表
如果有序集合底层采用的skiplist编码。那么它维护了一个跳跃表和字典作为底层实现
// 有序集合结构体
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
// 字典结构体
typedef struct dict {
dictType *type;
// 字典私有数据
void *privdata;
// dictht数组
dictht ht[2];
// rehash时表示的状态,-1表示完成,0表示开始,每个元素rehash时完成+1
long rehashidx;
unsigned long iterators;
} dict;
// 跳跃表结构体
typedef struct zskiplist {
// 跳表中的节点,采用双向指针
struct zskiplistNode *header, *tail;
unsigned long length;
// 跳表的层级
int level;
} zskiplist;
typedef struct zskiplistNode {
// 元素值
sds ele;
// 元素的分数
double score;
// 后退指针,指向前一个节点
struct zskiplistNode *backward;
// 指向另外一个层级
struct zskiplistLevel {
// 前进指针
struct zskiplistNode *forward;
// 前一个节点和当前节点之间的元素个数
unsigned long span;
} level[];
} zskiplistNode;
上图中,有序集合中包含了 6个元素,它们分数是 [1,11,21,31,41,51,61]。它们的值假设为 zsNode1,zsNode11,zsNode21,zsNode31,zsNode41,zsNode51,zsNode61。以zsNode21为例的话,它分别在第0层,第1层,第2层。但是三层的指针都是指向同一个地址的。
有序结合采用了字典加跳表来实现,保证了以O(1)复杂度查找元素,也保证了跳跃表范围查找的有点。
跳表的创建
- 确定节点的层高
#define ZSKIPLIST_MAXLEVEL 64
#define ZSKIPLIST_P 0.25
int zslRandomLevel(void) {
int level = 1;
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
Redis跳表随机获取1-64之间个层数,当然层数越高出现概率越小。这里就涉及节点期望层高的算法了。(一个计算期望公式,不得已翻下高中课本),一个节点的层高确定了就不会修改了。
- 创建节点
跳表中的每个节点都是有序集合一个元素,层高在最开始确定了,而分值和ele也是命令输入的。
- 创建一个空的跳表
从跳表的数据结构看,头部节点指向的一个64层并且不保存分数和元素值的一个节点。所以在创建头节点时候,层高是ZSKIPLIST_MAXLEVEL,分值是0,元素值是NULL。这样一个只有头节点的跳表就创建了。剩下就是插入节点补充这个跳表的各个节点使其完整。
添加节点
- 确定插入位置
通过上面跳表数据结构,有些节点会分布在不同层上,查找节点先从最高层的链表开始查找,当查找到某个节点比目标节点大或者是下个指针是NULL的话,就从当前节点下一层的链表继续往后查找。
通过 zskipListNode 数组 update[64]来保存需要被更新的节点。rank[64]来记录当前层从header节点到update[i]节点所经历的步长,在更新update[i]的span和设置新插入节点的span时用到。
- 高度变更
插入节点的高度是随机的,假设要插入节点的高度为3,大于跳跃表的高度2,所以我们需要调整跳跃表的高度。
rank是用来更新span的变量,其值是头节点到update[i]所经过的节点数,而此次修改的是头节点,所以rank[2]为0,update[2]一定为头节点。update[2]->level[2].span的值先赋值为跳跃表的总长度,后续在计算新插入节点level[2]的span时会用到此值。在更新完新插入节点level[2]的span之后会对update[2]->level[2].span的值进行重新计算赋值。
-
插入节点
以上面分数跳表为例。看下每次循环跳表结构的变化。
- x的level[0]的forward为update[0]的level[0]的forward节点,即x->level[0].forward为score=41的节点。
- update[0]的level[0]的下一个节点为新插入的节点。
- rank[0]-rank[0]=0,update[0]->level[0].span=1,所以x->level[0].span=1。
- update[0]->level[0].span=0+1=1。
- x的level[1]的forward为update[1]的level[1]的forward节点,即x->level[1].forward为NULL。
- update[1]的level[1]的下一个节点为新插入的节点。
- rank[0]-rank[1]=1,update[1]->level[1].span=2,所以x->level[1].span=1。
- update[1]->level[1].span=1+1=2。
-
x的level[2]的forward为update[2]的level[2]的forward节点,即x->level[2].forward为NULL。
-
update[2]的level[2]的下一个节点为新插入的节点。
-
rank[0]-rank[2]=2,因为update[2]->level[2].span=3,所以x->level[2].span=1。
-
update[2]->level[2].span=2+1=3
Redis跳表设计很是巧妙,对应的数据结构也比较复杂,参考了Redis5 源码分析,debug下能看下每个添加节点,各个层数,各个节点之间变化。