MySQL 玩家数据存储方案
概述
玩家数据最终落在 MySQL,但"几百万玩家 × 几十个字段"的单表方案撑不住在线游戏。本文给出 MySQL 存储的完整方案:分表策略(Uid Hash / 时间范围)、冷热分离、读写分离,以及 ShardingSphere 集成方式,让数据层从第一天就具备水平扩展能力。
一、为什么需要分表
1.1 单表瓶颈
千万级玩家的单表:
索引体积大,内存放不下 → 磁盘随机 IO
写入热点集中(在线玩家同时落库)
备份、迁移、运维成本高
游戏场景的特殊性:
玩家数据按 playerId 天然隔离 → 完美分片维度
99% 查询都带 playerId → 分片后命中率高
→ "按玩家分表"是游戏数据层的核心策略1.2 分表 vs 分库
| 维度 | 分表 | 分库 |
|---|---|---|
| 粒度 | 表拆分,单库多表 | 库拆分,多实例 |
| 作用 | 单表数据量降低 | 分散 IO 与连接 |
| 难度 | 低(同库) | 中(跨库事务复杂) |
| 游戏实践 | 先分表 | 量级再大时分库 |
演进路径:
单表 → 分表(同库) → 分库分表(多实例)
每一步都要求分片键设计从一开始就正确二、分表策略
2.1 Uid Hash 分表
规则:表名 = player + (playerId % 表数)
例如 64 张表:player_0 ... player_63
优点:
数据均匀分布
路由计算简单:playerId % 64
缺点:
扩表困难(2 倍扩容需迁移)
按时间范围查询跨表
适合:玩家主表、背包、好友等按玩家访问的数据路由实现:
table = "player_" + (playerId & 63) // 2 的幂取模用位运算
分表数选 2 的幂,便于位运算与后续扩容2.2 时间范围分表
规则:表名 = 业务表 + 日期/月份
如 login_log_202701、currency_log_202701
优点:
按时间归档与清理方便
热数据集中在近期表
缺点:
单表可能不均匀(活动期暴涨)
跨时间查询多表 union
适合:日志、流水、邮件等"只增不改、按时间查"的数据2.3 混合策略
混合示例:
玩家主数据:Uid Hash(player_0~63)
流水日志:时间分表(currency_log_按月)
好友关系:Uid Hash(friend_0~63)
原则:
按访问模式选策略:
随机访问(按玩家查)→ Uid Hash
顺序访问(按时间扫)→ 时间分表2.4 分片键选择
分片键 = playerId(90% 场景)
理由:所有核心查询都带 playerId
反例:
按 nickname 分片 → 无法按 playerId 定位,绝对禁止
跨分片查询 → 避免(如全服扫背包统计)三、冷热分离
3.1 冷热定义
热数据:在线玩家数据、近期活跃玩家数据
特征:频繁读写、必须秒级访问
存储:内存 + 热表(最近 30 天活跃)
冷数据:长期不登录玩家数据、历史流水
特征:极少访问、需要保留(合规/找回)
存储:冷表 / 归档库 / 对象存储3.2 冷热迁移
判定标准:
按最后登录时间:超过 N 天未登录 → 转冷
按数据大小:超大背包 → 拆分冷存储
迁移流程:
定期任务扫描活跃表 → 筛出冷玩家
移动到冷表(同构 / 归档格式)
保留索引指向,找回时按需恢复
找回流程:
玩家重新登录 → 命中冷数据标记
→ 从冷表加载 → 回迁热表 → 继续游戏收益:
热表数据量大幅下降,查询更快
冷数据低成本存储(压缩、对象存储)四、读写分离
4.1 为什么读写分离
游戏数据读写特点:
读多写少(查配置、查排行、查他人资料)
但玩家自身数据"写多读少"(每局结算都写)
→ 读写分离主要服务于:排行榜、资料查询等读密集场景
读写分离结构:
主库:承担写 + 实时读
从库:承担读(延迟同步,容忍秒级延迟)4.2 实现方式
客户端(应用层)方案:
配置主从数据源,读写路由
优点:可控性强
缺点:应用要处理主从延迟
中间件方案(ShardingSphere / MyCat):
数据源层自动路由
读写分离 + 分库分表一站式4.3 主从延迟处理
延迟问题:
写主库后立即读从库 → 可能读不到
对策:
强一致场景(自己刚改的数据)强制走主库
弱一致场景(排行、推荐)允许读从库
从库延迟监控告警五、ShardingSphere 集成
5.1 为什么选 ShardingSphere
ShardingSphere-JDBC:
以 JDBC 驱动形式嵌入应用,无独立部署
支持分库分表 + 读写分离 + 分布式事务
Java 生态友好,配置化接入5.2 分片配置示例
yaml
# application.yml
spring:
shardingsphere:
datasource:
names: ds0, ds1
ds0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://host0:3306/game?useSSL=false
ds1:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://host1:3306/game?useSSL=false
rules:
sharding:
tables:
player:
actual-data-nodes: ds$->{0..1}.player_$->{0..63}
table-strategy:
standard:
sharding-column: player_id
sharding-algorithm-name: player-inline
key-generate-strategy:
column: player_id
key-generator-name: snowflake
sharding-algorithms:
player-inline:
type: INLINE
props:
algorithm-expression: player_${player_id % 64}
key-generators:
snowflake:
type: SNOWFLAKE配置要点:
actual-data-nodes:声明实际库表
sharding-column:player_id
分片算法:取模/哈希
主键:雪花算法生成,避免自增跨表冲突5.3 代码无感
java
// 配置后,普通 DAO 代码零改动
public interface PlayerMapper {
@Select("SELECT * FROM player WHERE player_id = #{playerId}")
Player findByPlayerId(@Param("playerId") long playerId);
}ShardingSphere 自动完成:
路由到正确的库表
解析 SQL 改写(表名替换)
结果归并
应用层完全无感六、索引与 SQL 规范
6.1 索引设计
| 表 | 索引 | 说明 |
|---|---|---|
| player | PK(player_id) | 主键即分片键 |
| player_bag | PK(uid), idx(player_id) | 按玩家查背包 |
| player_friend | PK(player_id, friend_id) | 组合主键 |
| currency_log | idx(player_id, create_time) | 玩家流水时间序 |
规范:
查询必须带分片键(否则全分片扫描)
避免非分片键查询(如按 nickname 全局查)→ 加映射表
流水表按时间建索引,配合时间分表6.2 批量与事务
批量落库:
定时批量 update(减少连接与锁竞争)
用 ON DUPLICATE KEY / REPLACE 处理重复
事务边界:
玩家单模块操作 → 单表单行,天然短事务
跨模块(货币+道具+流水)→ 同一玩家分片内事务
跨玩家/跨分片事务 → 尽量拆解成异步补偿七、监控与运维
需要监控:
分片热点(某分片数据/连接不均)
慢查询(跨分片扫描、缺索引)
主从延迟
连接池使用率
日常运维:
分表扩容预案(2 倍扩容迁移流程)
冷数据归档任务执行情况
备份恢复演练八、常见问题
| 问题 | 处理 |
|---|---|
| 分片键选错 | 规划期定死 playerId,杜绝后改 |
| 跨分片 join | 拆分查询,应用层归并 |
| 扩表迁移 | 灰度双写 → 校验 → 切换 |
| 主从延迟读到旧值 | 强一致走主库 |
| 冷数据找回慢 | 预加载 + 异步回迁 |
九、小结
MySQL 存储方案的核心是按 playerId 分片:玩家主数据用 Uid Hash 分表(64 张起步,2 的幂便于位运算与扩容),流水日志用时间分表便于归档;冷热分离让在线数据保持轻量,长期不活跃数据低成本归档;读写分离服务排行榜等读密集场景,强一致读强制走主库。接入 ShardingSphere-JDBC 后,分库分表、读写分离、雪花主键全部配置化,业务代码保持普通 DAO 写法。牢记一条纪律:所有核心查询必须带分片键,这是分片架构能不能长期跑稳的分水岭。