# 游戏数据存储特殊需求
用生活化的比喻,让你从"只会用 MySQL"到"能设计支撑百万玩家的游戏数据存储方案"
前置知识:第06章 数据库深度进阶(B+树、MVCC、锁、分库分表)、第10章 高并发与分布式一致性(分布式锁、最终一致性)
阅读指南(初学者必看)
为什么你需要学习游戏数据存储?
游戏数据存储和普通 Web 应用完全不同:
- 玩家背包可能有 1000 个物品 → 怎么存?一条记录还是多条?
- 排行榜要实时更新 → MySQL 扛不住怎么办?
- 玩家日志海量 → 怎么存得省钱又查得快?
- 游戏服挂了 → 数据怎么不丢?
学完本章,你能回答:
- 游戏玩家数据怎么建模?(MySQL + Redis + MongoDB 的组合拳)
- 排行榜怎么设计?(Redis Sorted Set vs 数据库)
- 日志和埋点数据怎么处理?(ClickHouse vs Elasticsearch)
- 游戏数据怎么备份和恢复?(增量备份 + Binlog)
本文结构
第一部分:游戏数据存储架构(整体方案) 第二部分:玩家数据建模(核心数据) 第三部分:排行榜与社交数据(高频读写) 第四部分:日志与埋点(海量数据) 第五部分:数据备份与容灾(数据安全)
一、游戏数据存储架构
存储分层
` 游戏数据存储分层:
第一层:内存(Redis / 本地缓存)
- 在线玩家数据
- 排行榜、计数器
- 热点数据
- 延迟:< 1ms
第二层:关系型数据库(MySQL)
- 玩家基础数据
- 交易记录
- 需要事务的数据
- 延迟:1-10ms
第三层:文档数据库(MongoDB)
- 复杂结构数据(背包、配置)
- 日志、埋点
- 延迟:5-20ms
第四层:列式存储(ClickHouse / Elasticsearch)
- 大数据分析
- 日志检索
- 延迟:50-500ms `
架构图
游戏客户端 │ ▼ 网关服务器 │ ├──▶ Redis(在线数据缓存) │ └── 玩家状态、排行榜、计数器 │ ├──▶ MySQL(核心事务数据) │ └── 玩家账号、充值记录、交易日志 │ ├──▶ MongoDB(复杂结构数据) │ └── 玩家背包、邮件、配置 │ └──▶ ClickHouse(分析型数据) └── 运营报表、玩家行为分析
二、玩家数据建模
玩家核心数据(MySQL)
`sql -- 玩家基础表(小表,常驻内存) CREATE TABLE player ( player_id BIGINT PRIMARY KEY, account VARCHAR(64) NOT NULL UNIQUE, nickname VARCHAR(32) NOT NULL, level INT DEFAULT 1, exp BIGINT DEFAULT 0, gold BIGINT DEFAULT 0, diamond INT DEFAULT 0, vip_level INT DEFAULT 0, last_login_time TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_level (level), INDEX idx_last_login (last_login_time) ) ENGINE=InnoDB;
-- 分库分表:按 player_id % 16 分表 -- player_0, player_1, ..., player_15 `
玩家背包(MongoDB)
`javascript // MongoDB 文档 - 玩家背包 { _id: 10001, player_id: 10001, items: [ { item_id: 1001, count: 10, bind: false, expire_time: null }, { item_id: 2001, count: 1, bind: true, expire_time: null }, { item_id: 3001, count: 99, bind: false, expire_time: ISODate("2024-12-31") } ], capacity: 200, used_capacity: 3, updated_at: ISODate("2024-01-15") }
// 为什么用 MongoDB? // 1. 背包结构不固定(不同物品有不同属性) // 2. 嵌套数组适合文档模型 // 3. 读写性能高(不需要 JOIN) // 4. 扩容方便(水平分片) `
`java
// Java 操作 MongoDB
@Document(collection = "player_bag")
public class PlayerBag {
@Id
private Long playerId;
private List
@Autowired private MongoTemplate mongoTemplate;
// 查询背包 PlayerBag bag = mongoTemplate.findById(playerId, PlayerBag.class);
// 添加物品 Query query = new Query(Criteria.where("playerId").is(playerId)); Update update = new Update().push("items", new Item(itemId, count, false, null)); mongoTemplate.updateFirst(query, update, PlayerBag.class); `
缓存策略(Redis)
`java @Service public class PlayerDataService { @Autowired private RedisTemplate<String, Object> redisTemplate; @Autowired private PlayerRepository playerRepository; @Autowired private MongoTemplate mongoTemplate;
// 读数据:先查缓存,再查数据库
public Player getPlayer(long playerId) {
String key = "player:" + playerId;
Player player = (Player) redisTemplate.opsForValue().get(key);
if (player == null) {
player = playerRepository.findById(playerId).orElse(null);
if (player != null) {
redisTemplate.opsForValue().set(key, player, 30, TimeUnit.MINUTES);
}
}
return player;
}
// 写数据:先写数据库,再删缓存(Cache Aside)
@Transactional
public void updateGold(long playerId, long delta) {
playerRepository.updateGold(playerId, delta);
redisTemplate.delete("player:" + playerId);
}
} `
三、排行榜与社交数据
Redis 排行榜(Sorted Set)
`java @Service public class LeaderboardService { @Autowired private StringRedisTemplate redisTemplate;
// 更新玩家战力排行
public void updatePowerRank(long playerId, long power) {
String key = "rank:power:weekly";
redisTemplate.opsForZSet().add(key, String.valueOf(playerId), power);
// 设置过期时间(每周清零)
redisTemplate.expire(key, 7, TimeUnit.DAYS);
}
// 获取前 100 名
public List<RankEntry> getTop100() {
String key = "rank:power:weekly";
Set<ZSetOperations.TypedTuple<String>> top =
redisTemplate.opsForZSet().reverseRangeWithScores(key, 0, 99);
List<RankEntry> result = new ArrayList<>();
int rank = 1;
for (ZSetOperations.TypedTuple<String> entry : top) {
result.add(new RankEntry(rank++,
Long.parseLong(entry.getValue()),
entry.getScore().longValue()));
}
return result;
}
// 获取玩家自己的排名
public Long getPlayerRank(long playerId) {
String key = "rank:power:weekly";
Long rank = redisTemplate.opsForZSet().reverseRank(key, String.valueOf(playerId));
return rank != null ? rank + 1 : null;
}
// 获取玩家前后 10 名(附近的人)
public List<RankEntry> getNearbyPlayers(long playerId) {
String key = "rank:power:weekly";
Long rank = redisTemplate.opsForZSet().reverseRank(key, String.valueOf(playerId));
if (rank == null) return Collections.emptyList();
long start = Math.max(0, rank - 10);
long end = rank + 10;
Set<ZSetOperations.TypedTuple<String>> nearby =
redisTemplate.opsForZSet().reverseRangeWithScores(key, start, end);
// ... 转换逻辑
return result;
}
} `
社交数据(好友系统)
`java // Redis 好友关系(Set) public class FriendService { @Autowired private StringRedisTemplate redisTemplate;
// 添加好友
public void addFriend(long playerId, long friendId) {
String myKey = "friend:" + playerId;
String friendKey = "friend:" + friendId;
redisTemplate.opsForSet().add(myKey, String.valueOf(friendId));
redisTemplate.opsForSet().add(friendKey, String.valueOf(playerId));
}
// 获取好友列表
public Set<String> getFriends(long playerId) {
return redisTemplate.opsForSet().members("friend:" + playerId);
}
// 获取共同好友
public Set<String> getCommonFriends(long playerId1, long playerId2) {
return redisTemplate.opsForSet().intersect(
"friend:" + playerId1,
"friend:" + playerId2);
}
// 是否好友
public boolean isFriend(long playerId, long friendId) {
return redisTemplate.opsForSet().isMember(
"friend:" + playerId,
String.valueOf(friendId));
}
} `
四、日志与埋点
日志存储方案
` 日志类型与存储方案:
| 日志类型 | 存储方案 | 保留时间 | 查询场景 |
|---|---|---|---|
| 玩家行为日志 | ClickHouse | 90 天 | 运营分析、留存计算 |
| 游戏战斗日志 | MongoDB | 30 天 | 战斗回放、举报审核 |
| 系统错误日志 | Elasticsearch | 30 天 | 错误追踪、告警 |
| 充值消费日志 | MySQL | 永久 | 财务对账 |
| 性能监控日志 | Prometheus | 15 天 | 性能分析 |
| ` |
ClickHouse 玩家行为表
`sql -- ClickHouse 建表 CREATE TABLE player_behavior ( event_time DateTime, player_id UInt64, event_type String, event_data String, level UInt16, vip_level UInt8, server_id UInt16 ) ENGINE = MergeTree() PARTITION BY toYYYYMMDD(event_time) ORDER BY (event_type, player_id, event_time);
-- 查询 DAU(日活跃用户) SELECT toDate(event_time) as date, uniqExact(player_id) as dau FROM player_behavior WHERE event_type = 'login' GROUP BY date ORDER BY date;
-- 查询留存率 SELECT date, d1_retention, d7_retention, d30_retention FROM retention_table; `
日志采集架构
游戏服务器 → Filebeat(日志收集)→ Kafka(消息队列)→ ├── Logstash → Elasticsearch(错误日志) ├── Flink → ClickHouse(行为分析) └── 直接归档 S3(冷数据)
五、数据备份与容灾
备份策略
` 备份分层:
实时备份
- MySQL 主从复制(Binlog)
- Redis AOF + RDB
- MongoDB Replica Set
定时备份
- 每天凌晨全量备份(MySQL dump)
- 每小时增量备份(Binlog)
- 每周 MongoDB dump
异地备份
- 跨区域复制(AWS S3 Cross-Region)
- 冷数据归档( Glacier / 阿里云 OSS 归档)
备份验证
- 每月恢复演练
- 自动备份完整性检查 `
MySQL 主从切换
`sql -- 1. 检查主库状态 SHOW MASTER STATUS; -- File: mysql-bin.000123 -- Position: 1234567
-- 2. 从库追平数据 -- 确保 Exec_Master_Log_Pos = 主库 Position
-- 3. 从库提升为主库 STOP SLAVE; RESET SLAVE ALL;
-- 4. 应用连接新主库 -- 修改配置中心的数据源
-- 5. 原主库修复后作为新从库 CHANGE MASTER TO MASTER_HOST='new_master_host', MASTER_USER='repl', MASTER_PASSWORD='password', MASTER_LOG_FILE='mysql-bin.000123', MASTER_LOG_POS=1234567; START SLAVE; `
游戏数据容灾设计
` 容灾等级:
RTO(恢复时间目标):
- 核心业务(登录、充值):RTO < 5 分钟
- 游戏玩法:RTO < 30 分钟
- 数据分析:RTO < 4 小时
RPO(恢复点目标):
- 充值数据:RPO = 0(实时同步)
- 玩家数据:RPO < 5 分钟(Binlog)
- 日志数据:RPO < 1 小时(可接受部分丢失) `
自问自答
Q1:为什么背包用 MongoDB 不用 MySQL?
背包数据结构复杂且不固定(不同物品有不同属性),MongoDB 的文档模型天然适合。MySQL 需要多个表 JOIN,性能差且 schema 变更困难。
Q2:Redis 排行榜怎么保证数据不丢?
定期(如每 5 分钟)将 Redis 排行榜数据持久化到 MySQL。Redis 重启后从 MySQL 恢复基础数据,再接收新的实时更新。
Q3:ClickHouse 和 Elasticsearch 怎么选?
ClickHouse 适合结构化数据的聚合分析(留存、付费率),查询快。Elasticsearch 适合文本搜索和日志检索。游戏运营分析用 ClickHouse 更合适。
Q4:游戏数据怎么删除玩家数据(GDPR 合规)?
逻辑删除:标记 deleted=true,保留必要记录(如充值记录用于财务)。物理删除:90 天后清理非必要数据。必须记录删除操作日志。
Q5:怎么处理跨服数据迁移?
玩家从 A 服迁到 B 服:1)锁定玩家账号;2)导出数据(MySQL + MongoDB + Redis);3)导入 B 服;4)更新路由表;5)解锁账号。整个过程需要在维护时间窗口进行。
实践任务
- 设计玩家数据模型:MySQL 基础表 + MongoDB 背包 + Redis 缓存
- 实现排行榜服务:Redis Sorted Set + 定时持久化到 MySQL
- 实现好友系统:Redis Set + 共同好友查询
- 搭建日志采集:Filebeat + Kafka + ClickHouse,统计 DAU
- 数据备份脚本:MySQL 全量 + 增量备份,自动上传到 OSS
- 容灾演练:模拟主库宕机,完成主从切换
与其他章节的关联
| 本节内容 | 关联章节 | 关联点 |
|---|---|---|
| MySQL 建模 | 第06章 数据库深度进阶 | 索引、分库分表、事务 |
| Redis 缓存 | 第10章 高并发与分布式一致性 | 缓存策略、分布式锁 |
| MongoDB | 第06章 数据库 | 文档数据库的适用场景 |
| 日志采集 | 第14章 游戏实时通信优化 | 性能监控数据来源 |
| 容灾 | 第15章 云原生与容器化 | K8s 的 StatefulSet 持久化 |
上一章:12-游戏协议设计与优化 | 下一章:14-游戏实时通信优化