必须用ZSET,因其唯一支持按分数排序、快速查Top N及动态更新;LIST需O(N)重排,SET无序;ZSET底层跳表使操作均为O(log N),且ZADD与ZINCRBY需按业务语义严格区分使用。

为什么必须用 ZSET,而不是 LIST 或 SET
ZSET 是 Redis 中唯一能同时满足「按分数排序 + 快速查 Top N + 支持动态更新」这三项硬需求的数据结构。LIST 插入或更新某成员分数时需全量遍历重排,复杂度 O(N);SET 本身无序,根本无法排序。ZSET 底层是跳表(skiplist),ZADD、ZINCRBY、ZREVRANGE 等操作都是 O(log N) 时间复杂度,查前 10 名只需 ZREVRANGE key 0 9 WITHSCORES,不拉全量数据。
常见误操作:用 LPUSH 存 "score:uid123" 字符串,再在 Go 侧 sort.Slice,既浪费网络带宽,又丢失原子性——并发写时排名可能错乱。
ZADD 和 ZINCRBY 到底该选哪个
关键看业务语义是否允许覆盖:
- 如果每次上报的是「当前总分」(如游戏通关最终得分),用
ZADD覆盖旧值,确保榜单反映最新状态 - 如果每次是「本次增量」(如直播打赏 50 积分、答题加 10 分),必须用
ZINCRBY,它对不存在的 member 自动初始化为 0 再累加
Go 客户端(如 github.com/redis/go-redis/v9)中:ZAdd 方法默认不返回变更数量,想判断是否新增成员,得用 ZAddArgs 显式传 NX(仅当 member 不存在时添加)或 XX(仅当已存在时更新);而 ZIncrBy 直接返回新分数,无需额外判断。
立即学习“go语言免费学习笔记(深入)”;
键名、member 和 score 的设计陷阱
键名建议带业务维度前缀,比如 rank:weekly:video、rank:daily:user,避免不同周期/规则的榜单互相污染。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
member 必须全局唯一,推荐用 "uid:12345" 这类 ID,别用昵称或邮箱——前者可能重复,后者含 @、. 等特殊字符,虽 Redis 允许,但易引发序列化/解析歧义。
score 类型是 float64,但业务中多数用整数排名即可。Go 传 int64(100) 会被自动转成 100.0,不影响排序和查询;但如果允许负分,得在业务层校验——ZINCRBY 对负增量不报错,1 - 5 就存成 -4,Redis 不拦。
并发写同一个 member 时会不会丢数据
ZADD 和 ZINCRBY 单条命令都是原子的,但二者混用会出问题。比如先 ZINCRBY key 10 "uid:123" 把分数从 5 加到 15,紧接着又 ZADD key 5 "uid:123",结果分数被覆盖回 5 —— 业务逻辑就断了。
所以得统一写法:同一榜单所有写操作只用一种命令。更稳妥的做法是封装一层,比如定义 Ranking.IncrScore(ctx, uid, delta) 或 Ranking.SetScore(ctx, uid, total),避免调用方随意混用。
真正容易被忽略的点是:ZSET 没有“事务级”多命令原子性。如果你需要「先查当前分,再决定加多少」,就得用 Lua 脚本或 WATCH+MULTI,不能靠客户端两次独立请求保证一致性。

















