🏆 设计一个实时排行榜系统:竞技场的荣耀!

110 阅读13分钟

📖 开场:游戏排行榜

想象你在玩王者荣耀 🎮:

简单排行榜

每天零点统计昨天的数据:
1. 张三:100胜
2. 李四:95胜
3. 王五:90胜

实现:SQL ORDER BY + LIMIT ✅

实时排行榜

每赢一局,立即更新排名:
张三赢了 → 排名实时变化 ⚡
    ↓
10:00:00  第3名
10:00:30  第2名(刚赢了一局)
10:01:00  第1名(又赢了一局)

要求:
- 实时更新(毫秒级)
- 高并发(千万用户同时玩)
- 准确排名(不能错)

这就是实时排行榜的挑战!


🤔 核心需求

业务场景

场景说明难度
游戏排行榜积分实时更新⭐⭐⭐
直播打赏榜礼物金额累加⭐⭐⭐
电商销量榜商品销量统计⭐⭐
阅读排行榜文章阅读量⭐⭐

核心要求

要求说明重要性
实时性毫秒级更新⭐⭐⭐
高并发千万QPS⭐⭐⭐
准确性排名不能错⭐⭐⭐
分页查询Top10、Top100⭐⭐
个人排名查询我的排名⭐⭐

🎯 技术方案

方案1:MySQL(不推荐)❌

表设计

CREATE TABLE user_rank (
    user_id BIGINT PRIMARY KEY,
    score INT NOT NULL,
    username VARCHAR(64),
    update_time DATETIME,
    
    INDEX idx_score (score DESC)  -- 按分数倒序索引
) ENGINE=InnoDB;

查询排行榜

-- ⭐ 查询Top10
SELECT user_id, score, username
FROM user_rank
ORDER BY score DESC
LIMIT 10;

-- ⭐ 查询我的排名
SELECT COUNT(*) + 1 AS rank
FROM user_rank
WHERE score > (SELECT score FROM user_rank WHERE user_id = 1001);

更新分数

@Service
public class MySQLRankService {
    
    @Autowired
    private UserRankDao rankDao;
    
    /**
     * 更新分数
     */
    public void updateScore(Long userId, int score) {
        UserRank rank = rankDao.findByUserId(userId);
        
        if (rank == null) {
            // 新用户
            rank = new UserRank();
            rank.setUserId(userId);
            rank.setScore(score);
            rankDao.insert(rank);
        } else {
            // 更新分数
            rank.setScore(rank.getScore() + score);
            rankDao.update(rank);
        }
    }
    
    /**
     * 查询Top10
     */
    public List<UserRank> getTopList(int size) {
        return rankDao.findTopN(size);
    }
    
    /**
     * 查询我的排名
     */
    public long getMyRank(Long userId) {
        return rankDao.countByScoreGreaterThan(userId);
    }
}

问题分析

性能问题 ❌:

1. 更新分数:
   - 每次更新都要写数据库 → 慢 ❌
   - 高并发时数据库压力大 ❌

2. 查询排名:
   - COUNT(*) 需要全表扫描 → 慢 ❌
   - 千万数据量,查询要好几秒 ❌

3. 实时性差:
   - 数据库写入有延迟
   - 无法做到毫秒级更新 ❌

结论:MySQL不适合实时排行榜 ❌


方案2:Redis ZSet(推荐)⭐⭐⭐

原理

ZSet = Sorted Set(有序集合)

数据结构:
key: "rank:game"
value: {
    member: userId,
    score: 积分
}

自动按score排序 ✅

例如:
rank:game = {
    (1001, 100),  ← 张三,100分
    (1002, 95),   ← 李四,95分
    (1003, 90)    ← 王五,90分
}

ZSet的特点

  • 自动排序(按score)
  • 去重(member唯一)
  • 高性能(O(logN))

Redis命令

# ⭐ 添加/更新分数
ZADD rank:game 100 1001  # 用户1001,分数100

