Redis有序集合:ZADD/ZREM、ZRANGE/ZREVRANGE、ZSCORE、ZRANK、ZINCRBY
有序集合(Sorted Set)是 Redis 里我最喜欢的数据结构之一。为什么?因为它把「集合的去重特性」和「排序能力」完美结合在了一起。说白了,每个元素都带一个分数(score),Redis 自动按分数从小到大排好序。你想想看,排行榜、延迟队列、优先级任务……这些场景用有序集合来实现,简直不要太顺手。
我记得刚接触 Redis 时,总觉得有序集合和列表差不多。后来踩了坑才明白——列表只能按插入顺序访问,而有序集合是按分数排序的,而且支持范围查询。这个区别,在实战中天差地别。
ZADD 与 ZREM:增删元素
ZADD 用来向有序集合添加元素,每个元素必须带一个分数。如果元素已存在,就更新它的分数。
# 添加一个元素,分数是 100
ZADD leaderboard 100 "user:1001"
# 添加多个元素
ZADD leaderboard 200 "user:1002" 150 "user:1003" 180 "user:1004"
# 如果 "user:1001" 已存在,更新分数为 120
ZADD leaderboard 120 "user:1001"
我在项目中遇到过一个问题:用 ZADD 批量插入大量数据时,如果某个元素已经存在,它不会报错,而是直接更新分数。这个行为有时候是好事,有时候却是坑。比如你希望只插入新用户,结果不小心把老用户的分数覆盖了。
ZREM 就简单多了,删除指定元素:
# 删除一个元素
ZREM leaderboard "user:1001"
# 删除多个元素
ZREM leaderboard "user:1002" "user:1003"
返回值是实际删除的元素个数。如果元素不存在,返回 0,不会报错。嗯,Redis 在这方面一直很温和。
ZRANGE 与 ZREVRANGE:范围查询
这是有序集合最常用的操作。按分数排序后,取出指定范围内的元素。
# 取出排名前 10 的用户(分数从低到高)
ZRANGE leaderboard 0 9
# 取出排名前 10 的用户,同时显示分数
ZRANGE leaderboard 0 9 WITHSCORES
# 取出排名后 10 的用户(分数从高到低)
ZREVRANGE leaderboard 0 9
# 同样可以带分数
ZREVRANGE leaderboard 0 9 WITHSCORES
你想想看,排行榜功能是不是一行命令就搞定了?ZREVRANGE 从高到低取,正好是「第一名到第十名」的顺序。
我曾经帮一个游戏团队优化排行榜查询。他们原来用 MySQL 的 ORDER BY score DESC LIMIT 10,每次查询要扫描几万行。换成 Redis 有序集合后,查询时间从 200 毫秒降到了 1 毫秒以内。这就是内存数据结构的威力。
ZRANGE key 0 -1。负索引表示从末尾开始数,-1 就是最后一个元素。
ZSCORE:获取元素分数
这个命令简单到不能再简单——给定元素,返回它的分数。
ZSCORE leaderboard "user:1002"
# 返回 "200"
如果元素不存在,返回 nil。我在做积分查询接口时经常用它,配合 ZRANK 一起用,能快速拿到用户的排名和分数。
ZRANK 与 ZREVRANK:排名查询
ZRANK 返回元素按分数从小到大排的排名(从 0 开始)。ZREVRANK 则是从大到小排。
# 分数最低的排名是 0
ZRANK leaderboard "user:1002"
# 假设 user:1002 分数最高,返回 3(如果集合有 4 个元素)
# 分数最高的排名是 0
ZREVRANK leaderboard "user:1002"
# 返回 0
这里有个容易搞混的地方:ZRANK 返回的是「升序排名」,分数最低的排第 0 名。而大多数业务场景需要「降序排名」,所以要用 ZREVRANK。我刚开始用的时候,就因为这个搞反了,查出来的排名全是反的。后来我养成了一个习惯:做排行榜一律用 ZREVRANK,做延迟队列用 ZRANK。
ZINCRBY:原子增减分数
这个命令太实用了。它给指定元素的分数增加一个增量,如果元素不存在,就创建它。
# 给 user:1002 加 10 分
ZINCRBY leaderboard 10 "user:1002"
# 给 user:1003 减 5 分(增量可以是负数)
ZINCRBY leaderboard -5 "user:1003"
关键是——它是原子操作。多个客户端同时调用 ZINCRBY,不会出现并发问题。我在做实时积分系统时,就用它来处理用户点赞、打赏等操作。不用加锁,不用事务,一行命令搞定。
知识体系总览
下面这张图把有序集合的核心操作串起来了。你看一遍,应该能对整体有个清晰的认识。
实战中的避坑指南
说了这么多,我总结几个实战中容易踩的坑:
- 分数精度问题:Redis 的分数是双精度浮点数。如果你需要精确到小数点后很多位,建议把分数乘以一个系数转成整数存。我曾经因为分数精度问题,导致排行榜上两个用户分数「相等」,排序结果不稳定。
- 内存占用:有序集合底层用了跳跃表(skiplist)和哈希表,每个元素的内存开销比普通集合大。如果元素数量上亿,要考虑内存是否够用。
- ZRANGE 的性能:取小范围数据很快,但如果取全部元素(比如
ZRANGE key 0 -1),数据量大时会阻塞 Redis。生产环境要小心。 - 分数相同的情况:分数相同时,按元素字典序排序。如果你需要自定义排序规则,可以在分数上做文章,比如
分数 + 时间戳的倒数。
主分数 * 10^6 + 时间戳。这样既能按主分数排序,又能保证先到者排在前面。比如游戏积分相同,先达到的玩家排名更高。
好了,有序集合的核心操作就这些。说白了就是增删改查加排名,但每个命令在实战中都有它的用武之地。你多用几次,自然就熟练了。