战斗结算是游戏服务器里最不能含糊的流程之一。玩家打完一场战斗,服务器要发经验、金币、道具、积分、任务进度,还可能触发活动、排行榜、公会贡献和成就。结算成功时,玩家立即感知到奖励;结算失败时,玩家会立刻投诉;结算重复时,问题会变成资产事故。很多服务端问题可以稍后修,重复发奖通常没有这么从容。
重复结算的来源比想象中多。客户端没收到返回会重试,网关超时可能重发,房间服上报结算时网络抖动可能发送两次,结算服务处理到一半崩溃后任务系统可能重放消息。分布式系统里,“同一个请求只来一次”不是可靠假设。结算服务的第一原则应该是:请求可以到达多次,结果只能生效一次。
唯一标识是基础。每场战斗应该有 battle_id,每次结算应该有 settlement_id。battle_id 标识这一场玩法实例,settlement_id 标识这一次结算请求。对于大多数玩法,一个 battle_id 只允许一个成功结算;对于分阶段奖励的玩法,可以允许多个 settlement_id,但每个阶段必须明确。没有唯一标识,后面的幂等都无从谈起。
结算服务收到请求后,应该先查 settlement_id 状态。如果已经 completed,就返回之前保存的结算结果;如果正在 processing,可以返回稍后查询或等待;如果 failed,要看失败是否可重试;如果不存在,才创建结算记录。不要先发奖励再写状态,那样服务崩溃时最难恢复。状态记录应该尽早创建,哪怕后续失败,也留下可追踪痕迹。
结算状态机要清楚。常见状态包括 received、validated、rewarding、completed、failed。received 表示请求进入系统,validated 表示战斗结果通过校验,rewarding 表示正在发放奖励,completed 表示全部完成。failed 也要记录失败原因和可否重试。状态机不是形式主义,它决定了服务重启后能不能知道该从哪里继续。
奖励发放本身也必须幂等。结算服务防重复不代表资产服务可以放松。每一次货币增加、道具发放、任务推进都要带上 source_type 和 source_id,例如 battle_reward:battle_id:player_id:item_id。资产服务看到同一个 source_id 第二次到来时,应该返回已处理结果,而不是再次入账。多层幂等会让系统在异常重试时更稳。
战斗结果不能完全相信客户端。结算请求应来自权威房间服或战斗服,包含参战玩家、开始时间、结束时间、胜负原因、关键统计、随机种子或回放摘要。结算服务可以做基础合理性校验:战斗是否存在,玩家是否属于该战斗,时长是否过短,奖励是否超过配置上限,关卡是否在活动时间内。校验不一定要重算整场战斗,但要挡住明显异常。
配置版本也要进入结算记录。战斗开始时使用哪个关卡配置、掉落配置、活动加成配置,结算时就应该按哪个版本处理。否则玩家开局时是双倍掉落,结算前活动结束,服务器如果读取当前配置,就可能少发奖励。房间服创建战斗时绑定配置版本,结算服务按版本读取奖励规则,这是比较稳妥的做法。
客户端返回设计也很重要。玩家结算时网络断开,重新登录后应该能查询这场战斗的最终奖励。服务端可以提供按 battle_id 查询结算结果的接口,客户端展示已生成的结果,而不是重新触发结算。很多重复发奖事故来自“客户端没收到结果,于是又发起一次结算”。查询和执行必须分开。
如果结算过程中部分奖励成功、部分失败,系统要能处理。比如金币已经入账,道具服务暂时不可用。此时不能简单把整次结算标记 failed 后重跑,否则金币可能重复。更好的方式是把每类奖励拆成子流水,记录每个子项状态。重试时只处理未成功的子项,已经成功的子项直接跳过。
奖励发放后的通知也要幂等。结算可能触发邮件、红点、任务推送、排行榜更新。即使奖励只发一次,通知重复也会让客户端表现混乱。通知可以根据结算版本或事件 ID 去重。对于不关键的异步通知,可以允许至少一次投递,但消费者必须能处理重复事件。
日志要能串起完整链路。一条战斗结算应能看到 battle_id、settlement_id、room_server、player_id、配置版本、奖励明细、资产流水、任务流水、排行榜事件和最终状态。玩家说奖励没到时,开发能快速判断是结算没生成、资产没入账、邮件没领取,还是客户端展示没刷新。没有链路日志,结算问题会非常难查。
结算任务还要考虑并发。同一场战斗的两个结算请求同时到达,数据库唯一键或分布式锁必须能挡住。不要只在应用内存里做判断,因为请求可能落到不同实例。最可靠的是让 settlement_id 或 battle_id 在持久层有唯一约束,应用层判断只是优化体验。
最终,战斗结算的可靠性来自一个朴素原则:所有外部调用都可能失败,所有请求都可能重复,但玩家资产变化必须只发生一次。把这个原则落实到结算记录、状态机、资产流水、配置版本和查询接口里,系统才能在真实线上环境里保持可信。
结算记录应该保存最终响应
幂等不仅是“不重复执行”,还包括“重复请求返回一致结果”。客户端第一次请求结算时,服务端完成发奖但响应丢失。客户端第二次请求,如果服务端只返回“已经结算”,客户端可能不知道奖励明细,展示就会出问题。更好的做法是在结算 completed 时保存最终响应,包括奖励列表、经验变化、任务推进、排行榜变化摘要。重复请求直接返回这份结果。
保存最终响应还能帮助重连。玩家战斗结束后断线,重新登录可以通过 battle_id 查询结算结果。客户端不需要重新走结算流程,只展示已完成结果。如果奖励通过邮件补发,也应该在响应里说明。这样玩家体验更连贯,服务端也避免重复触发复杂逻辑。
上游和下游都要防重复
结算服务通常处在中间位置。上游是房间服或战斗服,下游是资产、任务、活动、排行榜、邮件。只在结算服务做幂等不够,因为下游调用可能在结算服务崩溃后被重放。每个下游服务都应该识别 source_id。资产服务识别奖励流水,任务服务识别进度来源,排行榜服务识别积分事件,邮件服务识别补发来源。
这种设计看起来重复,但它是必要的纵深防御。线上事故往往发生在你以为不会失败的地方。某个下游超时了,其实已经处理成功;结算服务不知道,于是重试。如果下游没有幂等,就会重复生效。让每层都能安全处理重复请求,是分布式游戏服务的基本功。
失败恢复要避免“整单重跑”
结算失败时,最简单的想法是整单重跑。但如果前一次已经成功发了部分奖励,整单重跑就会有风险。更可靠的方式是把结算拆成可追踪子步骤:发货币、发道具、推进任务、增加活动积分、更新排行榜、发送通知。每个子步骤有独立状态和 source_id。恢复任务只处理未完成或明确失败的步骤。
当然,子步骤太细会增加复杂度。可以按资产风险分层。货币和稀有道具必须细粒度追踪;普通统计事件可以允许重复消费后由消费者去重;客户端通知可以重发。服务端要把精力放在资产和公平性最敏感的部分。
战斗结果校验的深度
不同游戏对结算校验要求不同。回合制和卡牌游戏可以在服务器完整模拟战斗,结算天然可信;实时动作游戏可能由房间服权威计算,结算服务只做结果校验;弱联网单机玩法可能需要客户端提交结果,服务端根据规则和风控判断是否可信。校验深度要和作弊收益匹配。
基础校验至少包括:玩家是否参加该战斗,战斗是否已经开始,结束时间是否合理,关卡是否开放,奖励是否超过上限,消耗是否已扣除,是否重复结算。高风险玩法还可以校验伤害曲线、技能释放次数、随机种子、回放 hash。服务端不一定每次都做最重校验,但要有抽样和异常复查能力。
结算和排行榜的关系
很多榜单积分来自战斗结算。这里要避免一个问题:战斗奖励已经发了,但排行榜积分更新失败。玩家看到奖励到账,却榜单没变化,会产生投诉。结算服务可以把排行榜更新作为子步骤,失败后进入重试队列。客户端结算页可以显示基础奖励,排行榜变化稍后刷新。只要最终一致,并且日志可查,体验可以接受。
对于限时榜,结算要检查战斗结束时间是否在活动窗口内。玩家在活动结束前开始战斗,结束时活动已结束,是否计入榜单?这个规则必须明确。常见做法是按战斗开始时间或结束时间之一判断,并写入活动规则。结算服务不能临时读取当前时间随意决定。
人工修复流程
再可靠的系统也会有异常。结算服务应该支持人工补发和修复,但修复也必须走流水。开发或客服不能直接改玩家资产表,而应该创建 correction 结算或补偿事件,记录原因、审批人和关联 battle_id。这样修复本身也可追踪、可去重、可审计。
如果需要撤销错误奖励,风险更高。撤销前要知道奖励是否已被消耗,消耗后是否允许扣成负数,是否需要邮件告知玩家。很多情况下,直接扣回不如发公告补偿更合适。技术系统提供能力,具体策略要结合玩家体验。
战斗结算的难点不是发奖励,而是让奖励在重试、故障、延迟、作弊和人工修复下仍然可信。只要坚持唯一标识、状态机、子流水、最终响应和多层幂等,重复发奖和漏发奖的风险会小很多。
上线前的工程核对
真正把这套方案放进生产环境前,团队还需要做一次朴素但有效的核对。第一,确认关键状态都有唯一标识,能从日志里串起一次完整链路。第二,确认重复请求不会造成重复副作用,尤其是资产、奖励、排名、邮件这类玩家能直接感知的结果。第三,确认配置、开关和版本都能回滚,而不是只能向前发布。第四,确认客服或运营能查到必要证据,避免所有问题都只能找开发临时查库。
还要准备一组小规模演练。演练不需要复杂,但要覆盖真实失败:服务重启一次,消息重复投递一次,下游接口超时一次,客户端重连一次,配置回滚一次。很多设计在文档里看起来可靠,只有演练时才会暴露状态缺失、错误码不清、日志字段不够、后台按钮不可用这些具体问题。把这些问题提前暴露出来,比在线上由玩家帮你测试要便宜得多。
最后,要把边界写进团队共识。哪些数据必须强一致,哪些可以最终一致;哪些操作允许重试,哪些必须人工确认;哪些异常直接降级,哪些必须停止入口。游戏服务器开发最怕每个模块都各自理解规则。规则统一后,代码实现、运营处理和客服解释才会站在同一条线上。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。