zset 适合排行榜因支持按分排序与动态更新,操作复杂度为 o(log n);string 和 hash 需全量读写或应用层排序,性能差。

为什么用 ZSet 而不是普通 String 或 Hash
排行榜本质是「按分数排序 + 支持动态更新」,ZSet 天然满足:每个成员(member)关联一个浮点型分数(score),自动按 score 降序/升序组织,插入、更新、查 Top N 都是 O(log N)。用 String 存 JSON 数组要全量读写、手动排序;Hash 没有内置排序能力,每次取 Top N 都得 HGETALL + 应用层排序,数据一多就卡。别图省事绕开 ZSet,否则后期改成本更高。
zrevrange 的实际调用方式和边界陷阱
Spring Data Redis 封装了 zrevrange,但直接调 redisTemplate.opsForZSet().reverseRange(...) 只能拿 member,不带 score;要 score 得用 reverseRangeWithScores(...)。常见错误是传错索引:比如想取前 10 名,却写成 0, 9(正确),但误写成 0, 10(会多拿 1 个,且当总分不足 11 时不会报错,容易漏掉边界逻辑)。另外,ZSet 索引从 0 开始,负数索引如 -1 表示最后一个元素——这点在分页查「后 10 名」时很有用,但新手常忽略。
- 取 Top 10:用
reverseRangeWithScores("rank:game", 0, 9) - 查某用户排名(从 1 开始计数):用
reverseRank("rank:game", "user_123"),返回值是 Long 类型,注意判 null - 用户没上榜时
reverseRank返回null,不是 -1,别直接做算术运算
分数设计:用时间戳还是业务分?怎么避免并列问题
如果纯用业务分(如游戏积分),相同分数的成员会按字典序排——这会导致「同分用户排名不稳定」,尤其并发更新时。稳妥做法是把时间戳作为低 10 位补丁:例如 score = (long)(bizScore * 100000) + (System.currentTimeMillis() % 100000),保证严格单调。但注意:Redis ZSet score 是 double,超过 2^53 会丢失精度,所以不要把时间戳整个塞进去。另外,如果业务允许并列(如“并列第 3 名”),那就别加时间戳,改用 zrevrank + 前向扫描找相同 score 的第一个位置来算名次。
性能和过期:排行榜要不要设 TTL?怎么批量清理冷数据
ZSet 本身不支持为单个 member 设过期,只能给整个 key 设 TTL。如果排行榜按天/周分区(如 rank:game:20240520),可以设 7 天 TTL,干净利落。但如果是长期榜(如“历史总榜”),就不能依赖 TTL,得主动清理:用 zremrangebyscore("rank:game", 0, 100) 删除低分段,或用 zremrangebyrank("rank:game", 0, -1001) 删掉垫底的 1000 名。注意 zremrangebyrank 的负索引是从末尾算,-1001 表示倒数第 1001 个往后的全部删掉——这个下标容易算反,建议先 zcard 查总数再算安全范围。
真正难的不是命令怎么写,而是分数生成策略和排名语义是否贴合业务。比如“实时榜”要防刷分,“历史榜”要考虑归档,这些都得在 ZSet 之上加一层控制,而不是指望 Redis 自己解决。











