Event Sourcing 在游戏中的应用
概述
传统存储只记"当前状态",Event Sourcing(事件溯源)只记"发生了什么":每个状态变更都是一个事件,当前状态由事件流回放得出。它的价值在于完整历史(可审计、可回放、可纠错)与状态可重建(故障恢复、新节点补状态)。本文介绍事件溯源在游戏中的应用:玩家操作事件流、回放恢复、CQRS 读写分离。
一、事件溯源模式
1.1 核心思想
传统模式:
状态表(余额=100)→ 直接覆盖更新
事件溯源:
事件流(+50、-30、+80)→ 状态=100(累加得出)
状态是事件流的投影,随时可重算游戏场景映射:
货币变动 → CoinChangedEvent(amount)
对局结果 → BattleFinishedEvent(result)
道具获得 → ItemAddedEvent(itemId, count)
每一条都是不可变事实1.2 事件要求
事件特性:
不可变(发生后不可修改)
有完整字段(谁/何时/什么/为何)
版本化(字段演进兼容旧事件)
幂等(同一事件重放结果一致)// 事件示例(不可变)
public final class CoinChangedEvent {
public final long playerId;
public final long delta;
public final String reason;
public final String bizId;
public final long timestamp;
}二、玩家操作事件流
2.1 事件流组织
事件流维度:
按玩家(playerId)组织:该玩家全部事件
按聚合(房间/公会)组织:该聚合的事件
存储:
event_store 表(或 Kafka 主题)
聚合根 + 事件序号(版本号)
事件追加写(只 insert,不 update)事件流与状态:
玩家余额查询 → 回放 CoinChangedEvent(慢)
优化:定期生成快照(snapshot),回放从快照起
快照 = 某个版本的状态 + 版本号2.2 游戏事件源应用
适用场景:
玩家资产(货币/背包)→ 事件流可审计
对局记录 → 可回放复盘
活动数据 → 可重算排名
不适用场景:
实时对局中间状态(帧同步高频)→ 快照更合适
大状态频繁变更 → 事件量爆炸
这类场景用"快照 + 增量事件"混合混合策略(推荐):
高频对局 → 定时快照 + 关键事件
低频资产 → 全事件流 + 定期快照三、事件回放恢复状态
3.1 回放机制
回放流程:
1. 加载最近快照(version = N)
2. 读取 N 之后的事件
3. 依次 apply 到状态
4. 得到最新状态
应用:
玩家重连 → 回放恢复玩家 Actor 状态
节点故障 → 新节点回放恢复房间状态
数据错误 → 修正事件后重新回放// 回放示例
public PlayerState restore(long playerId) {
PlayerState state = snapshotStore.load(playerId); // 快照
List<Event> events = eventStore.readFrom(playerId,
state.getVersion());
for (Event e : events) {
state.apply(e); // 按事件推进状态
}
return state;
}3.2 一致性保证
事件应用要求:
同一聚合内事件有序(版本号递增)
幂等(重复应用同事件结果一致)
事件不可变(错误修正 = 追加补偿事件)
并发冲突:
两写竞争 → 版本号冲突检测(乐观锁)
冲突方重读事件流重新应用四、CQRS 读写分离
4.1 模式介绍
CQRS(命令查询职责分离):
写侧(Command):只接收命令、追加事件
读侧(Query):只读状态/投影,独立优化
好处:
读写独立扩展
读侧可自由构建投影(榜单/报表)
写侧简单(只追加)游戏映射:
写侧:玩家操作 → 校验 → 追加事件
读侧:排行榜/背包查询 → 投影表/缓存
读侧从事件流异步构建4.2 投影(Projection)
投影定义:
事件流 → 派生数据(读模型)
示例:
CoinChangedEvent → 余额投影表
BattleFinishedEvent → 排行榜投影
ItemAddedEvent → 背包投影
实现:
消费者消费事件 → 更新投影
投影可重建(从事件流重算)
读侧延迟(秒级)可接受// 投影消费者示例
public void onCoinChanged(CoinChangedEvent e) {
balanceProjection.add(e.playerId, e.delta);
}4.3 与游戏架构结合
写入路径:
玩家请求 → 校验 → 追加事件(写侧)
读取路径:
查询 → 读投影/缓存(读侧)
实时对局 → 走 Actor 内存状态(不落投影)
一致性权衡:
强一致场景(扣款)→ 事件落库 + 同步投影
弱一致场景(榜单)→ 异步投影(秒级延迟)五、实现要点
设计清单:
状态变更全部事件化(不可变、版本化)
按聚合组织事件流 + 定期快照
回放恢复状态(快照 + 增量)
读写分离(命令写事件、查询读投影)
高频中间状态用快照兜底
工程注意:
事件表只追加(不更新/删除)
事件版本兼容(字段演进)
投影消费幂等(offset 管理)
事件量大 → 分区 + 归档常见坑:
事件被修改 → 回放结果不一致(不可变红线)
快照与事件不一致 → 版本号校验
投影重复消费 → 幂等写
事件溯源滥用 → 高频大状态别用与其他系统衔接:
持久化 → 数据持久化章节
Actor 恢复 → Actor 生命周期章节
流水审计 → 货币/道具流水
跨服同步 → 消息队列章节