游戏排行榜的实时存储架构:Redis + MySQL的混合存储与AI驱动的缓存预热
一、当Top100变成一场事故:排行榜延迟引发的连锁雪崩
在一款DAU超过2000万的竞技手游中,赛季结算日凌晨,排行榜服务突然崩溃。究其原因,并非数据库扛不住写入压力,而是排行榜查询接口的P99延迟从平时的15ms飙升至3.2秒——大量玩家在结算前疯狂刷新排名,Redis Sorted Set的ZRANK操作在百万级成员集合上出现了CPU瓶颈。运维临时加了10台Redis只读副本,却发现新节点冷启动时数据加载耗时40分钟,完全来不及。
这不是孤例。任何带有排行榜功能的游戏,迟早会遇到同一个问题:Sorted Set在数据量大、并发高时性能退化严重,而单纯加机器解决不了冷启动的预热问题。
更深层的矛盾在于数据一致性的取舍。Redis作为纯内存存储,宕机即丢数据的风险在排名场景下尤为敏感——玩家刚打上去的分数因为Redis主从切换丢失,投诉量会瞬间爆炸。而在MySQL里跑排名查询(ORDER BY score DESC LIMIT 100),千万行数据走全表扫描,哪怕加了复合索引,延迟也动辄秒级。
还有一个被忽略的维度:排名的"时效性梯度"。赛季榜需要强一致性,实时反映每一次对局结果;周榜允许秒级延迟;历史榜则是纯只读。这三种时效性需求挤在同一套架构里,互相拖累,这就是典型的"架构塌缩"。
二、三层分离的混合存储引擎:冷热数据的智能分流
解决思路是将排行榜数据按"热度—时效性"矩阵拆分为三层,每层用最合适的存储引擎:
Hot Tier(热层):存储当前赛季的实时排名数据。采用Redis Cluster分片,按rank_key哈希分布。每个分片只存储对应赛季的排名数据,单个分片数据量控制在500MB以内,保证ZRANK/ZREVRANGE操作在1ms内完成。关键设计是开启了AOF持久化(appendfsync everysec模式),配合Sentinel实现30秒内自动故障转移。
Warm Tier(温层):存储周榜、日榜等具有明确生命周期的排名数据。同样使用Redis,但数据带有TTL(7天),过期自动清理。这部分数据允许最终一致性,主从复制开启min-replicas-to-write 1,在可用性和一致性之间取平衡。
Cold Tier(冷层):MySQL分区表存储所有历史排名快照。按赛季+月份做RANGE分区,每个分区独立索引。查询时走分区裁剪,避免全表扫描。典型查询"2025年S1赛季第3周Top100"只需扫描一个分区的几十万行数据。
三、从Redis Pipeline到AI预热引擎的完整实现
先说最基础的写入链路。排名计算服务消费Kafka消息后,使用Redis Pipeline批量更新分数:
import redis
import json
from typing import List, Tuple
from dataclasses import dataclass
@dataclass
class ScoreEvent:
player_id: str
score: int
season: str
timestamp: int
class RankingWriter:
def __init__(self, redis_cluster: redis.RedisCluster, mysql_pool):
self.redis = redis_cluster
self.mysql = mysql_pool
def batch_update_scores(self, events: List[ScoreEvent]) -> int:
"""批量更新排名分数,返回成功数量"""
success_count = 0
pipeline = self.redis.pipeline(transaction=False)
try:
for event in events:
hot_key = f"rank:season:{event.season}:hot"
warm_key = f"rank:season:{event.season}:week:{event.timestamp // 604800}"
pipeline.zadd(hot_key, {event.player_id: event.score})
pipeline.zadd(warm_key, {event.player_id: event.score})
results = pipeline.execute()
success_count = sum(1 for r in results if r is not None)
# 异步写MySQL冷层
self._async_write_cold(events)
except redis.RedisError as e:
# Redis写入失败时的降级策略
self._fallback_to_mysql(events)
raise RankingWriteException(f"Redis写入失败,已降级到MySQL: {e}")
finally:
pipeline.reset()
return success_count
def _async_write_cold(self, events: List[ScoreEvent]):
"""异步写入MySQL冷层"""
conn = None
try:
conn = self.mysql.get_connection()
cursor = conn.cursor()
sql = """
INSERT INTO ranking_history
(player_id, score, season, week_start, created_at)
VALUES (%s, %s, %s, %s, NOW())
ON DUPLICATE KEY UPDATE
score = GREATEST(score, VALUES(score))
"""
for event in events:
week_start = (event.timestamp // 604800) * 604800
cursor.execute(sql, (
event.player_id, event.score,
event.season, week_start
))
conn.commit()
except Exception as e:
if conn:
conn.rollback()
# 冷层写入失败不影响热层服务
print(f"Cold tier write failed: {e}")
finally:
if conn:
conn.close()
def _fallback_to_mysql(self, events: List[ScoreEvent]):
"""Redis完全不可用时降级到MySQL直接写入"""
conn = self.mysql.get_connection()
try:
cursor = conn.cursor()
sql = """
INSERT INTO ranking_fallback
(player_id, score, season, created_at)
VALUES (%s, %s, %s, NOW())
"""
cursor.executemany(sql, [
(e.player_id, e.score, e.season)
for e in events
])
conn.commit()
except Exception as e:
conn.rollback()
raise e
finally:
conn.close()
然后是AI预热引擎。核心思路是:通过分析历史访问模式,预测哪些排名区间会在特定时间段被高频访问,提前将数据加载到Redis热层:
import numpy as np
from collections import defaultdict
from datetime import datetime, timedelta
class AIPreheatEngine:
def __init__(self, redis_cluster, mysql_pool, model):
self.redis = redis_cluster
self.mysql = mysql_pool
self.model = model # 预训练的LSTM时序模型
def predict_hot_ranks(self, season: str,
target_time: datetime) -> List[Tuple[int, int]]:
"""预测目标时段的高频访问排名区间"""
# 特征工程:提取历史访问模式
features = self._extract_features(season, target_time)
# LSTM推理
predictions = self.model.predict(features)
# 解析预测结果:返回 [(start_rank, end_rank, probability), …]
hot_ranges = []
for pred in predictions:
if pred['probability'] > 0.6:
hot_ranges.append((pred['start_rank'], pred['end_rank']))
return hot_ranges
def preheat(self, season: str, target_time: datetime):
"""执行预热:将预测的热门排名区间数据从MySQL加载到Redis"""
hot_ranges = self.predict_hot_ranks(season, target_time)
if not hot_ranges:
return 0
conn = None
try:
conn = self.mysql.get_connection()
loaded = 0
for start, end in hot_ranges:
sql = """
SELECT player_id, score
FROM ranking_history
WHERE season = %s
ORDER BY score DESC
LIMIT %s OFFSET %s
"""
cursor = conn.cursor(dictionary=True)
cursor.execute(sql, (season, end – start + 1, start – 1))
rows = cursor.fetchall()
if rows:
pipeline = self.redis.pipeline(transaction=False)
hot_key = f"rank:season:{season}:hot"
for row in rows:
pipeline.zadd(hot_key, {row['player_id']: row['score']})
pipeline.expire(hot_key, 86400 * 7) # 7天TTL
pipeline.execute()
loaded += len(rows)
return loaded
except Exception as e:
raise PreheatException(f"预热失败: {e}")
finally:
if conn:
conn.close()
def _extract_features(self, season: str, target_time: datetime):
"""提取时序特征"""
# 获取过去4周同一时段的历史访问数据
features = []
for week_offset in range(1, 5):
ts = target_time – timedelta(weeks=week_offset)
# 提取该时段的访问频次、数据量、热点排名等
features.append({
'hour': ts.hour,
'day_of_week': ts.weekday(),
'is_weekend': 1 if ts.weekday() >= 5 else 0,
'season_stage': self._calc_season_stage(season, ts)
})
return np.array(features)
def _calc_season_stage(self, season: str, ts: datetime) -> int:
"""计算赛季阶段:0=季前, 1=季中, 2=冲刺期, 3=结算期"""
# 简化逻辑,实际会查赛季配置表
return 0
四、当AI预判失效:混合架构的六重边界
这套方案不是银弹,有两类场景需要特别注意:
场景一:突发热点事件。当某个主播直播冲榜、或者官方推出限时活动时,访问模式会偏离历史规律。AI模型基于历史数据的预测会完全失效。此时需要兜底的"实时流量感知"机制——通过Redis的MONITOR命令采样TopKey,检测到异常访问后触发即时预热。
场景二:小数据量排行榜。如果排行榜只有几万条数据,三层分离反而是过度设计。单机Redis + AOF足够,引入MySQL和AI预热引擎只会增加运维复杂度。决策阈值建议:单个排行榜数据量 < 10万且QPS < 1000时,单层Redis即可。
场景三:跨赛季查询。"这个玩家上赛季什么排名?"这类查询会穿透到MySQL冷层。虽然做了分区裁剪,但跨赛季的复合查询(如"近3个赛季都进过Top100的玩家")仍然需要扫描多个分区。建议对这类低频但重要的查询建立物化视图,按天刷新。
场景四:热度预热的精度衰减。LSTM模型的效果依赖于特征的时效性。赛季前期的预测准确率可能只有65%,到赛季末期随着数据积累可以提升到85%以上。在初期阶段,应以规则预热为主,逐步切换到模型预热。
场景五:Redis Cluster的resharding成本。赛季更替时需要迁移大量数据。建议将赛季维度编码到key中(如rank:season:S25:hot),每个赛季使用独立的key空间,避免跨赛季的resharding。
场景六:AI引擎自身的性能开销。预热引擎需要定期访问MySQL提取特征和加载数据,本身会成为数据库的负载源。建议将预热操作限制在业务低峰期(凌晨2-5点),单次预热的MySQL连接数控制在5以内。
五、总结
游戏排行榜的存储问题本质是"冷热数据的时空分离"——不同热度的数据用不同成本的存储,不同时效性的数据用不同一致性保证。三层架构(Redis热层、Redis温层、MySQL冷层)解决的是当前态,AI预热引擎解决的是未来态——在流量洪峰到来之前,把冷数据提前搬到热层。
架构上真正体现功力的是降级路径的设计:Redis挂了降级到MySQL,预热失效降级到实时感知,模型不准降级到规则驱动。每一层都有退路,系统才能在极端场景下保持韧性。
在下一篇文章中,将探讨社交场景下的好友关系存储,看看图数据库在这个维度上是如何挑战关系型数据库的。
本文属于「行业场景与项目复盘」系列,聚焦游戏/社交场景的数据库架构实践。