# ⭐ 增加分数
ZINCRBY rank:game 10 1001  # 用户1001,分数+10

# ⭐ 查询Top10(倒序)
ZREVRANGE rank:game 0 9 WITHSCORES
# 返回:
# 1) "1001"  2) "110"
# 3) "1002"  4) "95"
# 5) "1003"  6) "90"

# ⭐ 查询用户排名(倒序,从0开始)
ZREVRANK rank:game 1001
# 返回:0(第1名)

# ⭐ 查询用户分数
ZSCORE rank:game 1001
# 返回:110

# ⭐ 查询排名范围(从第10名到第20名)
ZREVRANGE rank:game 10 20 WITHSCORES

# ⭐ 查询分数范围(90-100分的用户)
ZRANGEBYSCORE rank:game 90 100

# ⭐ 获取成员数量
ZCARD rank:game

代码实现

@Service
@Slf4j
public class RedisRankService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    private static final String RANK_KEY = "rank:game";
    
    /**
     * ⭐ 更新分数(增加)
     */
    public void addScore(Long userId, double score) {
        // ⭐ ZINCRBY:增加分数
        Double newScore = redisTemplate.opsForZSet()
            .incrementScore(RANK_KEY, userId.toString(), score);
        
        log.info("用户{}分数增加{},当前分数:{}", userId, score, newScore);
    }
    
    /**
     * ⭐ 设置分数(覆盖)
     */
    public void setScore(Long userId, double score) {
        // ⭐ ZADD:设置分数
        redisTemplate.opsForZSet().add(RANK_KEY, userId.toString(), score);
        
        log.info("用户{}分数设置为{}", userId, score);
    }
    
    /**
     * ⭐ 查询Top N
     */
    public List<RankItem> getTopList(int size) {
        // ⭐ ZREVRANGE:倒序查询(分数从高到低)
        Set<ZSetOperations.TypedTuple<String>> result = redisTemplate.opsForZSet()
            .reverseRangeWithScores(RANK_KEY, 0, size - 1);
        
        if (result == null || result.isEmpty()) {
            return Collections.emptyList();
        }
        
        List<RankItem> rankList = new ArrayList<>();
        int rank = 1;
        
        for (ZSetOperations.TypedTuple<String> tuple : result) {
            RankItem item = new RankItem();
            item.setRank(rank++);
            item.setUserId(Long.parseLong(tuple.getValue()));
            item.setScore(tuple.getScore());
            
            rankList.add(item);
        }
        
        return rankList;
    }
    
    /**
     * ⭐ 查询用户排名
     */
    public Long getMyRank(Long userId) {
        // ⭐ ZREVRANK:倒序排名(从0开始)
        Long rank = redisTemplate.opsForZSet()
            .reverseRank(RANK_KEY, userId.toString());
        
        if (rank == null) {
            return null;
        }
        
        // 转换为从1开始
        return rank + 1;
    }
    
    /**
     * ⭐ 查询用户分数
     */
    public Double getMyScore(Long userId) {
        // ⭐ ZSCORE:查询分数
        return redisTemplate.opsForZSet().score(RANK_KEY, userId.toString());
    }
    
    /**
     * ⭐ 查询排名范围(分页)
     */
    public List<RankItem> getRankRange(int start, int end) {
        // start和end都是从1开始的排名
        Set<ZSetOperations.TypedTuple<String>> result = redisTemplate.opsForZSet()
            .reverseRangeWithScores(RANK_KEY, start - 1, end - 1);
        
        if (result == null || result.isEmpty()) {
            return Collections.emptyList();
        }
        
        List<RankItem> rankList = new ArrayList<>();
        int rank = start;
        
        for (ZSetOperations.TypedTuple<String> tuple : result) {
            RankItem item = new RankItem();
            item.setRank(rank++);
            item.setUserId(Long.parseLong(tuple.getValue()));
            item.setScore(tuple.getScore());
            
            rankList.add(item);
        }
        
        return rankList;
    }
    
    /**
     * ⭐ 查询周围排名(我前后各5名)
     */
    public List<RankItem> getAroundRank(Long userId, int count) {
        // 1. 查询我的排名
        Long myRank = getMyRank(userId);
        if (myRank == null) {
            return Collections.emptyList();
        }
        
        // 2. 计算范围
        long start = Math.max(1, myRank - count);
        long end = myRank + count;
        
        // 3. 查询范围内的排名
        Set<ZSetOperations.TypedTuple<String>> result = redisTemplate.opsForZSet()
            .reverseRangeWithScores(RANK_KEY, start - 1, end - 1);
        
        if (result == null || result.isEmpty()) {
            return Collections.emptyList();
        }
        
        List<RankItem> rankList = new ArrayList<>();
        long rank = start;
        
        for (ZSetOperations.TypedTuple<String> tuple : result) {
            RankItem item = new RankItem();
            item.setRank((int) rank++);
            item.setUserId(Long.parseLong(tuple.getValue()));
            item.setScore(tuple.getScore());
            
            // 标记是否是自己
            item.setIsMe(item.getUserId().equals(userId));
            
            rankList.add(item);
        }
        
        return rankList;
    }
    
    /**
     * ⭐ 获取总人数
     */
    public Long getTotalCount() {
        return redisTemplate.opsForZSet().zCard(RANK_KEY);
    }
    
    /**
     * ⭐ 删除用户
     */
    public void removeUser(Long userId) {
        redisTemplate.opsForZSet().remove(RANK_KEY, userId.toString());
    }
}

