📖 开场:游戏排行榜
想象你在玩王者荣耀 🎮:
简单排行榜:
每天零点统计昨天的数据:
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: 两种方案:
-
Redis持久化:
- AOF:实时持久化
- RDB:定期快照
-
归档到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是最佳选择!✅
🎉 恭喜你!
你已经完全掌握了实时排行榜系统的设计!🎊
核心要点:
- Redis ZSet:自动排序,高性能
- 多维度榜单:多个ZSet + 过期时间
- 分数相同:加时间戳区分
- 持久化: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⭐!