游戏客户端离线与重试:不要让按钮点击变成请求风暴

讨论弱网、离线、请求重试、幂等和本地队列,帮助客户端在不稳定网络下保持可控体验。

弱网下最怕重复点击

玩家点领取奖励,按钮没反应,于是又点几次。网络恢复后,客户端把几次请求一起发出去,服务端有的成功有的失败,界面状态乱成一团。很多线上问题不是网络断了造成的,而是客户端在网络不稳定时没有控制住用户意图。

请求要分类

不是所有请求都适合重试。查询类请求通常可以重试,比如拉邮件列表、刷新商店、获取排行榜。提交类请求要谨慎,比如购买、抽卡、消耗道具、领取奖励。实时战斗输入则更特殊,过期后重试没有意义。

请求类型例子重试策略
查询邮件列表、角色信息可自动重试,显示加载状态
幂等提交领取指定任务奖励可带 requestId 重试
非幂等提交抽卡、购买礼包必须服务端支持幂等,否则不自动重试
实时输入移动、技能方向超时丢弃或等待状态同步

requestId 是救命绳

提交类请求如果要支持重试,必须有客户端生成的 requestId 或服务端生成的事务 ID。玩家点击一次领取奖励,客户端生成一个唯一 ID,超时后用同一个 ID 重试。服务端看到重复 ID,就返回同一结果,而不是再次发奖励。

UI 要表达正在提交

很多重复请求来自 UI 没有状态。按钮点下去后仍然可点,玩家自然会继续点。提交类按钮应该进入 pending 状态,显示轻量反馈,并禁止重复点击。

重试要退避

自动重试不能每帧发一次。常见策略是指数退避:第一次 1 秒后重试,第二次 2 秒,第三次 4 秒,并设置最大次数。网络恢复事件可以触发一次立即重试,但也要防止所有请求同时冲出去。

小结

离线和重试处理的是不确定性。客户端要承认网络会断、响应会丢、玩家会重复点击,也要通过请求分类、幂等 ID、队列上限、按钮状态和退避策略把不确定性关在笼子里。

上线前的复盘清单

离线与重试最后容易输在细节。团队可以在提测前做一次十五分钟复盘:入口是否只有一个,失败路径是否能被重复触发,日志里是否能看到关键上下文,弱网、低内存、切后台、热更新后首次进入这些场景是否有人真正跑过。清单不需要很长,但要能挡住最常见的事故。

第一项是边界。哪些状态属于客户端暂存,哪些必须等服务端确认,哪些只是表现层效果,要写在需求文档或接口说明里。第二项是恢复。玩家断网、杀进程、锁屏、切换账号、更新资源后回来,客户端应该回到哪个画面,是否会重复扣道具或重复弹奖励。第三项是可观测。没有日志、没有埋点、没有版本号和配置号,线上问题只能靠猜。第四项是降级。低端机、老资源包、灰度配置错误时,系统能否退到朴素但可用的路径。

请求治理不是为了把代码写得保守,而是为了让客户端在真实环境里少一点脆弱。玩家不会按测试用例玩游戏,他会在地铁里切网络,在战斗结算前接电话,在更新到一半时锁屏,也会在礼包倒计时最后几秒连续点击。能承受这些动作的系统,通常不是靠某个聪明函数撑起来的,而是靠清楚的状态、稳定的数据、可回放的日志和足够朴素的失败处理撑起来的。

和策划、美术、服务端对齐

很多离线与重试问题表面看是客户端实现,根上却是协作边界没有说清楚。策划需要知道哪些反馈可以立即出现,哪些反馈必须等待权威结果;美术需要知道资源尺寸、动画事件、特效峰值和加载时机的预算;服务端需要知道客户端会缓存什么、重试什么、放弃什么。只要这些假设没有写下来,后续迭代就会靠口头记忆运转。

