QUIC 0-RTT 让首包更快,也可能把写操作发两次:HarmonyOS 7 重放边界怎么守
弱网启动时启用 QUIC 0-RTT,首页数据更早回来,但一次“领取权益”却偶发执行两次。0-RTT 的价值是减少握手等待,不代表早期数据天然只执行一次。只读查询和有副作用写操作如果走同一策略,性能优化就可能变成业务重复。本文不讨论抽象协议名词,而是给出应用侧可以复现和验收的重放边界。
验证范围:官方页面用于确认 HarmonyOS 7/API 26 的能力方向;文章代码只验证应用侧状态、预算或一致性模型。当前电脑是 API 24 SDK,且没有连接 HarmonyOS 7 真机,所以下面的断言不是 API 26 编译、设备性能或线上效果证明。
先用同一请求重放两次
构造同一个业务键,在连接恢复或握手回退时连续发送两次。服务端分别记录传输请求 ID、业务幂等键和提交结果。如果第二次请求创建了新的订单、预约或权益记录,就证明“连接层重试”越过了业务边界。不要只看客户端最终收到一个成功。
案例一:商品列表允许早期重放
首页列表和公开配置是只读查询。即使服务器处理两次,也不会改变业务状态,可以作为 0-RTT 候选,但仍要限制响应缓存和用户数据范围。
案例二:创建预约必须等确认或带幂等键
预约、支付前置、发送消息都可能产生副作用。要么禁止早期数据,要么携带服务端可验证的幂等键,并让重复请求返回同一业务结果。
安全关键在业务语义而不是 HTTP 方法
连接层只知道字节是否需要重发,不理解“这段数据会不会创建第二份业务记录”。把所有 POST 禁掉过于粗糙,把所有请求都放开又过于危险。真正的分类依据是可重放性、身份上下文和副作用。
type RequestPolicy = { idempotencyKey?: string; sideEffect: boolean; authenticated: boolean }; function allowEarlyData(p: RequestPolicy): boolean { if (p.authenticated && p.sideEffect) return false; if (p.sideEffect && !p.idempotencyKey) return false; return true; } if (!allowEarlyData({ sideEffect:false, authenticated:false })) throw new Error('只读请求被误拦截'); if (allowEarlyData({ sideEffect:true, authenticated:true, idempotencyKey:'order-1' })) throw new Error('敏感写操作不应进入早期数据');这段模型用确定输入验证“敏感写操作不能因为追求首包速度进入 0-RTT”。它适合先发现边界错误,但不会冒充平台接口或设备性能测试。
三种门禁放到一张表
| 方案 | 适用范围 | 主要风险 |
| 全部使用 0-RTT | 公开只读接口 | 写操作重放 |
| 按 HTTP 方法判断 | 接口规范非常严格 | 方法不能代表业务副作用 |
| 按业务策略声明 | 正式应用 | 需要维护请求元数据 |
我会采用业务策略声明:请求定义里明确只读、身份和副作用。高风险写操作等握手确认;低风险且具备幂等键的操作也要以服务端账本为准。
把早期数据策略收进请求描述
封装NetworkRequestPolicy,网络层只根据策略决定是否允许早期数据、是否附带幂等键以及失败后能否自动重试。页面不直接控制 0-RTT。
上线前做一次受控重放
- 同一业务键重放两次只产生一份业务结果。
- 只读请求能区分缓存命中和服务端重复处理。
- 身份令牌不会进入不合适的早期数据。
- 握手回退不会触发页面二次提交。
- 日志记录策略和业务键,不记录令牌原文。
QUIC 优化应该缩短等待,不该改变业务次数。先为请求写清副作用,再决定是否走 0-RTT,比“全开后观察”可靠得多。
官方资料
- 2026 年 8 月开发者月刊:网络预连接与 QUIC
- HarmonyOS 7 新能力
- HarmonyOS 应用开发指南