当前位置: 首页 > news >正文

每天一道面试题之架构篇|低延迟实时排行榜系统架构设计

面试官直接问道:"如果要设计一个支持百万用户实时竞技的游戏排行榜,如何保证低延迟和高并发?"

一、开篇:实时排行榜的核心挑战

想象一下:王者荣耀巅峰赛最后10秒,百万玩家同时刷新排名,系统如何保证实时性和准确性?

实时排行榜核心挑战

  • 极低延迟:95%请求响应时间<10ms
  • 高并发读写:瞬时万级QPS处理能力
  • 数据一致性:排名准确无跳变
  • 弹性扩展:支持从千级到百万级用户平滑扩容

这就像奥运会百米决赛,计时系统必须精确到毫秒,排名结果必须实时准确

二、核心架构设计

2.1 技术选型与对比

各方案性能对比

方案响应延迟并发能力排名精度适用场景
MySQL+实时计算100ms+千级QPS精确小型系统
Redis SortedSet1-5ms万级QPS精确中型排行榜
Redis+本地缓存<1ms十万级QPS最终一致大型实时榜

推荐架构Redis SortedSet + 本地缓存 + 异步持久化

三、关键技术实现

3.1 Redis SortedSet核心操作

Spring Boot集成Redis排行榜

@Service
@Slf4j
publicclassRankingService{

@Autowired
privateRedisTemplate<String, Object> redisTemplate;

privatestaticfinalString RANKING_KEY ="game:ranking:season1";

// 更新玩家分数
publicvoidupdatePlayerScore(String playerId,doublescore){
// 使用管道提升性能
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
connection.zAdd(RANKING_KEY.getBytes(), score, playerId.getBytes());
returnnull;
});

log.debug("更新玩家{}分数: {}", playerId, score);
}

// 获取玩家排名
publicLonggetPlayerRank(String playerId){
// ZREVRANK获取排名(从0开始)
Long rank = redisTemplate.opsForZSet().reverseRank(RANKING_KEY, playerId);
returnrank !=null? rank +1:null;// 转换为从1开始
}

// 获取排行榜前N名
publicSet<ZSetOperations.TypedTuple<Object>> getTopN(intn) {
returnredisTemplate.opsForZSet().reverseRangeWithScores(RANKING_KEY,0, n -1);
}

// 获取玩家周围排名(前后各5名)
publicSet<ZSetOperations.TypedTuple<Object>> getAroundPlayer(String playerId,intrange) {
Long rank = getPlayerRank(playerId);
if(rank ==null)returnCollections.emptySet();

longstart = Math.max(0, rank - range -1);
longend = rank + range -1;

returnredisTemplate.opsForZSet().reverseRangeWithScores(RANKING_KEY, start, end);
}
}

3.2 多级缓存架构

本地缓存优化设计

@Component
@Slf4j
publicclassRankingCacheManager{

// 本地缓存top100排行榜
privatefinalCache<String, List<RankingItem>> localCache = Caffeine.newBuilder()
.maximumSize(10)// 缓存10个不同的排行榜
.expireAfterWrite(100, TimeUnit.MILLISECONDS)// 100ms过期
.refreshAfterWrite(50, TimeUnit.MILLISECONDS)// 50ms刷新
.build();

@Autowired
privateRankingService rankingService;

// 获取带缓存的排行榜
publicList<RankingItem>getTopNWithCache(intn){
String cacheKey ="top_"+ n;
returnlocalCache.get(cacheKey, key -> {
Set<ZSetOperations.TypedTuple<Object>> topN =
rankingService.getTopN(n);
returnconvertToRankingList(topN);
});
}

// 异步刷新缓存
@Scheduled(fixedRate =50)
publicvoidrefreshCache(){
// 异步刷新前100名缓存
CompletableFuture.runAsync(() -> {
localCache.put("top_100",
convertToRankingList(rankingService.getTopN(100)));
});
}

privateList<RankingItem>convertToRankingList(Set<ZSetOperations.TypedTuple<Object>> set){
List<RankingItem> result =newArrayList<>();
longrank =1;

for(ZSetOperations.TypedTuple<Object> tuple : set) {
result.add(newRankingItem(
(String) tuple.getValue(),
tuple.getScore(),
rank++
));
}

returnresult;
}
}

3.3 异步持久化与监控

RocketMQ异步数据持久化