比较有效的做法是把一页协作说明放在需求旁边,列出输入、输出、失败处理和验收方式。比如资源类需求要写明包体归属、依赖关系、是否允许边玩边下;战斗类需求要写明本地预演和服务端确认的差异;UI 类需求要写明列表规模、刷新频率和关闭后的状态保留。说明越具体,返工越少。

上线后也要保留一条反馈通道。客服截图、玩家录像、崩溃堆栈、埋点漏斗和灰度数据都能帮助团队判断问题在哪一层。客户端工程师不应该只等 bug 单,而要主动把现象翻译成可定位的问题:是资源缺失、状态跳转错误、请求重复、表现未降级,还是需求本身给了互相冲突的规则。

一个容易忽略的成本

离线与重试还有一个成本是新人理解成本。项目越到中后期,真正危险的不是某个类多了两百行,而是没人能说清一次完整流程经过哪些模块。新同事接手时,如果只能靠全局搜索和断点追踪,很容易在修一个小问题时改坏另一条路径。

因此我更偏向把关键流程画出来,并在代码里保留少量稳定的命名:状态名、事件名、错误码、资源阶段名尽量和文档一致。这样排查问题时,日志、配置、代码和运营后台看到的是同一套语言。语言统一以后,团队讨论会短很多,也更少出现“我以为你说的是另一个状态”的误会。

这类维护成本不会在第一周显现,但会在每次版本合入、每次活动复用、每次紧急修复里持续计息。早一点把结构讲清楚,后面就少一点靠资深同学记忆救火的依赖。

上线前的复盘清单

离线与重试最后容易输在细节。团队可以在提测前做一次十五分钟复盘:入口是否只有一个,失败路径是否能被重复触发,日志里是否能看到关键上下文,弱网、低内存、切后台、热更新后首次进入这些场景是否有人真正跑过。清单不需要很长,但要能挡住最常见的事故。

第一项是边界。哪些状态属于客户端暂存,哪些必须等服务端确认,哪些只是表现层效果,要写在需求文档或接口说明里。第二项是恢复。玩家断网、杀进程、锁屏、切换账号、更新资源后回来,客户端应该回到哪个画面,是否会重复扣道具或重复弹奖励。第三项是可观测。没有日志、没有埋点、没有版本号和配置号,线上问题只能靠猜。第四项是降级。低端机、老资源包、灰度配置错误时,系统能否退到朴素但可用的路径。

请求治理不是为了把代码写得保守,而是为了让客户端在真实环境里少一点脆弱。玩家不会按测试用例玩游戏,他会在地铁里切网络,在战斗结算前接电话,在更新到一半时锁屏,也会在礼包倒计时最后几秒连续点击。能承受这些动作的系统,通常不是靠某个聪明函数撑起来的,而是靠清楚的状态、稳定的数据、可回放的日志和足够朴素的失败处理撑起来的。

和策划、美术、服务端对齐

很多离线与重试问题表面看是客户端实现,根上却是协作边界没有说清楚。策划需要知道哪些反馈可以立即出现,哪些反馈必须等待权威结果;美术需要知道资源尺寸、动画事件、特效峰值和加载时机的预算;服务端需要知道客户端会缓存什么、重试什么、放弃什么。只要这些假设没有写下来,后续迭代就会靠口头记忆运转。

比较有效的做法是把一页协作说明放在需求旁边,列出输入、输出、失败处理和验收方式。比如资源类需求要写明包体归属、依赖关系、是否允许边玩边下;战斗类需求要写明本地预演和服务端确认的差异;UI 类需求要写明列表规模、刷新频率和关闭后的状态保留。说明越具体,返工越少。

上线后也要保留一条反馈通道。客服截图、玩家录像、崩溃堆栈、埋点漏斗和灰度数据都能帮助团队判断问题在哪一层。客户端工程师不应该只等 bug 单,而要主动把现象翻译成可定位的问题:是资源缺失、状态跳转错误、请求重复、表现未降级,还是需求本身给了互相冲突的规则。

一个容易忽略的成本

