Redis 的有序集和(ordered set)同时具有"有序"和"集和"两种性质, 这种结构中每个元素都由一个成员和一个与成员相关联的分值组成, 其中成员与字符串方式存储, 而分值以64位双精度浮点数格式存储.
例如下面一个记录薪水的集和:
成员 | 分值 |
---|---|
“perter” | 3500 |
“bob” | 3800 |
“jack” | 4500 |
“tom” | 5000 |
“mary” | 5500 |
与集和一样, 有序集和中的元素都是唯一的, 同时, 成员将按照分值大小进行排序.
有序集和分值除了可以是数字外, 还可以是字符串 “+inf” 或者 “-inf”, 这两个特殊值分别表示无穷大和无穷小.
虽然有序集和的成员不可相同, 但是分值可以是相同的, 当两个或多个成员拥有相同的分值时,Redis 将按照这些成员在字典序中的大小对其进行排列.
有序集合是Redis提供的所有数据结构中最为灵活的一种, 它可以以多种不同的方式获取数据, 比如根据成员获取分值、根据分值获取成员、根据成员的排名获取成员、根据指定的分值范围获取多个成员等.
ZADD: 添加或更新成员
PLAINTEXTZADD sorted_set socre number [score number ...]
默认情况下, ZADD 命令将返回成功添加的新成员数量作为返回值, 对于更新操作会返回0(未添加新成员).
使用XX | NX
选项来显示地指示命令 只更新 或 只添加操作PLAINTEXTZADD sorted [XX|NX] socre member [socre member ...]
若要返回所有被修改的成员数量(新添加 + 更新数量), 可使用 CH 选项
PLAINTEXTZADD sorted_set [CH] socre number [score number ...]
复杂度: O(M * log(N)) 其中 M 为给定成员数量, N 为有序集和的成员数量
ZREM: 移除指定的成员
PLAINTEXTZREM sorted_set member [member ...]
ZREM 会返回被移除成员的数量作为返回值, 如果给定的某个成员不存在, 则会自动忽略该成员.
复杂度: O(M * log(N)), 其中 M 为给定成员的数量, N 为有序集和中包含的成员数量.ZSCORE: 获取成员的分值
PLAINTEXTZSCORE sorted_set member
如果给定的 member 不存在, 将返回空值
复杂度: O(1)ZINCRBY: 对成员的分值进行自增或自减操作
PLAINTEXTZINCRBY sorted_set increment member
该命令完成操作后, 将返回成员当前分值, increment 为正数时就是自增操作, 为负数时就是自减操作.
如果命令给定的有序集和或者成员不存在, 则相当于 ZADD 命令, 会自动创建对应的值.
复杂度: O(log(N))ZCARD: 获取有序集和的大小
PLAINTEXTZCARD sorted_set
返回集和中成员数量, 若不存在则返回0.
复杂度: O(1)ZRANK、ZREVRANK: 取得给定成员在有序集和中的排名
PLAINTEXTZRANK sorted_set member # 升序 ZREVRANK sorted_set member # 降序
若给定的集和或者成员不存在, 则返回空值.
复杂度: O(log(N))ZRANGE、ZREVRANGE: 获取索引范围内的成员
PLAINTEXTZRANGE sorted_set start end # 升序 ZREVRANGE sorted_set start end # 降序
索引范围为 [start, end], 即这两个索引上的成员也会包含在命令返回的结果当中, 索引从0开始. 此外, 还可以使用负数索引, 索引最后一个从 -1 开始.
默认情况下, ZRANGE 和 ZREVRANGE 命令只会返回指定索引范围内的成员, 如果用户想要获取这些成员以及其分值, 可以使用 WITHSCORES 选项PLAINTEXTZRANGE sorted_set start end [WITHSCORES] ZREVRANGE sorted_set start end [WITHSCORES]
如果给定的有序集和不存在, 则会返回一个空列表.
复杂度: O(M + log(N)), 其中 M 为命令返回成员数量, N 为有序集和成员数量.
示例: 排行榜
在网站上经常会有各种各样的排行榜. 比如, 音乐网站上可能有试听排行榜, 下载排行榜等. 而视屏网站上可能看到观看排行榜, 收藏排行榜等. 甚至 GitHub 项目托管网站也有 star 排行榜.
下面代码实现了一个排行榜程序:
- 这个程序使用 ZADD 命令向排行榜中添加被排序的元素及分数, 并使用 ZREVRANK 命令获取元素在排行榜中的排名, 以及使用 ZSCORE 命令去获取元素的分数
- 当不再需要对某个元素进行排序的时候, 可以调用 ZREM 命令实现
remove()
方法, 从排行榜中移除该元素 - 如果用户想要修改某个被排序的元素的分数, 则调用 ZINCRBY 命令实现的
increase_score()
方法或者decrease_score()
方法即可 - 当用户想要获取排行榜前N位的元素及其分数时, 只需要调用由 ZREVRANGE 命令实现的
top()
方法即可
class RankingList:
def __init__(self, client, key):
self.client = client
self.key = key
def set_score(self, item, score):
"""为排行榜中指定元素设置分数, 不存在的元素会被添加到排行榜中"""
self.client.zadd(self.key, {item: score})
def get_score(self, item):
"""获取排行榜中指定元素的分数"""
return self.client.zscore(self.key, item)
def remove(self, item):
"""从排行榜中删除指定的元素"""
self.client.zrem(self.key, item)
def increase_score(self, item, increment):
"""将给定的元素分数增加 increment 分"""
self.client.zincrby(self.key, increment, item)
def decrease_score(self, item, decrement):
"""将给定的元素减少 increment 分"""
self.client.zincrby(self.key, 0-decrement, item)
def get_rank(self, item):
"""获取给定元素排行榜中的排名"""
rank = self.client.zrevrank(self.key, item)
if rank is not None:
return rank + 1 # Redis 排名从0开始
def top(self, n, with_score=False):
"""获取排行榜中得分最高的元素"""
return self.client.zrevrange(self.key, 0, n-1, withsores=with_score)
ZRANGEBYSCORE、ZREVRANGEBYSCORE: 获取分值范围内的成员
PLAINTEXTZRANGEBYSCORE sorted_set min max ZREVRANGEBYSCORE sorted_set max min
命令的 min 参数和 max 参数分别用于指定用户想要获取的最小分值和最大分值, 要注意这两条命令接受最大最小值的顺序正好相反.
- 该命令也可以使用 WITHSCORES 参数来同时获取成员及其分值
PLAINTEXTZRANGEBYSCORE sorted_set min max [WITHSCORES] ZREVRANGEBYSCORE sorted_set max min [WITHSCORES]
- 默认情况下, 该命令会返回范围内的所有成员, 有时成员数量很多, 就可以使用 LIMIT 选项来限制命令返回的成员数量.
PLAINTEXTZRANGEBYSCORE sorted_set min max [LIMIT offset count] ZREVRANGEBYSCORE sorted_set min max [LIMIT offset count]
其中, offset 是指从满足 min 和 max 的元素中, 跳过前面的 offset 个元素, count 是从 offset 之后开始, 返回接下来的 count 个元素.
- 如果要使用开区间而不是闭区间, 在分词的前面加上一个分括号
PLAINTEXTZRANGEBYSCORE salary (3500 (5000 WITHSCORES
这样会返回分值大于3500, 小于5000的成员.
- 最后, 还可以使用无限值作为分值范围
PLAINTEXTZRANGEBYSCORE salary -inf (5000 WITHSCORES
复杂度: O(log(N) + M), N 为有序集和的成员数量, M 为命令返回的成员数量
ZCOUNT: 统计指定分词范围内的成员数量
PLAINTEXTZCOUNT sorted_set min max
复杂度: O(N)
示例: 时间线
在互联网上, 很多网站都会根据发布的内容来对时间进行排序:
- 博客系统按照文章发布时间的先后, 将最近发布的文章放在前面
- 新闻网站会按照新闻的发布时间, 把最新的新闻放在网站前面
类似的情形还有很多, 通过对这类行为进行抽象, 写出下面的时间线程序:
- 把被添加到时间线里的元素用作成员, 与元素相关的时间戳用作分值, 将元素和时间戳添加到集和中
- 将时间线中的元素按照时间戳的大小排序
- 通过对时间线中的元素执行 ZREVRANGE 或者 ZREVRANGEBYSCORE 命令, 用户可以通过分页的方式取出时间线中的元素, 或者从时间线中取出指定时间区间内的元素
class Timeline:
def __init__(self, client, key):
self.client = client
self.key = key
def add(self, item, time):
"""将元素添加到时间线中"""
self.client.zadd(self.key, {item: time})
def remove(self, item):
"""将元素从时间线中移除"""
self.client.zrem(self.key, item)
def count(self):
"""返回时间线包含的元素数量"""
return self.client.zcard(self.key)
def pagging(self, number, count, with_time=False):
"""
按照每页 count 个元素, 取出时间线第 number 页上的所有元素, 将元素按照时间戳逆序排序
with_time 表示是否返回时间信息
"""
start_index = (number - 1) * count
end_index = number * count - 1
return self.client.zrevrange(self.key, start_index, end_index, with_time)
def fetch_by_time_range(self, min_time, max_time, number, count, with_time=False):
"""
按照每页 count 个元素, 取出时间线第 number 页上的所有元素, 按照时间戳逆序排序
with_time 表示是否返回时间信息
"""
start_index = (number - 1) * count
end_index = start_index + count - 1
return self.client.zrevrangebyscore(self.key, max_time, min_time, start_index, end_index, withscores=with_time)
ZREMRANGEBYRANK: 移除指定排名范围内的成员
PLAINTEXTZREMRANGEBYRANK sorted_set start end
该命令可以从升序排列的有序集和中移除位于指定排名范围内的成员, 然后返回被移除成员的数量
start->end 可以是正数, 也可以使用负数 end->startZUNIONSTORE、ZINTERSTORE: 有序集和的并集运算和交集运算
PLAINTEXTZUNIONSTORE destination numbers sorted_set [sorted_set ...] ZINTERSOTRE destination numbers sorted_set [sorted_set ...]
其中, numbers 用于指定参与计算的有序集和数量, 计算结果会存储到 destination 参数指定的键中, 最后返回计算结果包含的成员数量作为返回值
- 此外, 还可以决定使用什么方法来获得集和成员的分值:
PLAINTEXTZUNIONSTORE destination numbers sorted_set [sorted_set ...] [AGGREGATE SUM|MIN|MAX] ZINTERSTORE destination numbers sorted_set [sorted_set ...] [AGGREGATE SUM|MIN|MAX]
- 如果上面的聚合函数不够用, 还可以为每个集和设置权重
PLAINTEXTZUNIONSTORE destination numbers sorted_set [sorted ...] [WEIGHTS weight [weight ...]]
复杂度: ZUNIONSTORE - O(N * log(N)), ZINTERSTORE - O(N * log(N) * M)
还有很多其他关于有序集和的内容, 这里就不再讨论了.