@Component
@Slf4j
publicclassRankingDataAsyncService{

@Autowired
privateRocketMQTemplate rocketMQTemplate;

// 异步记录分数变更
@Async
publicvoidasyncRecordScoreChange(String playerId,doubleoldScore,
doublenewScore, String source)
{
ScoreChangeEvent event =newScoreChangeEvent(playerId, oldScore,
newScore, source,newDate());

rocketMQTemplate.sendOneWay("ranking-score-topic",
MessageBuilder.withPayload(event).build());
}

// 批量更新数据库
@RocketMQMessageListener(topic ="ranking-score-topic",
consumerGroup ="ranking-persist-group")
publicvoidpersistScoreChanges(List<ScoreChangeEvent> events){
if(events.isEmpty())return;

// 批量插入数据库
try{
rankingMapper.batchInsertScoreHistory(events);
log.info("成功持久化{}条分数记录", events.size());
}catch(Exception e) {
log.error("分数记录持久化失败", e);
// 加入重试队列
events.forEach(event ->
rocketMQTemplate.sendOneWay("ranking-score-retry-topic",
MessageBuilder.withPayload(event).build()));
}
}
}

四、高级特性实现

4.1 分数防刷与校验

基于滑动窗口的限流防护

@Component
@Slf4j
publicclassScoreAntiCheatService{

@Autowired
privateRedisTemplate<String, Object> redisTemplate;

// 检查分数更新频率
publicbooleancheckUpdateFrequency(String playerId,doublenewScore){
String key ="score_update:"+ playerId;
longnow = System.currentTimeMillis();

// 使用滑动窗口限制频率
Long count = redisTemplate.opsForZSet().count(key, now -60000, now);
if(count !=null&& count >=100) {
log.warn("玩家{}分数更新过于频繁", playerId);
returnfalse;
}

// 记录本次更新
redisTemplate.opsForZSet().add(key, String.valueOf(now), now);
redisTemplate.expire(key, Duration.ofMinutes(2));

returntrue;
}

// 分数变化合理性校验
publicbooleanvalidateScoreChange(String playerId,doubleoldScore,
doublenewScore)
{
doublemaxIncrease = getMaxAllowedIncrease(playerId);

if(newScore - oldScore > maxIncrease) {
log.warn("玩家{}分数异常增长: {} -> {}", playerId, oldScore, newScore);
returnfalse;
}

returntrue;
}

privatedoublegetMaxAllowedIncrease(String playerId){
// 根据玩家等级、历史表现等动态计算最大允许增长
return1000.0;// 示例值
}
}

4.2 实时监控与告警

排行榜健康度监控

@Component
@Slf4j
publicclassRankingMonitorService{

@Autowired
privateRedisTemplate<String, Object> redisTemplate;

@Scheduled(fixedRate =30000)
publicvoidmonitorRankingHealth(){
// 监控Redis内存使用
Long zsetSize = redisTemplate.opsForZSet().size(RANKING_KEY);
Double memoryUsage = getRedisMemoryUsage();

if(zsetSize !=null&& zsetSize >1000000) {
log.warn("排行榜数据量过大: {}", zsetSize);
// 触发数据归档
archiveOldData();
}

if(memoryUsage >0.8) {
log.error("Redis内存使用率过高: {}", memoryUsage);
// 发送告警通知
sendMemoryAlert(memoryUsage);
}
}

// 性能监控端点
@Endpoint(id ="ranking-stats")
@Component
publicclassRankingStatsEndpoint{

@ReadOperation
publicMap<String, Object>rankingStats(){
Map<String, Object> stats =newHashMap<>();
stats.put("totalPlayers",
redisTemplate.opsForZSet().size(RANKING_KEY));
stats.put("updateQps", getUpdateQps());
stats.put("avgLatency", getAverageLatency());
returnstats;
}
}
}

五、完整架构示例

5.1 系统架构图

[游戏客户端] -> [API网关] -> [排行榜服务] -> [Redis集群]
| | | |
v v v v
[分数校验] <- [限流防护] <- [本地缓存] <- [异步持久化]
| | | |
v v v v
[监控告警] -> [数据归档] -> [MySQL集群] -> [数据分析]

5.2 配置优化

# application-ranking.yml
spring:
redis:
cluster:
nodes:redis-cluster:6379
timeout:1000
lettuce:
pool:
max-active:1000
max-wait:10ms
max-idle:100

