zset是唯一能同时满足排行榜“按分数排序+快速查top n+动态更新”三项需求的数据结构,因其底层跳表支持o(logn)插入、更新及o(logn+m)范围查询,而set无序、list更新排名开销为o(n)。

为什么用 ZSET 而不是 SET 或 LIST
排行榜本质是「按分数排序 + 快速查 Top N + 支持动态更新」,SET 无序、LIST 插入/更新排名开销大(O(N)),只有 ZSET 天然支持 O(log N) 插入、O(log N) 更新、O(log N + M) 查范围 —— 这是 Redis 唯一能同时满足三项需求的数据结构。
常见误操作:用 LIST 存 score+id 拼接字符串再手动排序,结果每次 LRANGE 后还得在 Go 侧 sort.Slice,既浪费带宽又丢掉原子性;或者把分数当 SET 成员存,完全无法排序。
实操建议:
- 键名设计用业务前缀 + 排行榜维度,比如
rank:weekly:score,避免不同周期/规则的榜单互相污染 - 成员(member)必须唯一,推荐用用户 ID(如
"uid:12345"),别用昵称或邮箱(可能重复或含特殊字符) - 分数(score)用
float64,但实际业务中多数场景只需整数排名,直接传int64会被自动转成float64,不影响正确性
ZADD 和 ZINCRBY 怎么选
核心区别:是否允许非幂等更新。如果每次上报的是「当前总分」(比如游戏最终通关得分),用 ZADD 覆盖;如果是「本次获得积分」(比如直播打赏、答题加分),必须用 ZINCRBY 累加。
容易踩的坑:
-
ZADD默认是CH模式(返回实际变更数量),但 Go 客户端如github.com/go-redis/redis/v9的ZAdd方法默认不带CH,返回值是 error,想判断是否新增成员得用ZAddArgs显式传XX或NX -
ZINCRBY对不存在的 member 会自动创建,分数为 0 + 增量,这点很安全;但若增量是负数且导致分数下溢(比如原 1 分减 2 分),Redis 不报错,只是存成 -1 —— 业务需自行校验是否允许负分 - 并发写同一 member 时,
ZINCRBY是原子的,ZADD也是原子的,但二者混用会导致逻辑错乱(比如先ZINCRBY加 10,再ZADD覆盖成 5)
示例(v9 客户端):
// 累加模式(打赏)
rdb.ZIncrBy(ctx, "rank:daily:score", 50, "uid:789")
// 覆盖模式(最终成绩)
rdb.ZAdd(ctx, "rank:final:score", &redis.Z{Score: 98.5, Member: "uid:789"})
查 Top N 时 ZREVRANGE 和 ZRANGE 别搞反
「排行榜」默认指「从高到低」,对应 Redis 命令是 ZREVRANGE(reverse range)。用 ZRANGE 会拿到最低分那批人 —— 这个错误在线上很难被立刻发现,因为数据存在、格式也对,只是逻辑全反了。
实操细节:
-
ZREVRANGE key 0 9 WITHSCORES查前 10 名,注意索引从 0 开始,第 11 名是10 - 如果要带分数,Go 客户端返回的是
[]redis.Z,每个元素含Member(interface{})和Score(float64),需类型断言;Member通常是string,但若存的是 JSON 字符串,得额外json.Unmarshal - 分页查中间段(比如第 21–30 名)用
ZREVRANGE key 20 29,但要注意:如果榜单总人数不足 30,结果会自动截断,不会报错 - 想查某用户具体排名,用
ZREVRANK(注意是 reverse rank!),返回的是 0-based 索引;若用户不在榜上,返回nil
过期、清榜与内存控制的实际处理
Redis 不会自动清理过期的 ZSET 成员,只会在整个 key 过期时删掉整个结构。所以「周榜」不能靠 EXPIRE 键来实现自动归零 —— 你得主动清空或重建。
推荐做法:
- 用时间戳做键后缀,比如
rank:week:20240520(周一),每周一凌晨用DEL清旧键 + 新建空ZSET,避免ZREM全量删除的阻塞风险 - 如果必须复用同一个键(如实时榜),用
ZREMRANGEBYSCORE清理过期数据(比如只保留最近 30 天的记录),但前提是你的 score 能表达时间维度(例如用 Unix 时间戳作 score) - 大榜(百万级成员)慎用
ZCARD查总数 —— 虽然是 O(1),但某些 Redis 版本在ZSET极大时有轻微延迟;更稳的方式是单独用一个INCR计数器维护活跃人数 - 内存方面,
ZSET比LIST占更多空间(因要维护跳表),但只要 member 不冗长(别存完整用户信息)、score 用整数,单榜千万级成员在 1–2 GB 内可控
真正容易被忽略的点:没有监控 ZCARD 异常增长。某个活动期间 member 写入逻辑出 bug(比如把 session_id 当 uid 写进去),几天就撑爆 Redis 内存 —— 建议对关键排行榜键加 INFO keyspace 定期采样或用 redis_exporter 暴露指标。











