数据持久化策略
概述
游戏数据常驻内存换取性能,但内存会丢——进程崩溃、断电、发布重启都会造成损失。持久化策略解决"内存数据何时、以何种方式安全落库":定时落盘 + 事件触发 + 下线强制三管齐下,配合离线清理、备份与恢复,在性能与安全之间取得平衡。
一、为什么不能每次都落库
内存操作:微秒级
数据库写:毫秒级 + 连接 + 锁
每局每步都落库:
延迟拉高(结算要写几十字段)
数据库成为瓶颈(写放大)
锁竞争(同一玩家并发写)
结论:写路径必须"批量 + 时机化"1.1 持久化的三要素
一致性:玩家数据不丢、不错
性能:不拖垮在线响应
可控:停机/崩溃损失在可接受范围二、入库时机设计
2.1 三种时机
| 时机 | 场景 | 特点 |
|---|---|---|
| 定时落盘 | 在线玩家周期保存 | 批量高效,有丢失窗口 |
| 事件触发 | 关键操作即时保存 | 精确,防重要数据丢失 |
| 下线强制 | 玩家退出/掉线 | 必保存,收尾保障 |
组合策略:
常规数据:定时批量(如 30s)
关键数据:事件触发(货币、道具增减)
生命周期:下线/掉线强制落库2.2 定时落盘
实现:
定时任务(如 30s 一次)扫描在线玩家
只写"脏"玩家(有变更的)
批量更新,一条玩家一行 SQL
脏标记:
Player 对象加 dirty 标志
任一字段变更 → 置脏
落库成功 → 清除
优点:写压力小、稳定
缺点:丢失窗口 = 定时周期(30s 内的操作可能丢)java
public class PlayerSaveService {
private static final long SAVE_INTERVAL = 30_000L; // 30s
public void tick() {
for (GameSession session : sessionManager.onlineSessions()) {
Player player = session.getPlayer();
if (player.isDirty()) {
playerDao.update(player);
player.clearDirty();
}
}
}
}2.3 事件触发
哪些操作必须立即落库:
货币变动(充值、消费)→ 涉及资金
道具变动(获得/消耗稀有道具)→ 防回档
关键状态变更(段位晋级、活动领取)→ 防重复发放
每日首次登录(活跃统计)→ 防漏
实现:
统一在"数据变更服务"里判断是否需要即时保存
同一批变更(货币+道具+流水)事务一起提交事件触发的成本控制:
只对关键数据即时写
结合异步队列,把写请求异步化
用"操作日志 + 对账"兜底(见后文)2.4 下线强制保存
下线流程(正常/掉线):
玩家数据 → 强制落库(无论是否脏)
清理会话与内存对象
为什么必须强制:
掉线/崩溃后玩家重新登录要读到完整数据
定时落盘可能还没来得及写入最后操作掉线保存注意:
掉线 ≠ 立即清理(宽限期等待重连,见会话篇)
数据仍保留在内存,宽限期内可无缝恢复
超宽限期 → 落库 + 清理三、丢失窗口与补偿
3.1 丢失窗口评估
丢失窗口 = 定时落盘周期内的变更
30s 周期 → 最多丢 30s 的操作
对货币/道具影响大 → 必须即时落库
可接受度分级:
低价值(昵称、浏览记录)→ 定时即可
高价值(货币、道具)→ 事件触发 + 流水3.2 操作流水兜底
为什么要有流水:
即时落库也可能失败(DB 抖动)
流水是"可回放的操作记录"
设计:
currency_log / item_log 追加写
落库失败 → 流水仍在 → 对账/重放
玩家数据 = 基础快照 + 流水回放
适用场景:
充值到账、活动发放等关键链路
全量回档恢复时用流水重放四、离线数据清理
4.1 清理对象
在线玩家长期不登录:
内存对象 → 释放(已落库)
Redis 缓存 → 过期
会话 → 删除
清理时机:
掉线超宽限期
定期扫描不活跃在线数据
服务器低峰期批量归档4.2 冷数据归档
长期未登录(如 30 天):
热表 → 冷表/归档库(见 MySQL 冷热分离)
定时任务执行,可配置阈值
找回时按需回迁4.3 清理的正确顺序
下线清理顺序(防数据丢失):
1. 强制落库(数据先安全)
2. 记录下线日志
3. 移除会话(SessionManager)
4. 释放内存对象
5. 清理 Redis 在线标记
落库成功才允许清理内存,顺序不可颠倒五、备份与恢复
5.1 备份策略
| 备份类型 | 频率 | 内容 |
|---|---|---|
| 全量备份 | 每日/每周 | 全库导出 |
| 增量备份 | 每小时 | binlog 增量 |
| 即时备份 | 操作前 | 关键运维操作前快照 |
游戏场景建议:
每日全量 + binlog 持续
备份保存多份(异地)
定期演练恢复(验证备份可用)5.2 恢复流程
故障恢复路径:
1. 恢复最近全量备份
2. 回放 binlog 到故障时刻
3. 丢失部分用操作流水补偿
4. 校验数据一致性(对账)
回档场景(事故/作弊回滚):
全体/指定玩家回到快照点
配合流水:找回回档期间的合法操作5.3 对账机制
对账目的:发现并修复不一致
对账维度:
内存 vs 数据库(在线玩家抽查)
流水 vs 余额(货币收支平衡)
Redis vs MySQL(缓存一致性)
实现:
低峰期离线对账 + 实时异常告警
发现差异 → 以权威数据为准修正六、发布与停机
发布停机(游戏更新):
优雅下线:停止接收 → 落库全部在线玩家 → 停服
发布后:玩家登录加载新版本数据
不停服热更(见运维章节):
配置热更:动态加载
逻辑热更:灰度节点
数据兼容:新增字段默认值优雅停机实现:
JVM shutdown hook:
停止 Netty 接收
通知在线玩家"即将维护"
强制落库全部在线玩家
关闭线程池与连接七、常见问题
| 问题 | 处理 |
|---|---|
| 进程崩溃丢 30s 数据 | 关键操作即时落库 + 流水兜底 |
| 落库失败静默 | 重试 + 告警 + 流水对账 |
| 掉线后数据没存 | 掉线强制落库流程 |
| 回档后数据错乱 | 快照 + 流水重放 + 对账 |
| 备份恢复耗时长 | 增量 + binlog + 演练预案 |
八、小结
数据持久化的核心是"分层时机 + 流水兜底 + 清理安全序":常规数据定时批量落盘控制写压力,货币/道具等关键数据事件触发即时入库,下线/掉线强制保存收尾;操作流水作为可回放日志,在落库失败或回档时兜底补偿。离线清理严格遵循"先落库、后清理"的顺序,备份采用"每日全量 + binlog 增量 + 定期演练"。这套策略把丢失窗口压到最小,同时避免数据库成为性能瓶颈,是"内存快 + 存储稳"这一架构哲学的具体实现。