已解决:Martinit‑Kit:Typescript 运行时,在用户之间同步状态的多人应用/游戏
Source: Dev.to
请提供您希望翻译的正文内容,我将为您翻译成简体中文并保留原有的格式、Markdown 语法以及技术术语。
执行摘要
TL;DR: 多人应用经常因网络延迟和竞争条件而遭遇实时状态同步问题,导致客户端不同步。本文介绍了 Martinit‑Kit,一个 TypeScript 运行时,主要通过 权威服务器模型 实现稳健的状态同步,为所有连接的用户建立唯一的真相来源。
问题
- 网络延迟 是多人应用中状态不同步的根本原因。
- 当多个用户同时尝试修改相同状态时,延迟会导致 竞争条件。
权威服务器模型
- 行业标准,用于稳健的状态同步。
- 服务器充当 唯一真相来源,验证意图,并向所有客户端广播官方状态更改。
乐观 UI
- 通过即时更新客户端屏幕来提升感知性能。
- 风险: 如果服务器拒绝更改,会出现突兀的回滚。最适合冲突较少的操作。
事件溯源
- 提供所有状态更改操作的 不可变日志。
- 提供完整的审计追踪和历史回放能力。
- 对于复杂系统而言,这是一项重大的架构转变。
为什么会痛苦
“我永远不会忘记 Project Chimera 的演示。我们正在向副总裁们展示我们新的协作设计工具。一切都很顺利,直到两位高管同时尝试移动同一个组件。在一台屏幕上它向左卡住;在另一台屏幕上它向右移动。随后,整整十秒钟,它在两个位置之间闪烁,随后彻底消失在数字虚空中。那间屋子的寂静……震耳欲聋。”
共享状态不是一个特性;它是一场 分布式系统的终极Boss战。
简单演练
- User A 点击按钮 → 客户端向
api‑gw‑01发送“更改状态”消息。 - 该消息的传输耗时约 ≈ 80 ms。在这 80 毫秒内,世界仍在转动。
- User B 尚未收到 User A 的更新,点击了另一个会修改同一状态的按钮。他们的消息现在正处于传输途中。
- 服务器收到了 两个冲突的指令。
- 谁会获胜?
- 最后一个会覆盖第一个吗?
- 如果第一个更重要怎么办?
没有唯一、无可争议的真相来源和明确的冲突解决规则,客户端必然会逐渐脱节,导致混乱、困惑,以及在 VP 演示期间组件消失。
常见解决方案(从“伪装成功”到全规模架构)
1. 乐观 UI – “伪装成功”
对感知性能非常有帮助。思路是立即更新用户自己的界面,假设服务器会同意。
// 超简化伪代码
function handleMoveButtonClick(itemId: string, newPosition: Position) {
// 1️⃣ 立即更新我们自己的 UI。感觉很快!
const previousPosition = updateLocalItemPosition(itemId, newPosition);
// 2️⃣ 告诉服务器我们做了什么。
api.sendItemMove(itemId, newPosition)
.catch(error => {
// 3️⃣ 哎呀——服务器拒绝了!回滚我们的乐观更改。
console.error("Move rejected by server:", error);
updateLocalItemPosition(itemId, previousPosition); // 跳回去!
showErrorToast("Couldn't move the item.");
});
}
专业提示: 这会让用户感受到“魔法般”的快速响应,但如果服务器拒绝更改(例如权限问题或冲突),UI 元素会“跳回”原来的位置。仅在冲突概率低的操作中使用。
2. 权威服务器 – “成年”方案
在此模型中,客户端是 愚笨的:它永远不决定最终状态,只发送 意图 给服务器。服务器是 唯一真相来源。
流程:
-
用户 点击 “向左移动项目”。
-
客户端 发送一条消息,例如:
{ "action": "MOVE_INTENT", "itemId": "abc-123", "direction": "left" }UI 可能会显示一个加载指示,但它 不会 立即移动项目。
-
服务器(例如
game‑state‑worker‑03)接收意图,进行验证,检查冲突,并在内存或像 Redis 这样的高速缓存中更新规范状态。 -
服务器 将新的、官方的状态广播给 所有 已连接的客户端(包括发起者)。
-
所有客户端 接收新状态并渲染。每个人都保持完美同步,因为他们都是服务器的镜像。
像 Martinit‑Kit 这样的框架专门为简化此模式而构建。它们处理 WebSocket、状态广播和调和的样板代码,让你专注于服务器端逻辑——这才是关键所在。
3. 事件溯源 – 当“当前状态”不足以应对时
有时状态逻辑如此复杂,仅存储 当前状态 并不够。你需要知道 它是如何得到的。
引入事件溯源。 与其存储最终结果,不如在不可变日志中记录每一次操作(事件)。
示例 – 银行账户:
| 事件 | 数据 |
|---|---|
ACCOUNT_CREATED | initialBalance: $0 |
DEPOSIT_MADE | amount: $100 |
WITHDRAWAL_MADE | amount: $50 |
当前余额($50)通过 回放 这些事件计算得出。这种做法提供了完美的审计能力和重建任意过去状态的可能性,但代价是增加了架构复杂度。
选择合适的方法
| 情境 | 推荐策略 |
|---|---|
| 快速原型 / 演示 | 乐观 UI(带有明确的回滚处理) |
| 生产级多人游戏应用 | 权威服务器 + 类似 Martinit‑Kit 的库 |
| 复杂领域逻辑,需要审计追踪 | 事件溯源(通常与权威服务器结合使用) |
最后思考
- Latency ≠ Instantaneous – 互联网并非瞬时;请为延迟做好设计。
- Single Source of Truth – 防止漂移和竞争条件。
- Clear Conflict Rules – 提前决定如何解决冲突意图。
通过理解分布式系统的底层物理原理并选择合适的架构,你可以把“分布式系统终极对决”变成一个可管理、可预测的过程。同步愉快!
Warning: 不要轻视这条路径。这是一次根本性的架构转变。它需要不同的思维方式和工具(如 Kafka 或专用事件存储)。对合适的问题它极其强大,但它并不是简单聊天应用的快速解决方案。
那么,哪种方案最适合你?
| 方案 | 实现速度 | 用户体验 | 稳健性 / 可扩展性 |
|---|---|---|---|
| Optimistic UI | 快 | 非常响应(但可能出现“跳动”) | 低(易出现竞争条件) |
| Authoritative Server | 中等(框架有帮助) | 良好(操作有轻微延迟) | 高(行业标准) |
| Event Sourcing | 慢(工作量大) | 良好(与权威模型相同) | 极高(复杂但强大) |
我的建议?
先采用 Authoritative Server 模型。它在可靠性和实现工作量之间达到了最佳平衡。寻找能让你更快实现的工具。如果你的应用感觉迟缓,可以在非关键操作上加入一些 Optimistic UI。如果你的应用发展成下一个 Google Docs,那就是个好问题——可能是时候研究 Event Sourcing 了。
现在去构建一些酷炫的东西,尽量不要让 VP(版本发布)把它弄坏。
👉 阅读原文请访问 TechResolve.blog
☕ 支持我的工作
如果本文对你有帮助,你可以请我喝咖啡:
👉