直接用GORM查排行榜会卡死,因关系型数据库深分页需扫描大量行且高并发更新引发锁与MVCC开销;应改用Redis ZSET存score排名,GORM仅查用户资料,实现职责分离与性能优化。

为什么直接用 GORM 查排行榜会卡死
因为 GORM 默认走 PostgreSQL/MySQL,而百万级实时排名需要按 score 排序 + 按 rank 分页,数据库在 ORDER BY score DESC LIMIT 10000, 20 这种深分页下必须扫描前一万行,IO 和 CPU 都扛不住。更关键的是:排名是动态的,每秒可能有上千次 score 更新,关系型数据库的行锁和 MVCC 开销会直接拖垮吞吐。
用 Redis ZSET 存排名,GORM 只管用户资料
核心思路是职责分离:ZSET 只存 user_id → score 映射,负责排序、范围查询、排名获取;GORM 只查 User 表补全昵称、头像等字段。不要把用户详情塞进 Redis,否则更新不一致风险高,且浪费内存。
-
ZADD leaderboard:weekly 8724 user:1001—— 每次更新 score 直接覆盖 - 用
ZREVRANGEBYSCORE或ZREVRANGE做分页,例如查第 10001~10020 名:ZREVRANGE leaderboard:weekly 10000 10019 WITHSCORES - 返回的是
[user:1001, 8724, user:2055, 8691, ...],再批量查 GORM:db.Where("id IN ?", []uint{1001,2055,...}).Find(&users)
分页游标比 offset 更可靠
用 offset(比如 ZREVRANGE ... 10000 10019)在高并发写入时会出现漏排或重复——因为上一页最后一名的 score 可能被新 entry 覆盖,导致下一页跳过或重叠。正确做法是游标分页:
- 首次请求:
ZREVRANGE leaderboard:weekly 0 19 WITHSCORES,记录返回的最小 score(比如 8691)和对应 user_id(2055) - 下一页请求:
ZREVRANGEBYSCORE leaderboard:weekly (8691 0 WITHSCORES LIMIT 0 20,注意(8691表示「严格小于 8691」,再加user_id做二级去重(防止同分) - GORM 批量查时,用
IN加ORDER BY FIELD(id, ...)(MySQL)或VALUES子句(PostgreSQL)保持顺序
注意 ZSET 的内存与 TTL 策略
一个百万级 ZSET 在 Redis 中实际占用远超 10MB(每个 member 至少 64 字节+score+内部跳表开销),别忘了设过期:EXPIRE leaderboard:weekly 604800(一周)。另外,避免用字符串 ID(如 "user:1001")当 member——它比整数 ID 多占一倍内存,统一转成 int64 存(ZADD leaderboard:weekly 8724 1001),GORM 查时再拼成 user.ID 即可。
真正难的不是代码怎么写,而是确定哪部分数据必须强一致(比如结算后的最终排名)、哪部分允许几秒延迟(比如实时上升箭头),以及 Redis 故障时降级方案——比如 fallback 到数据库临时排序,但只对非核心页面生效。


















