Skip to content

排行榜功能怎么实现

设计

  • Redis 有序集合 (Sorted Set):Redis 是一个基于内存的数据结构存储系统,读写性能极高。其 Sorted Set 数据结构(也称为 ZSET)专门为排序而生,它在集合(Set)的基础上为每个成员(member)关联了一个分数(score)。这使得 Redis 能够以极高的效率(时间复杂度为 O(log N))进行成员的插入、更新、删除以及排名查询等操作。因此,Redis 非常适合处理实时性要求高的排行榜数据查询。

  • 数据库 (如 MySQL, PostgreSQL):关系型数据库(或部分 NoSQL 数据库)则负责数据的持久化存储和复杂查询。所有用户的得分记录、历史排行数据等都会在数据库中存档。这确保了数据的安全性和完整性,并且可以支持离线分析、数据备份等需求。

通过这种架构,我们可以将实时的读写请求交给 Redis 处理,保证排行榜的快速响应;同时,通过将数据异步写入数据库,确保数据的持久性和一致性。

典型的排行榜系统架构和数据流如下:

  1. 分数更新

    • 当用户的分数发生变化时,应用程序首先将新的分数数据写入 Redis 的 Sorted Set。这是一个非常快的操作,可以立即反馈给用户最新的排名。
    • 同时,应用程序将该次分数更新的记录(例如用户ID、分数、时间戳等)发送到一个消息队列中(如 RabbitMQ, Kafka)。
  2. 数据持久化

    • 一个独立的后台服务会消费消息队列中的数据,并将分数更新的详细记录批量写入数据库中。这种异步化的“写后” (Write-Behind) 模式,可以有效降低对主应用流程的影响,并减轻数据库的瞬时写入压力。
  3. 排行榜查询

    • 当用户需要查看排行榜时,应用程序直接从 Redis 的 Sorted Set 中查询数据。 Redis 提供了丰富的命令来满足各种查询需求,例如获取排名前 N 的用户、查询特定用户的排名和分数、获取某个用户周边的排名等。
  4. 数据恢复与一致性

    • 数据库作为最终的数据源。如果 Redis 发生故障或数据丢失,可以通过数据库中的数据重建整个或部分的排行榜。
    • 可以定期(例如每天凌晨)进行数据校准,以确保 Redis 中的数据与数据库中的核心数据保持最终一致。

实现

1. Redis Sorted Set 的设计与使用

Redis 的 Sorted Set 是实现实时排行榜的核心。 我们通常会为不同的排行榜设置不同的 key。

命名规范:为了支持多种类型的排行榜(如日榜、周榜、总榜),key 的命名至关重要。例如: * leaderboard:daily:2025-09-03 * leaderboard:weekly:2025-W36 * leaderboard:global

常用 Redis 命令

  • 更新用户分数 ZADD:

    • ZADD leaderboard:global 1000 "user:123"
    • 此命令会将 "user:123" 的分数更新为 1000。如果该用户已存在,则更新其分数;如果不存在,则添加该用户。
  • 增加用户分数 ZINCRBY:

    • ZINCRBY leaderboard:global 50 "user:123"
    • 此命令会给 "user:123" 的分数增加 50。
  • 获取排名前 N 的用户 ZREVRANGE:

    • ZREVRANGE leaderboard:global 0 9 WITHSCORES
    • 此命令会返回分数最高的前 10 名用户及其分数(从高到低排序)。
  • 获取用户的排名 ZREVRANK:

    • ZREVRANK leaderboard:global "user:123"
    • 此命令返回指定用户在排行榜中的排名(从 0 开始,分数越高排名越靠前)。
  • 获取用户的分数 ZSCORE:

    • ZSCORE leaderboard:global "user:123"
    • 此命令返回指定用户的分数。

2. 数据库的 Schema 设计

数据库的设计需要考虑到数据的持久化和未来的扩展性。可以设计如下几张表:

  • users: 存储用户信息。 sql CREATE TABLE users ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE, -- 其他用户信息 );

  • scores: 存储每一次的分数变更记录。 sql CREATE TABLE scores ( score_id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id INT, score_change INT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 其他相关信息,如来源、类型等 FOREIGN KEY (user_id) REFERENCES users(user_id) );

  • leaderboard_summary: 存储用户的总分和最后更新时间,用于数据恢复和一致性校验。 sql CREATE TABLE leaderboard_summary ( user_id INT PRIMARY KEY, total_score BIGINT, last_updated_at TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(user_id) );

3. 数据同步策略

确保 Redis 和数据库之间的数据同步是该架构的关键。以下是几种常见的同步策略:

  • 双写模式 (Dual Write)

    • 流程:应用程序在更新分数时,先写 Redis,成功后再写数据库。
    • 优点:实现简单,实时性高。
    • 缺点:存在数据不一致的风险。例如,写 Redis 成功,但写数据库失败,会导致 Redis 中的是新数据,而数据库中是旧数据。这会给系统带来额外的复杂性,需要引入重试或补偿机制。
  • 写后模式 (Write-Behind Caching)

    • 流程:应用程序只管写入 Redis,然后通过消息队列异步地将数据写入数据库。
    • 优点:写入性能高,主流程与数据库解耦,可靠性强。即使数据库暂时不可用,只要消息队列正常,数据就不会丢失。
    • 缺点:数据在 Redis 和数据库之间存在短暂的不一致。对于大多数排行榜场景,这种最终一致性是可以接受的。

推荐采用“写后模式”,因为它在性能、可靠性和系统解耦方面都表现更优。

4. 处理特殊场景

  • 同分处理(Tie-Breaking)

    • Redis 的 Sorted Set 在分数相同时,会按照成员的字典序进行排序。
    • 如果需要自定义同分规则(例如,先达到该分数者排名靠前),可以在分数上做一些“手脚”。一个常见的做法是将时间戳作为一个小数部分附加到分数上。例如,分数为 score,时间戳为 timestamp,那么存入 Redis 的最终分数可以为 score.timestamp。由于时间戳是递增的,所以可以实现先到先得的排序规则。
  • 数据恢复

    • 如果 Redis 实例因故宕机且数据无法恢复,可以编写一个脚本,从数据库的 leaderboard_summary 表中读取所有用户的总分,然后使用 ZADD 命令批量重建 Redis 中的 Sorted Set。
  • 获取用户周边的排名

    • 这是一个常见的需求,例如显示用户前后各 5 名的玩家。可以通过先用 ZREVRANK 获取用户的排名 rank,然后计算出起始和结束位置(如 rank-5rank+5),再使用 ZREVRANGE 来获取这个区间的用户列表。