客户端在弱网下重试是正常行为,玩家连续点击领取按钮也是正常行为,网关在超时后重发上游请求同样正常。真正不正常的是服务端把这些正常现象处理成重复扣钻、重复发奖或重复释放技能。2021 年很多项目开始从“接口返回快一点”转向“命令语义稳定一点”,命令去重也从支付接口的专属能力,变成游戏服务器的基础设施。
核心判断
- 命令去重不是简单查 requestId,而是要结合玩家、业务、序列和结果缓存
- 高频战斗命令与低频资产命令的去重窗口不同,不能用一套表硬套
- 重复请求应该尽量返回第一次处理结果,而不是粗暴报错
架构示意
sequenceDiagram
participant C as 客户端
participant G as 网关
participant D as 去重层
participant S as 业务服务
participant R as 结果缓存
C->>G: commandId + payload
G->>D: 检查玩家命令窗口
D->>R: 查询已完成结果
alt 首次命令
D->>S: 执行业务
S-->>D: 业务结果
D->>R: 保存结果摘要
else 重复命令
R-->>D: 返回首次结果
end
D-->>G: 稳定响应
G-->>C: response
为什么 requestId 不够
很多团队第一次做去重,会要求客户端每次请求带 requestId,然后服务端用 Redis setnx 挡住重复。这只能解决最表层的问题。游戏命令里更常见的麻烦是客户端重连后序列重置、玩家多端登录产生旧命令、网关超时但业务已经成功、业务成功但响应丢失。单独的 requestId 不知道这个命令属于哪个会话,也不知道它是否和上一次命令语义相同,更不知道应该返回什么结果。因此命令去重要把 playerId、sessionId、commandSeq、commandType、payloadHash 和业务阶段一起看。
按命令类型分层
移动、转向、瞄准这类命令频率高、价值低、窗口短,可以用每玩家递增序列和滑动窗口处理,落后太多的命令直接丢弃;释放技能、使用道具、购买商品、领取奖励价值高,必须保存结果摘要,重复请求要返回同一个结果;聊天、点赞、上报日志则更适合基于内容指纹做短时间去重,避免刷屏但不要求永久幂等。把所有命令都放进一张去重表,会让高频命令拖垮存储,也会让关键命令缺少足够审计信息。
命令编号的生成责任
命令编号最好由客户端和服务端共同约束。客户端生成本地递增 commandSeq,服务端在登录时下发 sessionEpoch。最终命令键可以是 playerId + sessionEpoch + commandSeq。这样旧客户端离线缓存的命令不会污染新会话,多端登录时也能被 fencing。对于资产类命令,还要增加业务键,比如 orderId、rewardBatchId、mailId。因为玩家可能对同一个奖励入口点两次,如果只有 commandSeq,重装客户端后很难判断这是不是同一笔业务。
结果缓存比重复报错更重要
重复请求最理想的响应不是 duplicate error,而是第一次执行的结果。玩家点击购买后响应丢了,客户端重试,如果服务端只返回“重复请求”,客户端仍然不知道商品是否到账。去重层应该保存必要的结果摘要:状态码、资产变更版本、获得物品列表、展示用文案 key、后续跳转信息。结果缓存不需要保存完整响应里的所有字段,但必须足够让客户端收敛到正确 UI。缓存过期时间按业务价值设置,支付发货可能保留数天,普通奖励领取保留几小时,战斗技能只保留几十秒。
正在执行中的重复命令
最容易漏掉的是 in-flight 状态。第一个命令刚进入业务服务,还没写结果,第二个重复命令已经来了。此时不能简单放行,也不能永久等待。常见做法是去重层写入 processing 状态和短租约,后续重复请求等待一小段时间后查询结果;如果租约超时,需要进入 reconcile 流程,检查业务侧是否已经提交。资产类命令尤其要避免 processing 租约过短,否则慢 SQL 或下游抖动会导致第二个请求误判为首次执行。
和业务事务的边界
去重记录与业务变更最好在同一个原子边界内完成,做不到时至少要有可恢复的顺序。比如领取奖励,先写去重 processing,再执行业务事务,在事务内写奖励流水和去重完成记录。若 Redis 去重而数据库发奖,两者之间没有事务,就必须通过业务流水唯一键兜底。否则 Redis 丢数据或主从切换时,重复发奖会绕过第一道防线。服务端架构里,去重层是体验优化,最终的资产唯一性仍然应该由账本或流水约束保护。
压测与故障演练
命令去重要专门压测重复率,而不是只压测 QPS。可以构造 10% 超时重试、1% 双端旧会话、5% 响应丢失、Redis 延迟抖动等场景,观察重复响应是否稳定、去重窗口内存是否可控、processing 是否堆积。线上则重点看 duplicate hit、processing timeout、reconcile count、result cache miss、business unique violation。命令去重做得好,玩家不会感知它;做得不好,事故复盘里一定会出现“接口已经做过幂等,但还是重复了”这句话。
工程落地表
| 关注点 | 推荐做法 | 常见风险 |
|---|---|---|
| 状态边界 | 明确权威服务、缓存副本和可恢复事实 | 把运行态散落在多个服务里,故障时无法判断谁说了算 |
| 版本控制 | 给协议、配置、策略和数据结构都记录版本 | 发布后新旧逻辑交错,排查时无法复现 |
| 失败补偿 | 每个跨服务步骤都设计超时、重试和幂等结果 | 成功路径能跑通,异常路径留下脏状态 |
| 观测指标 | 指标贴近玩家体验,同时保留技术细分维度 | 只有机器指标,事故发生时不知道玩家卡在哪 |
| 演练方式 | 用脚本制造重试、掉线、超时、重启和版本不一致 | 只在测试服点几次正常流程,线上第一次遇到边界 |
一个可执行的落地步骤
第一步,不急着重构所有代码,而是把 命令去重 的关键事件和状态列出来,形成一张状态表。表里至少要有事件来源、状态 owner、是否可重试、是否需要持久化、失败后谁补偿。很多团队会在这一步发现,线上所谓的随机故障其实是状态没有 owner。
第二步,先在边界处加版本和审计。即使内部实现暂时没改,只要每次请求、每次状态转换、每次跨服务调用都能留下版本、原因和结果,后续迭代就有依据。不要等事故后再补日志,那时最关键的上下文已经丢了。
第三步,挑一条高价值路径做闭环,例如登录进房、领取奖励、切换场景或活动开启。闭环要包含成功、重复、超时、失败、回滚和人工处理。只要一条路径跑通,团队就能把模式复制到其他路径。
第四步,把演练自动化。命令去重 的风险大多不会在正常点击里出现,而是在进程重启、网络抖动、配置切换、客户端重试、下游超时的组合里出现。自动化演练不需要一开始很复杂,能稳定复现三五个最危险场景,就已经比靠人工记忆可靠。
复盘问题清单
- 玩家在最差网络条件下,是否仍然能得到明确结果,而不是一直转圈?
- 服务重启或发布时,是否有清晰的进入、等待、迁移和退出策略?
- 重复请求、延迟响应和旧会话消息是否会污染新状态?
- 关键决策是否能通过日志复现,包括输入、版本、策略和输出?
- 如果下游服务短暂不可用,当前架构是保护玩家体验,还是把错误直接扩散到客户端?
- 运维或客服是否有安全的人工介入入口,还是只能直接改数据库?
在实际落地 命令去重 时,团队还需要把责任边界写进代码和文档。第 1 个容易被忽略的点,是不要让临时判断散落在调用方。调用方只表达意图,平台层给出明确结果,业务层再根据结果决定是否继续。这样做看似多了一层接口,后续排查却非常省时间:日志能说明哪个版本的策略参与了决策,指标能看到哪个阶段开始变慢,回滚时也能只回滚策略而不是重启整组服务。对于游戏服务器来说,很多架构问题最终都会落到玩家体验上,稳定的边界比聪明的捷径更重要。
在实际落地 命令去重 时,团队还需要把责任边界写进代码和文档。第 2 个容易被忽略的点,是不要让临时判断散落在调用方。调用方只表达意图,平台层给出明确结果,业务层再根据结果决定是否继续。这样做看似多了一层接口,后续排查却非常省时间:日志能说明哪个版本的策略参与了决策,指标能看到哪个阶段开始变慢,回滚时也能只回滚策略而不是重启整组服务。对于游戏服务器来说,很多架构问题最终都会落到玩家体验上,稳定的边界比聪明的捷径更重要。
在实际落地 命令去重 时,团队还需要把责任边界写进代码和文档。第 3 个容易被忽略的点,是不要让临时判断散落在调用方。调用方只表达意图,平台层给出明确结果,业务层再根据结果决定是否继续。这样做看似多了一层接口,后续排查却非常省时间:日志能说明哪个版本的策略参与了决策,指标能看到哪个阶段开始变慢,回滚时也能只回滚策略而不是重启整组服务。对于游戏服务器来说,很多架构问题最终都会落到玩家体验上,稳定的边界比聪明的捷径更重要。
在实际落地 命令去重 时,团队还需要把责任边界写进代码和文档。第 4 个容易被忽略的点,是不要让临时判断散落在调用方。调用方只表达意图,平台层给出明确结果,业务层再根据结果决定是否继续。这样做看似多了一层接口,后续排查却非常省时间:日志能说明哪个版本的策略参与了决策,指标能看到哪个阶段开始变慢,回滚时也能只回滚策略而不是重启整组服务。对于游戏服务器来说,很多架构问题最终都会落到玩家体验上,稳定的边界比聪明的捷径更重要。
在实际落地 命令去重 时,团队还需要把责任边界写进代码和文档。第 5 个容易被忽略的点,是不要让临时判断散落在调用方。调用方只表达意图,平台层给出明确结果,业务层再根据结果决定是否继续。这样做看似多了一层接口,后续排查却非常省时间:日志能说明哪个版本的策略参与了决策,指标能看到哪个阶段开始变慢,回滚时也能只回滚策略而不是重启整组服务。对于游戏服务器来说,很多架构问题最终都会落到玩家体验上,稳定的边界比聪明的捷径更重要。
总结
游戏服务器命令去重架构设计 的重点不在于堆更多组件,而在于把状态、时间、版本和失败路径讲清楚。游戏服务器的复杂度通常不是来自单个算法,而是来自玩家行为、网络环境、运营动作和服务故障同时发生。一个可信的架构,应该让正常路径足够顺,让异常路径有边界,让每一次自动处理和人工介入都有证据可查。做到这一点,系统即使不能避免所有问题,也能把问题限制在可理解、可恢复、可继续迭代的范围内。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。