ranking:
local-cache:
enabled:true
top-n:1000
expire-time:100ms
anti-cheat:
enabled:true
max-updates-per-minute:100
max-score-increase:1000
monitor:
enabled:true
check-interval:30s
memory-threshold:0.8

六、面试陷阱与加分项

6.1 常见陷阱问题

问题1:"Redis内存爆了怎么办?"

参考答案

  • 定期归档历史数据到MySQL
  • 使用Redis集群分片存储
  • 设置适当的数据过期策略
  • 监控内存使用并设置自动告警

问题2:"网络分区时排名不一致怎么处理?"

参考答案

  • 使用Redis集群的WAIT命令确保数据同步
  • 客户端缓存降级方案
  • 最终一致性+版本号控制

问题3:"如何支持多种排序规则?"

参考答案

  • 使用多个SortedSet存储不同维度的排名
  • 基于标签的分数设计(如:分数+时间戳)
  • 实时计算综合排名

6.2 面试加分项

  1. 业界最佳实践

    • 腾讯游戏:Redis集群+自定义内存分配策略
    • 网易:多级缓存+动态扩容机制
    • 暴雪:分区排行榜+跨服排名合并
  2. 高级特性

    • 实时弹幕:排名变化实时通知
    • 赛季系统:自动赛季切换和数据重置
    • 数据分析:玩家行为深度分析
  3. 性能优化

    • 连接池优化:动态调整Redis连接数
    • 序列化优化:使用Protobuf减少网络传输
    • 批量处理:分数更新批量提交

七、总结与互动

排行榜设计哲学Redis扛实时,缓存降延迟,异步保持久,监控稳运行——四位一体构建高性能排行榜系统

记住这个性能公式:Redis SortedSet + 本地缓存 + 异步持久化 + 实时监控= 完美实时排行榜


思考题:在你的游戏项目中,排行榜最大的性能瓶颈是什么?欢迎在评论区分享优化经验!

关注我,每天搞懂一道面试题,助你轻松拿下Offer!

http://www.cnnetsun.cn/news/136873.html

相关文章:

  • 新人必看盘点知名CTF练习靶场,从零基础入门到精通,收藏这一篇就够了!
  • Pythonselenium自动化测试实战项目
  • 关于Comtos Linux (朱雀)主体源码的选择
  • 超级Mini小车功能说明
  • STC32G12单片机替换成STC32F12单片机,直接替换的结果
  • SIEMENS 6SL3210-1PE33-0CL0 变频器
  • 软件测试常用的7种方法,最后一个是升职加薪关键!(零基础小白转行IT互联网高效进阶)
  • 【RTOS】EasyLog的移植与使用
  • 在数据库里玩“平行宇宙”:MatrixOne Data Branch 让数据也拥有Git 的分支/合并/对比/回滚(含跨集群同步)
  • 基于单片机的全自动洗衣机系统的设计
  • 5.6 模型部署与智能体集成实战
  • 基于单片机的球赛计分牌的设计
  • ArcGIS Pro 从入门到实战基础篇(10):地图菜单
  • Kotaemon与Redis/Memcached集成:构建高速缓存层
  • 【鸿蒙三方库编译】lycium_plusplus(lycium++)高效完成鸿蒙C/C++编译
  • 2025年度GEO服务商权威甄选指南:技术深度与商业价值的双重考量
  • 收藏备用!Java程序员转AI大模型:从技术沉淀到AI爆发的进阶之路
  • Python 爬虫实战:Session 会话维持爬取需登录内容
  • 基于移相全桥变换器的电池充电仿真模型,采用电压电流双闭环PI控制。 电池先经历CC模式而后进入...
  • 基于COMSOL模拟的水力压裂技术研究:固体力学与达西定理的应用
  • Redis 性能调优(二)
  • Doris 性能调优实践指南(可直接落地)
  • presum|二分try+滑窗cnt
  • Web自动化测试:Unittest单元测试框架
  • Apache2最佳实践
  • 实力派,也可以是偶像派
  • 基于单片机的多功能万年历
  • AI搜索时代:技术演进、产业分化与深度变革
  • SGMICRO圣邦微 SGM2019-2.5YC5G/TR SC70-5 线性稳压器(LDO)
  • 一文搞懂 低功耗蓝牙BLE 中的 ATT、GATT、MTU 与 20 字节限制