@Data
class RankItem {
    private Integer rank;      // 排名
    private Long userId;       // 用户ID
    private String username;   // 用户名
    private Double score;      // 分数
    private Boolean isMe;      // 是否是自己
}

API接口

@RestController
@RequestMapping("/api/rank")
public class RankController {
    
    @Autowired
    private RedisRankService rankService;
    
    @Autowired
    private UserService userService;
    
    /**
     * ⭐ 查询Top10
     */
    @GetMapping("/top10")
    public Result<List<RankItem>> getTop10() {
        List<RankItem> rankList = rankService.getTopList(10);
        
        // 填充用户信息
        for (RankItem item : rankList) {
            User user = userService.getById(item.getUserId());
            item.setUsername(user.getUsername());
        }
        
        return Result.success(rankList);
    }
    
    /**
     * ⭐ 查询我的排名
     */
    @GetMapping("/my")
    public Result<RankItem> getMyRank(@RequestParam Long userId) {
        Long rank = rankService.getMyRank(userId);
        Double score = rankService.getMyScore(userId);
        
        if (rank == null) {
            return Result.fail("未上榜");
        }
        
        RankItem item = new RankItem();
        item.setRank(rank.intValue());
        item.setUserId(userId);
        item.setScore(score);
        
        // 填充用户信息
        User user = userService.getById(userId);
        item.setUsername(user.getUsername());
        
        return Result.success(item);
    }
    
    /**
     * ⭐ 更新分数(游戏胜利)
     */
    @PostMapping("/win")
    public Result<?> win(@RequestParam Long userId, @RequestParam int score) {
        rankService.addScore(userId, score);
        
        // 返回新排名
        Long newRank = rankService.getMyRank(userId);
        return Result.success("分数+%d,当前排名:%d", score, newRank);
    }
    
    /**
     * ⭐ 查询周围排名
     */
    @GetMapping("/around")
    public Result<List<RankItem>> getAroundRank(@RequestParam Long userId) {
        List<RankItem> rankList = rankService.getAroundRank(userId, 5);
        
        // 填充用户信息
        for (RankItem item : rankList) {
            User user = userService.getById(item.getUserId());
            item.setUsername(user.getUsername());
        }
        
        return Result.success(rankList);
    }
}

性能测试

@SpringBootTest
public class RankPerformanceTest {
    
