# rank **Repository Path**: mutou6349/rank ## Basic Information - **Project Name**: rank - **Description**: 排行榜样例 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-04-28 - **Last Updated**: 2025-04-30 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ### 接口定义 ```go type RankInfo struct { UserId string `json:"userId"` Rank int `json:"rank"` Score int `json:"score"` } // 更新分数, 无返回内容 func (c *LeaderboardCtrl) UpdateScore(leaderboardKey, userKey string, score int) error // 查询当前玩家排名信息 func (c *LeaderboardCtrl) QueryRank(leaderboardKey, userKey string) (*RankInfo, error) // 查询当前玩家附近共 `rangeNum` 名玩家的排名信息 func (c *LeaderboardCtrl) QueryRankRange(leaderboardKey, userKey string, rangeNum int) ([]*RankInfo, error) // 查询排行榜头部 `count` 名玩家的排名信息 func (c *LeaderboardCtrl) QueryTopN(leaderboardKey string, count int) ([]*RankInfo, error) ``` ### 系统设计 1. 排行榜数据存在redis中, 通过sorted set结构实现排序. 百万用户预计内存开销在50MB以内. 2. 对于分数相同的玩家, 在sorted set的score中加入时间相关的小数部分, 需要根据排序规则调整(正序/倒序). 3. 服务器接收请求操作redis中的排行榜数据, 本身不维护排行榜状态, 方便横向扩展以应对更大流量. 4. 在仅使用单点redis服务器的情况下可以确保数据的一致性和完整性, 最终得分以redis存储的数据为准. Redis对于请求操作在单线程上顺序执行, 加分/查询较为简单没有并发问题, 对于热点数据更新选择通过SetNX实现抢锁逻辑. 5. 对于top n热点数据, 额外在redis中维护一个缓存, 未命中时加锁更新缓存, 损失实时性换取响应速度. > 这里如果可以确定n的数值, 比如固定请求top 100, 可以起单独goroutine定时更新, 此时热点key可以设置为不过期, 效果更好. > > 如果是单榜百万用户, 多数玩家不关注附近分数, 因此常有关键排名的分数榜, 通过这个方式实现更新也更方便. 6. 如果用户被分配到多个排行榜, 考虑增加redis实例数量, 根据分组规则查询特定节点, 分摊请求压力. 7. 如果查询量远大于更新量, 考虑在redis上做读写分离. 主从同步延迟会导致短暂的数据不一致, 一般来说对排行榜业务影响不大. ### 需求变更: 密集排行榜 1. 记录两个zset, 一个维护分数到排名的映射, 一个还是原本的玩家分数榜(以保持获取TopN及附近玩家的逻辑). 此外维护一个hset来确定分数对应玩家数量, 以决定是否操作排名zset. 2. 更新分数逻辑: 在数量hset中增减对应分数玩家数量, 如果变成0/1则在排名zset中删除/添加分数, 然后正常更新玩家分数榜. 3. 实际排名需要用玩家分数查询排名zset, TopN和附近玩家排名先通过玩家分数榜获取, 再根据分数查询出结果. 4. 更新与查询过程中涉及多个步骤, 易导致并发问题, 可以将对应操作放在Redis的Lua脚本中执行, 赋予原子性. 5. 对于查询附近玩家分数, 不需要频繁根据分数查询名次, 只需要知道目标玩家名次, 即可根据分数推算出附近玩家名次. 对于TopN的更新逻辑也做类似处理即可.