可靠性、顺序与兼容性
Category
networking / 传输语义。节点定位:把可靠性、顺序、兼容性拆成三种独立语义分别决策。
Definition
“可靠送达”“顺序正确”“跨版本兼容”这三个词经常被一起提,但它们不是一件事。可靠不代表有序,有序不代表兼容,兼容更不意味着业务语义不会被误用。把这三件事混在一起,是很多网络设计和协议治理问题的起点。
Problem
可靠、有序、兼容三件事混着谈,「这条消息到底允许丢吗」就会变成各说各话——排障时的争执大多从这里来。先拆语义,再谈策略。
先把三种语义拆开
可靠性
关注的是消息最终有没有被送达、处理或确认。典型场景包括:
- 支付结果。
- 资产发放。
- 结算确认。
- 关键状态迁移。
顺序
关注的是消息以什么相对次序被消费。典型场景包括:
- 连续输入帧。
- 聊天消息。
- 房间事件序列。
- 同一对象的状态变化。
兼容性
关注的是不同版本客户端和服务端能否对同一条消息保持一致理解。典型场景包括:
- 灰度发布。
- 多端并行。
- 渠道包延迟更新。
- 热更新与旧包共存。
这三者会互相影响,但必须分开治理。
为什么不同消息要不同语义
同一个项目里,不同消息对这三种语义的需求差异很大:
- 角色资产变更必须可靠,通常还要幂等,顺序往往也重要。
- 位移同步更看重新消息,晚到的旧消息很多时候应直接丢弃。
- 聊天消息通常需要相对顺序,但不应该阻塞战斗主链路。
- 活动通知可以稍晚、可以重试,但不值得拖累高频交互。
如果把所有消息都套进同一语义,系统就会在不必要的地方付出额外时延和复杂度。
可靠性的工程问题不只是重发
很多人一想到可靠性,就想到“没收到就重发”。这只解决了一小部分问题。真正的可靠语义通常还包括:
- 有没有唯一请求 ID 或消息 ID。
- 重复到达时能否幂等处理。
- 服务端处理成功但回包丢了怎么办。
- 客户端超时重试会不会导致重复副作用。
也就是说,可靠性的核心不只是传输,而是最终处理语义是否可靠。
顺序也不是只有全局有序
顺序最容易被过度简化。真实项目里更常见的是:
- 同一连接内相对顺序。
- 同一对象或同一房间内局部顺序。
- 同一玩家操作流顺序。
并不是所有消息都需要全局总顺序。事实上,过度追求全局顺序往往会带来很高成本和没必要的阻塞。
例如:
- 同一个角色的移动更新需要局部顺序。
- 两个独立外围系统的通知未必需要互相保序。
- 新位置到了以后,旧位置更新完全可以作废。
兼容性不能靠传输层顺便解决
兼容性经常被错误地打包进“可靠协议”里。实际上它是更高层的契约治理问题,通常依赖:
- 版本协商。
- 字段默认值。
- 向前和向后兼容规则。
- 枚举扩展规则。
- 兼容窗口管理。
这和 TCP 还是 UDP 没有直接关系。传输层能送过去,不代表接收方理解得对。
常见的组合策略
成熟系统通常不会给所有消息统一上“可靠 + 有序 + 兼容”三件套,而是按类型组合:
- 关键控制消息:强调可靠、幂等、明确回执。
- 输入或状态消息:强调时效性,允许覆盖旧值或只要求局部顺序。
- 外围通知消息:强调最终送达,但不阻塞主链路。
- 跨版本高风险消息:强调协议治理和兼容窗口。
这种分层的好处是,系统不会在所有地方都背最高成本。
业务层、协议层、传输层该怎么分工
更现实的看法是:
- 传输层或协议层可以承载部分可靠和顺序语义。
- 业务层必须自己定义幂等、覆盖、过期和补偿规则。
- 兼容性治理通常属于协议契约和版本管理层。
如果这三层责任不分,最后就会出现一种局面:传输层觉得问题在业务层,业务层觉得问题在协议层,出了事故谁都解释不清。
一组更实用的判断问题
设计某条消息时,可以强制自己回答:
- 如果这条消息丢了,会发生什么。
- 如果它重复到了,会发生什么。
- 如果它晚到了,还要不要处理。
- 如果它和上一条顺序反了,会不会出事。
- 如果旧客户端收到它,会不会误解。
这几个问题答完,消息语义通常就清楚很多。
常见误区
- 要求“所有消息都可靠、都有序、都兼容”,结果代价极高且并不必要。
- 完全不区分语义,把问题留给业务代码补救,最终每个模块都发明一套自己的重试和幂等逻辑。
- 把兼容性误当成传输问题,结果新旧版本一并上线时整条链路一起抖。
这一页真正要强调的,不是三种概念的定义,而是复杂度必须分层,不同消息只应该背自己值得背的语义成本。
实战案例:一次「重连后道具多领了一份」的重复与顺序排查
案例为教学示例(非摘自真实项目),数字经过取整简化。它把本页「可靠性的工程问题不只是重发」「顺序也不是只有全局有序」「一组更实用的判断问题」用到深水区:传输层一直在重发,账却对不上——这是弱网期重复副作用与旧值覆盖的排查,也是「怎样给每类消息标注语义」的完整展开。
背景与现象:某放置养成手游,地铁、电梯场景重连高发。月末对账发现结算发奖流水比客户端到账记录多 217 笔,重复率 0.9%;同期「位置回退」「进度倒退」类投诉日 340 单。两类问题都集中在重连后的 10 秒窗口内。
第一步:现象与口径——先按「丢了、重复了、晚到了」分账
对账口径:发奖流水 24100 笔,客户端到账 23883 笔,差 217 笔;抽 20 笔重复样本比对,服务端每笔都收到了两次请求,处理了两次,发了两次奖。
幂等口径:重复的两笔请求内容完全一致,但服务端没有唯一请求 ID,无法区分「新请求」和「重试」——传输层忠实重发,业务层照单全收。
顺序口径:位置回退投诉的样本里,客户端先收到新位置、后收到旧位置,旧位置包直接覆盖了新位置——位移同步包没有时间戳,也没有覆盖规则。
第二步:分层归因——按本页三层分工逐层排查
月末差异账 217 笔重复 + 340 单回退 归因
回包丢失 + 客户端超时重试,业务层无幂等 217 笔重复的全部来源
位移包无版本号,旧值覆盖新值 340 单回退的全部来源
传输层可靠 ≠ 业务可靠:重发解决送达,不解决重复排查路径:传输层重发正常工作(该到的都到了);协议层也没有丢字段;问题出在业务层——发奖接口没有幂等表,位移同步没有局部顺序与覆盖规则。这正是本页说的「传输层觉得问题在业务层,业务层觉得问题在协议层,出了事故谁都解释不清」。
第三步:根因——可靠语义只做了一半,顺序语义没有做
可靠性只在传输层做了重发,业务层没有幂等:回包丢失后客户端超时重试,服务端把重试当新请求再发一次奖。顺序上追求的只是「都送到」,同一对象的局部顺序和晚到旧值的覆盖规则完全没有定义。两类缺口叠加,弱网重连窗口就成了重复与回退的高发区。
第四步:处置与回填——按本页五问给每类消息标注语义
- 发奖类消息补唯一请求 ID 与幂等表:重复到达直接返回首次处理结果,不再重复发奖;回包也带同一 ID,客户端凭 ID 对账。
- 位移与状态类消息带时间戳和序号:晚到的旧值直接丢弃,新值到达后旧值作废,按同一对象的局部顺序处理,不做全局排序。
- 用本页五问逐类标注语义:丢了会怎样、重复了会怎样、晚到还处理吗、反序会出事吗、旧端会误解吗——每类消息的答案写进协议评审表。
复核账:重复发奖 217 笔 → 0 笔;位置回退投诉日 340 单 → 12 单;对账差异率 0.9% → 0.01%;幂等表上线首月命中 3.1 万次重复请求,全部正确返回首次结果。
回填清单:
| 回填项 | 动作 | 回填处 |
|---|---|---|
| 唯一请求 ID 与幂等表 | 发奖、结算类消息带唯一 ID,服务端按 ID 幂等,重复到达返回首次结果 | 可靠性的工程问题不只是重发 |
| 状态覆盖规则 | 位移与状态包带时间戳和序号,晚到旧值直接丢弃,按对象局部有序 | 顺序也不是只有全局有序 |
| 五问语义标注 | 协议评审表按丢、重、晚、反、旧五问逐类标注,答案随协议变更同步更新 | 一组更实用的判断问题 |
| 回执与补偿 | 关键资产消息明确回执语义:处理成功回包丢失时,凭 ID 查证补偿,不盲目重发 | 常见的组合策略 |
案例的三个教训
- 重发解决送达,不解决重复,幂等必须落在业务层。传输层一直在忠实工作,账还是错了——可靠性问的是「最终处理语义可靠」,不是「包发了多次」。
- 顺序按对象局部有序设计,全局有序的成本不值。位置回退的全部根因就是没有定义「新值到了旧值作废」——一条覆盖规则抵得过整套全局排序。
- 语义标注要在出事前做,五问就是最短的检查单。丢、重、晚、反、旧五问答完,发奖类和位移类的语义差异一目了然,不用等对账差异来教。
Used By
协议评审时给每类消息标注语义;结算与支付类消息的强可靠设计;帧同步输入流的顺序设计。
Related
上游:消息模式(05)、协议契约(04)。下游:帧同步与状态同步(server 树)。关系链:传输语义 → 同步模型 → 确定性。
Reference
本页为工程经验归纳;页内实战案例为教学示例(数字取整简化)。