    @Autowired
    private RedisRankService rankService;
    
    @Test
    public void testAddScore() {
        int count = 1000000;  // 100万次
        long start = System.currentTimeMillis();
        
        for (int i = 0; i < count; i++) {
            rankService.addScore((long) (i % 10000), 10);
        }
        
        long end = System.currentTimeMillis();
        
        System.out.println("添加" + count + "次,耗时: " + (end - start) + "ms");
        System.out.println("QPS: " + (count * 1000 / (end - start)));
    }
    
    @Test
    public void testGetTopList() {
        int count = 10000;  // 1万次
        long start = System.currentTimeMillis();
        
        for (int i = 0; i < count; i++) {
            rankService.getTopList(10);
        }
        
        long end = System.currentTimeMillis();
        
        System.out.println("查询Top10共" + count + "次,耗时: " + (end - start) + "ms");
        System.out.println("QPS: " + (count * 1000 / (end - start)));
    }
    
    @Test
    public void testGetMyRank() {
        int count = 10000;  // 1万次
        long start = System.currentTimeMillis();
        
        for (int i = 0; i < count; i++) {
            rankService.getMyRank(1001L);
        }
        
        long end = System.currentTimeMillis();
        
        System.out.println("查询排名共" + count + "次,耗时: " + (end - start) + "ms");
        System.out.println("QPS: " + (count * 1000 / (end - start)));
    }
}

测试结果

添加1000000次,耗时: 5000ms
QPS: 200000  ← ⭐ 20万QPS

查询Top10共10000次,耗时: 500ms
QPS: 20000  ← ⭐ 2万QPS

查询排名共10000次,耗时: 500ms
QPS: 20000  ← ⭐ 2万QPS

优缺点

优点 ✅:

  • 高性能(20万QPS)
  • 实时更新(毫秒级)
  • 自动排序(无需手动维护)
  • 支持分页、范围查询
  • 实现简单

缺点 ❌:

  • 内存消耗(千万用户 × 8字节 = 80MB,可接受)
  • 数据持久化(需要AOF或RDB)

适用场景

  • 大部分排行榜场景 ⭐⭐⭐
  • 游戏、直播、电商等

🎯 高级功能

功能1:多维度排行榜 📊

场景

- 总榜(全部用户)
- 日榜(今天)
- 周榜(本周)
- 月榜(本月)
- 地区榜(按地区)

实现:多个ZSet

@Service
public class MultiDimensionRankService {
    
    private static final String TOTAL_RANK = "rank:total";    // 总榜
    private static final String DAILY_RANK = "rank:daily:";   // 日榜
    private static final String WEEKLY_RANK = "rank:weekly:"; // 周榜
    private static final String MONTHLY_RANK = "rank:monthly:"; // 月榜
    
    /**
     * ⭐ 更新分数(同时更新多个榜单)
     */
    public void addScore(Long userId, double score) {
        String today = LocalDate.now().toString();  // 2021-01-01
        String week = getWeekKey();    // 2021-W01
        String month = getMonthKey();  // 2021-01
        
        // ⭐ 同时更新4个榜单
        redisTemplate.opsForZSet().incrementScore(TOTAL_RANK, userId.toString(), score);
        redisTemplate.opsForZSet().incrementScore(DAILY_RANK + today, userId.toString(), score);
        redisTemplate.opsForZSet().incrementScore(WEEKLY_RANK + week, userId.toString(), score);
        redisTemplate.opsForZSet().incrementScore(MONTHLY_RANK + month, userId.toString(), score);
        
        // ⭐ 设置过期时间
        redisTemplate.expire(DAILY_RANK + today, 2, TimeUnit.DAYS);    // 日榜保留2天
        redisTemplate.expire(WEEKLY_RANK + week, 8, TimeUnit.DAYS);    // 周榜保留8天
        redisTemplate.expire(MONTHLY_RANK + month, 32, TimeUnit.DAYS); // 月榜保留32天
    }
    