离线与重试还有一个成本是新人理解成本。项目越到中后期,真正危险的不是某个类多了两百行,而是没人能说清一次完整流程经过哪些模块。新同事接手时,如果只能靠全局搜索和断点追踪,很容易在修一个小问题时改坏另一条路径。

因此我更偏向把关键流程画出来,并在代码里保留少量稳定的命名:状态名、事件名、错误码、资源阶段名尽量和文档一致。这样排查问题时,日志、配置、代码和运营后台看到的是同一套语言。语言统一以后,团队讨论会短很多,也更少出现“我以为你说的是另一个状态”的误会。

这类维护成本不会在第一周显现,但会在每次版本合入、每次活动复用、每次紧急修复里持续计息。早一点把结构讲清楚,后面就少一点靠资深同学记忆救火的依赖。

上线前的复盘清单

离线与重试最后容易输在细节。团队可以在提测前做一次十五分钟复盘:入口是否只有一个,失败路径是否能被重复触发,日志里是否能看到关键上下文,弱网、低内存、切后台、热更新后首次进入这些场景是否有人真正跑过。清单不需要很长,但要能挡住最常见的事故。

第一项是边界。哪些状态属于客户端暂存,哪些必须等服务端确认,哪些只是表现层效果,要写在需求文档或接口说明里。第二项是恢复。玩家断网、杀进程、锁屏、切换账号、更新资源后回来,客户端应该回到哪个画面,是否会重复扣道具或重复弹奖励。第三项是可观测。没有日志、没有埋点、没有版本号和配置号,线上问题只能靠猜。第四项是降级。低端机、老资源包、灰度配置错误时,系统能否退到朴素但可用的路径。

请求治理不是为了把代码写得保守,而是为了让客户端在真实环境里少一点脆弱。玩家不会按测试用例玩游戏,他会在地铁里切网络,在战斗结算前接电话,在更新到一半时锁屏,也会在礼包倒计时最后几秒连续点击。能承受这些动作的系统,通常不是靠某个聪明函数撑起来的,而是靠清楚的状态、稳定的数据、可回放的日志和足够朴素的失败处理撑起来的。

和策划、美术、服务端对齐

很多离线与重试问题表面看是客户端实现,根上却是协作边界没有说清楚。策划需要知道哪些反馈可以立即出现,哪些反馈必须等待权威结果;美术需要知道资源尺寸、动画事件、特效峰值和加载时机的预算;服务端需要知道客户端会缓存什么、重试什么、放弃什么。只要这些假设没有写下来,后续迭代就会靠口头记忆运转。

比较有效的做法是把一页协作说明放在需求旁边,列出输入、输出、失败处理和验收方式。比如资源类需求要写明包体归属、依赖关系、是否允许边玩边下;战斗类需求要写明本地预演和服务端确认的差异;UI 类需求要写明列表规模、刷新频率和关闭后的状态保留。说明越具体,返工越少。

上线后也要保留一条反馈通道。客服截图、玩家录像、崩溃堆栈、埋点漏斗和灰度数据都能帮助团队判断问题在哪一层。客户端工程师不应该只等 bug 单,而要主动把现象翻译成可定位的问题:是资源缺失、状态跳转错误、请求重复、表现未降级,还是需求本身给了互相冲突的规则。

一个容易忽略的成本

离线与重试还有一个成本是新人理解成本。项目越到中后期,真正危险的不是某个类多了两百行,而是没人能说清一次完整流程经过哪些模块。新同事接手时,如果只能靠全局搜索和断点追踪,很容易在修一个小问题时改坏另一条路径。

因此我更偏向把关键流程画出来,并在代码里保留少量稳定的命名:状态名、事件名、错误码、资源阶段名尽量和文档一致。这样排查问题时,日志、配置、代码和运营后台看到的是同一套语言。语言统一以后,团队讨论会短很多,也更少出现“我以为你说的是另一个状态”的误会。

这类维护成本不会在第一周显现,但会在每次版本合入、每次活动复用、每次紧急修复里持续计息。早一点把结构讲清楚,后面就少一点靠资深同学记忆救火的依赖。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页