酒店搜索太慢,大多数团队最先想到的优化是「先给点东西看,别让页面空着」。他们把 API 拆成初始响应和后台轮询,用户立刻看到酒店列表,价格几秒后刷新,团队庆祝 perceived latency 降低了。然后用户在第 3 秒点进一家酒店,看到 120 美元,等预订页在第 8 秒加载完,价格变成了 98 美元——或者涨到了 145。用户不知道该刷新、该等,还是该相信自己眼睛。

流式搜索不是延迟优化,它是被分布在时间轴上的状态一致性问题。

增量状态的陷阱

传统阻塞式 API 有一个优势:响应是一个快照。每个字段在单一时间点上彼此一致。当你用增量更新替代它时,你用快照一致性换来了响应速度——这笔交易不一定对用户有利。

在多供应商搜索里,A 供应商 200ms 返回,B 在 1.2s,C 在 4s,D 在 8s 超时。前端收到四份独立的 payload,必须合并成一个连贯的视图。如果合并逻辑太 naive——收到就覆盖——用户看着价格来回跳,便宜报价不断替换贵的。如果合并逻辑太激进——过早隐藏无报价的酒店——用户眼睁睁看着目录在自己面前缩水。如果流式端点和下游 hotelRates 调用之间的会话状态没有同步,用户点进一家酒店,后端已经不认这个价格了。

行业默认把流式当成展示层的问题。后端只管流原始供应商事件,前端自己搞定 UX。这等于把最难的问题——增量状态一致性——推给了最不具备解决能力的层。

HotelByte 的时间边界一致性模型

HotelByte 的 SSE 流式架构把增量状态当作一等工程问题,而不是前端事后补丁。协议定义了四种事件类型——initialupdateerrorcomplete——每种都携带单调递增的序列号、毫秒级时间戳,以及从 HTTP header 贯穿到每个事件的统一 trace ID。

initial 事件在 200ms 内交付完整的酒店目录,包含缓存价格和静态内容。这不是占位符,而是一个完全可用的目录。无实时报价的酒店仍然可见,并带有「无报价」标识,避免了 naive 流式实现中常见的空结果降级。

随着供应商响应到达,update 事件携带特定批次的实时价格。前端把它们增量合并到内存中的 HotelStateMap,保留更新中未出现字段的现有状态,只在出现更便宜报价时才更新最低价。150 毫秒的 debounce 窗口把高频更新批量为单次状态转换,防止 React 重渲染抖动,同时不延迟最终的 complete 事件。

最关键的一致性保证是会话持久化。在每个 update 事件写入客户端之前,当前会话状态会被缓存。如果用户在价格出现后立即点进酒店,随后的 hotelRates 调用操作的是完全同步的会话。用户看到的价格,后端认账。

流式架构的代价

这套架构用严格快照一致性换取了响应速度。在 initial 事件和 complete 事件之间的任何时刻,展示的目录都是局部视图。酒店价格可能在用户看一眼和点一下之间发生变化。平台通过会话同步和缓冲取消策略来缓解,但根本取舍不变:用户看到的是移动靶。

还有运维复杂度。SSE 绕过了默认的 JSON 序列化路径,而后者通常会产生单条结构化日志。HotelByte 的解法是一个内部事件日志,记录每一个发出的事件——类型、供应商、凭证、批次索引、酒店数量、耗时——并在流结束时通过 GetLogData() 暴露。没有这一层,流式响应将成为审计黑洞。

白皮书阅读路径

Streaming Architecture 章节详细阐述了三层协调架构——SSE 协议、批次处理、前端状态——包含精确的事件 schema 和合并语义。Stream Lifecycle 章节完整走查了从请求接入到结构化日志的十个阶段。对运维人员而言,Auditability 章节解释了 GetLogData()、trace ID 关联和逐事件耗时如何让流式响应与标准 API 调用一样可被审计。

完整白皮书见 英文版中文版。全部白皮书索引参见 Whitepaper Index