    /**
     * 查询日榜
     */
    public List<RankItem> getDailyTop(int size) {
        String today = LocalDate.now().toString();
        return getTopList(DAILY_RANK + today, size);
    }
    
    /**
     * 查询周榜
     */
    public List<RankItem> getWeeklyTop(int size) {
        String week = getWeekKey();
        return getTopList(WEEKLY_RANK + week, size);
    }
    
    private String getWeekKey() {
        // 获取本周的key(如:2021-W01)
        return LocalDate.now().format(DateTimeFormatter.ofPattern("YYYY-'W'ww"));
    }
    
    private String getMonthKey() {
        // 获取本月的key(如:2021-01)
        return LocalDate.now().format(DateTimeFormatter.ofPattern("YYYY-MM"));
    }
    
    private List<RankItem> getTopList(String key, int size) {
        // 实现省略...
        return null;
    }
}

功能2:分组排行榜 🏅

场景:按段位分组

青铜榜:0-999分
白银榜:1000-1999分
黄金榜:2000-2999分
钻石榜:3000+分

实现

@Service
public class TierRankService {
    
    /**
     * ⭐ 更新分数(根据分数分配到不同段位榜)
     */
    public void addScore(Long userId, double score) {
        Double currentScore = redisTemplate.opsForZSet()
            .score("rank:total", userId.toString());
        
        if (currentScore == null) {
            currentScore = 0.0;
        }
        
        double newScore = currentScore + score;
        
        // ⭐ 计算段位
        String tier = getTier(newScore);
        
        // 更新对应段位的榜单
        String tierKey = "rank:tier:" + tier;
        redisTemplate.opsForZSet().add(tierKey, userId.toString(), newScore);
    }
    
    private String getTier(double score) {
        if (score < 1000) return "bronze";   // 青铜
        if (score < 2000) return "silver";   // 白银
        if (score < 3000) return "gold";     // 黄金
        return "diamond";                     // 钻石
    }
    
    /**
     * 查询段位榜
     */
    public List<RankItem> getTierTop(String tier, int size) {
        String key = "rank:tier:" + tier;
        // 查询Top N
        return getTopList(key, size);
    }
}

功能3:定时任务(榜单重置)🕐

场景:每天0点重置日榜

@Component
@Slf4j
public class RankScheduler {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    /**
     * ⭐ 每天0点重置日榜
     */
    @Scheduled(cron = "0 0 0 * * ?")
    public void resetDailyRank() {
        String yesterday = LocalDate.now().minusDays(1).toString();
        String yesterdayKey = "rank:daily:" + yesterday;
        
        // ⭐ 1. 备份昨天的榜单到MySQL(归档)
        archiveToDB(yesterdayKey);
        
        // ⭐ 2. 删除昨天的榜单
        redisTemplate.delete(yesterdayKey);
        
        log.info("日榜已重置: {}", yesterday);
    }
    
    /**
     * 归档到数据库
     */
    private void archiveToDB(String key) {
        // 查询Top100
        Set<ZSetOperations.TypedTuple<String>> top100 = redisTemplate.opsForZSet()
            .reverseRangeWithScores(key, 0, 99);
        
        if (top100 == null || top100.isEmpty()) {
            return;
        }
        
        // 批量插入数据库
        List<RankHistory> historyList = new ArrayList<>();
        int rank = 1;
        
        for (ZSetOperations.TypedTuple<String> tuple : top100) {
            RankHistory history = new RankHistory();
            history.setDate(LocalDate.now().minusDays(1));
            history.setRank(rank++);
            history.setUserId(Long.parseLong(tuple.getValue()));
            history.setScore(tuple.getScore());
            
            historyList.add(history);
        }
        
        // 批量保存
        rankHistoryDao.batchInsert(historyList);
        
        log.info("归档完成: key={}, count={}", key, historyList.size());
    }
}

功能4:实时推送(排名变化通知)📢

场景:排名变化时,WebSocket推送

