zset适用于积分排行榜因其成员唯一、自动按score排序、支持范围查询等特性,而list需手动排序、hash不支持排序,均无法高效满足实时更新与排名查询需求。

为什么用 ZSET 而不是 LIST 或 HASH
积分排行榜本质是「按分数排序 + 实时更新 + 支持范围查询」,ZSET 天然满足:成员唯一、自动按 score 排序、支持 ZRANGE/ZREVRANGE 拿 Top N、ZSCORE 查单人排名、ZINCRBY 原子增减积分。用 LIST 得自己排序(O(n log n)),HASH 不支持排序,每次查榜都要全量拉取再 sort——高并发下直接拖垮 Redis 和 Go 服务。
实操建议:
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
-
ZSETkey 命名建议带业务前缀和时间维度,比如rank:202410:score,避免大 key 累积 - 用户 ID 作为 member,积分作为 score;不要把用户信息 JSON 序列化塞进 member——member 是字符串,过长影响比较性能,且无法复用已有用户数据
- score 必须是 float64 兼容格式,整数积分写成
100.0比100更稳妥,避免某些客户端解析歧义
ZINCRBY 更新积分时如何防止超卖或负分
直接 ZINCRBY rank:202410:score 10 user_123 看似简单,但业务常要求:积分不能为负、单日获取上限 500、新用户首次加入要初始化为 0。这些逻辑不能靠应用层 if-else + 多次 Redis 请求实现——竞态条件必然出现。
实操建议:
- 用 Lua 脚本封装原子逻辑,例如检查当前分 + 增量后是否 ≥ 0,且当日增量未超限(需配合另一个
HASH记录用户日增记录) - Gin 中调用示例:
script := redis.NewScript(` if tonumber(redis.call("ZSCORE", KEYS[1], ARGV[2])) == nil then redis.call("ZADD", KEYS[1], 0, ARGV[2]) end local cur = tonumber(redis.call("ZSCORE", KEYS[1], ARGV[2])) local new = cur + tonumber(ARGV[1]) if new cur + 500 then return -2 end redis.call("ZINCRBY", KEYS[1], ARGV[1], ARGV[2]) return new `) result, err := script.Run(ctx, rdb, []string{"rank:202410:score"}, "10", "user_123").Result() - 注意
result是interface{},需类型断言;-1和-2是自定义错误码,Gin 中转成 HTTP 状态码更清晰
Gin 接口返回 Top 100 时怎么避免阻塞主线程
ZREVRANGE rank:202410:score 0 99 WITHSCORES 本身很快,但若在 Gin handler 里同步执行,且 Redis 响应稍慢(比如网络抖动、慢日志),整个 goroutine 就卡住,连接池耗尽。别用 rdb.ZRevRangeWithScores(ctx, key, 0, 99).Val() 就完事。
实操建议:
- 加 context 超时:
ctx, cancel := context.WithTimeout(c.Request.Context(), 300*time.Millisecond),超时后直接返回缓存旧数据或降级空列表 - 对非核心字段(如用户头像、昵称)不要在 Redis 查,而是用异步 goroutine 并行查 MySQL 或本地 map,主流程只拼
user_id和score - 如果排行榜更新不频繁(比如每分钟聚合一次),直接用 Redis 的
EXPIRE+ 后台定时任务预热,接口只读GET rank:202410:score:cache(JSON 字符串),快十倍以上
用户查自己排名时 ZREVRANK 为什么总返回 nil
常见错误是:用户刚注册还没加进 ZSET,或者用错了命令——ZREVRANK 返回的是「降序下的索引(从 0 开始)」,但很多人误以为它能返回「第几名」。更坑的是,如果 member 不存在,Redis 返回 nil,Go 的 redis.IntCmd.Val() 会 panic。
实操建议:
- 查自己排名必须先
ZSCORE确认存在,再ZREVRANK;或者直接用ZCOUNT rank:202410:score (score +inf统计比自己分高的有多少人,+1 就是名次(注意处理并列) - 代码里必须检查 error:
rank, err := rdb.ZRevRank(ctx, "rank:202410:score", "user_123").Result() if errors.Is(err, redis.Nil) { // 用户不在榜上 c.JSON(200, gin.H{"rank": 0, "message": "not in rank"}) return } if err != nil { // 真实 Redis 错误 c.JSON(500, gin.H{"error": "redis failed"}) return } c.JSON(200, gin.H{"rank": rank + 1}) // ZREVRANK 从 0 开始,名次要 +1 - 别依赖
ZREVRANK的返回值做业务判断,比如「排名 ZREVRANK 只返回第一个位置,实际名次区间得靠ZRANGEBYSCORE算
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










