Redis有序集合(Sorted Sets)
Redis中的有序集合类似集合与哈希表的混合体。和集合一样,有序集合中同样存储了一组互不相同的字符串元素。在某些场景下,有序集合使用时就可以被看做是一种集合。
与集合不同的是,有序集合中的每个元素还与一个额外的浮点数关联,称为评分 (类似哈希表,元素与一个值关联)。
此外,有序集合中的元素都是按序排列的。排序规则如下:
- 如果A与B两个元素评分不同,如果A的评分大于B的评分,那么A大于B。
- 如果A与B的评分相同, 如果A字典排序大于B,那么A大于B。
- 在有序集合中A和B不能相等,以保证集合中元素的唯一性。
让我们先看这样一个示例:示例中有序集合记录了一组黑客人员名单,他们的姓名作为排序元素,而出生年份作为元素评分。
> zadd hackers 1940 "Alan Kay"
(integer) 1
> zadd hackers 1957 "Sophie Wilson"
(integer) 1
> zadd hackers 1953 "Richard Stallman"
(integer) 1
> zadd hackers 1949 "Anita Borg"
(integer) 1
> zadd hackers 1965 "Yukihiro Matsumoto"
(integer) 1
> zadd hackers 1914 "Hedy Lamarr"
(integer) 1
> zadd hackers 1916 "Claude Shannon"
(integer) 1
> zadd hackers 1969 "Linus Torvalds"
(integer) 1
> zadd hackers 1912 "Alan Turing"
(integer) 1
可以看到ZADD类似SADD命令,向有序集合添加元素,同时为每个元素附加一个评分。ZADD 也支持一次添加多个元素到集合中,虽然上面的例子没有体现。
对于有序集合,返回按出生年份排序的黑客列表是很简单的,因为实际上他们存储时已经排序了。
需要说明的是,有序集合是通过包含跳表和哈希表的双端口数据结构实现的,因此每次添加元素时,都需执行O(log(N))操作。这有一定开销,但是当需要获取排序元素时,则不数据不再需要排序操作,只需直接返回就好了:
> zrange hackers 0 -1
1) "Alan Turing"
2) "Hedy Lamarr"
3) "Claude Shannon"
4) "Alan Kay"
5) "Anita Borg"
6) "Richard Stallman"
7) "Sophie Wilson"
8) "Yukihiro Matsumoto"
9) "Linus Torvalds"
注意:上面的0和-1下标表示从下标为0的元素开始到最后一个元素全部返回(-1这种表达方式类似LRANGE 命令)。
如果需要获取逆序排列的元素呢?你可以使用ZREVRANGE替换ZRANGE命令:
> zrevrange hackers 0 -1
1) "Linus Torvalds"
2) "Yukihiro Matsumoto"
3) "Sophie Wilson"
4) "Richard Stallman"
5) "Anita Borg"
6) "Alan Kay"
7) "Claude Shannon"
8) "Hedy Lamarr"
9) "Alan Turing"
附加一个WITHSCORES
参数,可以同时返回评分结果:
> zrange hackers 0 -1 withscores
1) "Alan Turing"
2) "1912"
3) "Hedy Lamarr"
4) "1914"
5) "Claude Shannon"
6) "1916"
7) "Alan Kay"
8) "1940"
9) "Anita Borg"
10) "1949"
11) "Richard Stallman"
12) "1953"
13) "Sophie Wilson"
14) "1957"
15) "Yukihiro Matsumoto"
16) "1965"
17) "Linus Torvalds"
18) "1969"
使用区间操作
有序集合的功能不止如此,它还支持区间操作。想要查出1950年之前出生的人员列表,可以使用ZRANGEBYSCORE 命令:
> zrangebyscore hackers -inf 1950
1) "Alan Turing"
2) "Hedy Lamarr"
3) "Claude Shannon"
4) "Alan Kay"
5) "Anita Borg"
上面示例中,我们让Redis返回了评分在负无穷到1950之间的数据 (闭区间)。
也可以移除某个区间内的元素。例如:我们可以从集合中移除出生在1940到1960之间的黑客:
> zremrangebyscore hackers 1940 1960
(integer) 4
ZREMRANGEBYSCORE 命令可能命名比较复杂,但却十分管用,移除的内容会被当做返回结果。
为有序集合元素定义的另一个非常有用的行为是get-rank操作。它可以询问元素在有序集合中的排序下标位置。
> zrank hackers "Anita Borg"
(integer) 4
使用ZREVRANK 命令也可以方便的获取元素的逆序排名(从大到小)。
字典评分
从2.8版本开始, 当有序集合中的元素被赋予相同的评分插入集合时,Redis支持元素按字典排序和并支持相应的区间操作。(元素通过C语言 memcmp
函数比较来消除排序歧义和冲突)。
与字典区间操作有关的命令包括: ZRANGEBYLEX,ZREVRANGEBYLEX,ZREMRANGEBYLEX 和 ZLEXCOUNT。
例如,让我们再次添加之前的著名黑客列表,但这次对所有元素指定评分为0:
> zadd hackers 0 "Alan Kay" 0 "Sophie Wilson" 0 "Richard Stallman" 0
"Anita Borg" 0 "Yukihiro Matsumoto" 0 "Hedy Lamarr" 0 "Claude Shannon"
0 "Linus Torvalds" 0 "Alan Turing"
由于有序集合的排序规则,这些元素现在已经安字典顺序排序:
> zrange hackers 0 -1
1) "Alan Kay"
2) "Alan Turing"
3) "Anita Borg"
4) "Claude Shannon"
5) "Hedy Lamarr"
6) "Linus Torvalds"
7) "Richard Stallman"
8) "Sophie Wilson"
9) "Yukihiro Matsumoto"
使用ZRANGEBYLEX 命令,我们可以获取字典范围的数据集合:
> zrangebylex hackers [B [P
1) "Claude Shannon"
2) "Hedy Lamarr"
3) "Linus Torvalds"
通过括号表示开闭区间,字符串无限大,和负无穷分别用 +
和-
表示. 详情参见文档。
这个特性很重要,因为它允许我们将有序集合当做通用索引使用。例如,如果要通过128位无符号整数对元素进行索引,只需将元素添加到具有相同评分(例如0)但具有16字节前缀(由big endian中的128位数字组成)的有序集合中。由于big-endian中的数字在按字典顺序排列时实际上也是按数字顺序排列的,因此可以在128位空间中进行范围查询,在获取元素值后丢弃前缀。
一个真实的场景演示可以参见这里: Redis自动完成示例。
更新评分:排行榜应用
最后要说明的一点是:有序集合中,元素的分数可以随时更新。只要对已经包含在排序集中的元素调用ZADD,就会用O(log(N))时间更新其得分和排序位置。因为开销很小,有序集合也可以被用在有大量更新操作的场景下。
由于这种特性,有序集合的一个常见的使用场景是得分榜。典型的应用程序可能是一个Facebook游戏中,通过有序集合支持按用户得分排序,获取用户排名等操作,以便支持显示玩家游戏排名,和TOP 10玩家姓名。