@Service
public class RankPushService {
    
    @Autowired
    private WebSocketService wsService;
    
    /**
     * 更新分数并推送排名变化
     */
    public void addScoreWithPush(Long userId, double score) {
        // 1. 获取旧排名
        Long oldRank = rankService.getMyRank(userId);
        
        // 2. 更新分数
        rankService.addScore(userId, score);
        
        // 3. 获取新排名
        Long newRank = rankService.getMyRank(userId);
        
        // 4. ⭐ 判断排名是否变化
        if (oldRank == null || !oldRank.equals(newRank)) {
            // 排名变化,推送通知
            RankChangeMessage msg = new RankChangeMessage();
            msg.setUserId(userId);
            msg.setOldRank(oldRank);
            msg.setNewRank(newRank);
            msg.setScore(rankService.getMyScore(userId));
            
            // ⭐ WebSocket推送
            wsService.sendToUser(userId, msg);
            
            log.info("排名变化通知: userId={}, {}→{}", userId, oldRank, newRank);
        }
        
        // 5. ⭐ 如果进入Top10,推送给所有人
        if (newRank != null && newRank <= 10) {
            wsService.broadcast("用户" + userId + "进入Top10!");
        }
    }
}

📊 架构总结

        实时排行榜系统架构

┌──────────────────────────────────────┐
│           客户端                      │
│                                      │
│  - 查询排行榜                        │
│  - 更新分数                          │
│  - WebSocket接收推送                 │
└─────────────┬────────────────────────┘
              │
              ↓
┌──────────────────────────────────────┐
│         应用服务器                    │
│                                      │
│  - 排行榜API                         │
│  - WebSocket推送                     │
│  - 定时任务                          │
└───────┬──────────────┬───────────────┘
        │              │
        ↓              ↓
┌──────────────┐  ┌──────────────────┐
│    Redis     │  │     MySQL        │
│              │  │                  │
│ - ZSet存储   │  │ - 历史归档       │
│ - 实时查询   │  │ - 用户信息       │
│ - 多维度榜   │  │                  │
└──────────────┘  └──────────────────┘

🎓 面试题速答

Q1: 如何实现实时排行榜?

A: Redis ZSet(推荐):

数据结构:
- key: "rank:game"
- value: {member: userId, score: 积分}

核心命令:
- ZADD: 添加/更新分数
- ZINCRBY: 增加分数
- ZREVRANGE: 查询Top N
- ZREVRANK: 查询排名

性能:
- 更新:20万QPS
- 查询:2万QPS
- 实时性:毫秒级

Q2: 如何查询我的排名?

A: ZREVRANK命令

ZREVRANK rank:game 1001
# 返回:0(第1名,从0开始)
Long rank = redisTemplate.opsForZSet()
    .reverseRank(RANK_KEY, userId.toString());

// 转换为从1开始
return rank + 1;

时间复杂度:O(logN)


Q3: 如何实现多维度排行榜(日榜、周榜、月榜)?

A: 多个ZSet + 过期时间

// 同时更新4个榜单
redisTemplate.opsForZSet().incrementScore("rank:total", userId, score);
redisTemplate.opsForZSet().incrementScore("rank:daily:2021-01-01", userId, score);
redisTemplate.opsForZSet().incrementScore("rank:weekly:2021-W01", userId, score);
redisTemplate.opsForZSet().incrementScore("rank:monthly:2021-01", userId, score);

// 设置过期时间
redisTemplate.expire("rank:daily:2021-01-01", 2, TimeUnit.DAYS);

优点

  • 每个榜单独立
  • 自动过期,节省内存

Q4: 排行榜数据如何持久化?

A: 两种方案

  1. Redis持久化

    • AOF:实时持久化
    • RDB:定期快照
  2. 归档到MySQL

    @Scheduled(cron = "0 0 0 * * ?")  // 每天0点
    public void archiveTop100() {
        // 查询Top100
        List<RankItem> top100 = rankService.getTopList(100);
        
        // 批量保存到MySQL
        rankHistoryDao.batchInsert(top100);
        
        // 删除Redis中的旧数据
        redisTemplate.delete("rank:daily:" + yesterday);
    }
    

推荐:Redis AOF + 定期归档MySQL


Q5: 如何处理分数相同的情况?

A: 添加时间戳

// 分数 = 实际分数 × 10^10 + 时间戳
// 例如:100分,时间戳1609459200
// Redis分数:1000000000000 + 1609459200 = 1001609459200

public void addScore(Long userId, int score) {
    // 实际分数 × 10^10
    double baseScore = score * 10_000_000_000L;
    
    // 加上时间戳(微秒)
    double timestamp = System.currentTimeMillis() * 1000;
    
    double finalScore = baseScore + timestamp;
    
    redisTemplate.opsForZSet().add(RANK_KEY, userId.toString(), finalScore);
}

// 显示时,提取实际分数
public int getRealScore(double redisScore) {
    return (int) (redisScore / 10_000_000_000L);
}

优点

  • 分数相同时,先达到的排名靠前
  • 不会出现并列排名

Q6: 千万用户,Redis内存够吗?

A: 内存估算

数据量:
- 1000万用户
- 每个用户:userId(8字节) + score(8字节) = 16字节
- 总内存:1000万 × 16字节 = 160MB ✅

实际内存:
- ZSet的overhead(约2倍)
- 实际占用:320MB

结论:完全够用 ✅

优化

  • 只保存Top100万用户
  • 定期清理不活跃用户
  • 使用Redis Cluster分片

🎬 总结

       实时排行榜核心技术

┌────────────────────────────────────┐
 Redis ZSet(有序集合)              
                                    
 核心命令:                         
 - ZADD: 添加/更新                  
 - ZINCRBY: 增加分数                
 - ZREVRANGE: Top N                 
 - ZREVRANK: 查排名                 
                                    
 性能:20万QPS ⭐⭐⭐                
└────────────────────────────────────┘

┌────────────────────────────────────┐
 多维度榜单                         
 - 总榜、日榜、周榜、月榜           
 - 多个ZSet + 过期时间              
└────────────────────────────────────┘

┌────────────────────────────────────┐
 持久化                             
 - Redis AOF/RDB                    
 - 定期归档MySQL                    
└────────────────────────────────────┘

    Redis ZSet是最佳选择!✅

🎉 恭喜你!

你已经完全掌握了实时排行榜系统的设计!🎊

核心要点

  1. Redis ZSet:自动排序,高性能
  2. 多维度榜单:多个ZSet + 过期时间
  3. 分数相同:加时间戳区分
  4. 持久化:AOF + 归档MySQL

下次面试,这样回答

"实时排行榜使用Redis的ZSet实现。ZSet是有序集合,会自动按score排序,支持O(logN)的更新和查询。

核心命令包括ZADD添加分数、ZINCRBY增加分数、ZREVRANGE查询Top N、ZREVRANK查询排名。性能非常高,更新QPS可达20万,查询QPS可达2万。

对于多维度榜单,使用多个ZSet分别存储总榜、日榜、周榜、月榜,日榜设置2天过期,周榜8天过期,月榜32天过期,自动回收内存。

持久化方面,Redis使用AOF实时持久化,同时每天凌晨用定时任务将Top100归档到MySQL,作为历史数据查询。

我们项目的游戏排行榜采用这套方案,支持千万用户实时排名,平均响应时间5ms,运行稳定。"

面试官:👍 "很好!你对实时排行榜系统的设计理解很透彻!"


本文完 🎬

上一篇: 200-设计一个亿级用户的社交关系链存储.md
下一篇: 202-设计一个文件上传和存储服务.md

作者注:写完这篇,我都想去腾讯做王者荣耀排行榜了!🏆
如果这篇文章对你有帮助,请给我一个Star⭐!