Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第 8 章 低延迟与复杂读场景系统设计方法论:搜索、推荐、广告与 Feed

当系统面对搜索、推荐、广告投放、Feed 流、详情页这类“用户一打开就要马上看到结果”的场景时,设计重点不再只是数据对不对,而是如何在复杂查询、高 QPS 和持续变化的数据之间,把延迟、吞吐和结果质量一起控制住。

在上一章里,我们讨论了高准确性与强一致性系统的设计方法。那一类系统最关心的是“事实不能错”。
这一章讨论的是另一类非常典型、而且同样重要的系统问题:

结果可以不是全链路瞬时强一致,但必须足够快、足够稳、足够能支撑复杂查询与排序。

搜索、推荐、广告和 Feed 往往不会要求“每一条结果都和写库瞬时完全一致”,但它们对用户体验极其敏感:

  • 首屏慢 100 ms,点击率和转化率就可能下降
  • 排序抖动、召回不足、缓存失效,会直接影响 GMV 或广告收入
  • 高峰流量下,读链路一旦抖动,用户看到的就是白屏、空列表或结果乱序

因此这类系统的核心问题不是“如何做全局强一致”,而是:

如何围绕低延迟、高吞吐、复杂查询、多维排序和近实时更新,构建一条可扩展、可治理、可解释的读路径。

8.1 什么叫低延迟与复杂读场景

先看这类系统的共同特征。

8.1.1 用户对响应时间极其敏感

这类场景往往直接处在用户主路径上:

  • 搜索框输入后的结果页
  • 商品详情页
  • 首页 Feed 流
  • 广告召回与投放决策
  • 推荐卡片刷新

它们的共同目标通常是:

  • P99 延迟控制在 50-200 ms
  • 高峰下仍然保持稳定吞吐
  • 结果不能经常空、慢、乱、抖

8.1.2 查询模式比 OLTP 复杂得多

这类系统的读取通常不是“按主键查一条记录”,而是:

  • 多条件过滤
  • 多字段排序
  • 打分与召回
  • 聚合统计
  • 分页与游标
  • 个性化特征拼接

也就是说,它们面对的是复杂读,不是简单读。

8.1.3 读写压力不对称

很多这类系统都有一个鲜明特征:

  • 写入频率未必极高
  • 但读取频率极高,而且带有尖峰流量

例如:

  • 商品信息更新一天几次,但详情页每秒可能数十万次读取
  • 广告素材更新较慢,但广告检索和排序每秒大量请求
  • 酒店库存价格持续变化,但搜索请求量远远高于写请求

这决定了它们必须围绕读路径做专门设计。

8.1.4 结果质量和系统性能是耦合在一起的

搜索、推荐、广告和 Feed 的特别之处在于,性能问题不是纯技术问题,它直接影响业务结果:

  • 延迟过高会减少曝光和点击
  • 召回不足会降低交易转化
  • 排序不稳定会损害用户信任
  • 缓存策略不合理会带来陈旧结果或成本爆炸

所以这类系统不是“把数据读出来”就够了,而是要在延迟、吞吐、相关性、时效性、成本之间做平衡。

8.2 为什么不能套用强一致系统的设计思路

很多团队在做搜索、推荐或 Feed 时,会下意识沿用交易系统思路:所有读都回源数据库、写完立刻强一致可见、每次都现场实时计算。
这在低延迟复杂读场景里通常会迅速失效。

原因很简单:这类系统的主要矛盾不是“写入事实是否瞬时统一”,而是“复杂结果能不能在极短时间内稳定产出”。

8.2.1 直接回源数据库通常扛不住复杂读

如果把搜索、推荐、广告检索都直接落在交易数据库上,很快就会遇到:

  • 多条件查询慢
  • 排序成本高
  • 聚合和过滤打满数据库
  • 读流量挤压写流量

所以这类系统通常必须接受一个事实:

面向复杂读的系统,往往需要专门的数据组织方式,而不是直接复用交易库模型。

8.2.2 “每次实时算”通常扛不住延迟目标

如果每个请求都现算召回、特征、排序、拼接和过滤,结果往往是:

  • 请求路径过长
  • 下游依赖过多
  • P99 被拖高
  • 峰值时放大级联故障

所以成熟系统普遍会把一部分计算前移:

  • 预计算
  • 预聚合
  • 物化视图
  • 离线或准实时索引构建

8.2.3 读系统需要接受“近实时一致”而不是“瞬时全局一致”

搜索索引、缓存层、推荐候选池、广告特征库通常不会和源数据库绝对同时更新。
真正工程化的目标不是“完全零延迟一致”,而是:

  • 延迟窗口可控
  • 不一致范围可解释
  • 热门数据能优先收敛
  • 出错时能快速降级

这和上一章的强一致系统,是完全不同的设计哲学。

8.3 设计原则:先保护读路径,再治理一致性窗口

8.3.1 读写分离是起点,不是终点

在这类系统里,第一步通常都是把读模型从写模型里拆出来。

写模型负责:

  • 交易事实
  • 后台配置
  • 权威数据写入

读模型负责:

  • 检索
  • 聚合
  • 排序
  • 展示
  • 个性化查询

但读写分离只是开始。真正关键的是,读模型必须为读场景本身优化,而不是简单复制一份表。

8.3.2 为查询设计数据,而不是为存储设计数据

交易系统的建模往往围绕领域写入展开;
复杂读系统的建模则必须围绕查询模式展开。

例如:

  • 搜索需要倒排索引、过滤字段、排序字段
  • Feed 需要按用户、时间、权重组织候选集
  • 推荐需要召回池、特征视图、排序结果缓存
  • 详情页需要拼接好的展示视图和热点字段缓存

也就是说,低延迟系统里的数据结构,很多时候不是“最规范”,而是“最适合读”。

8.3.3 尽量把昂贵计算前移到写时或异步路径

这是复杂读系统最重要的原则之一。

如果一个结果可以提前准备,就不要在请求现场临时拼装。

典型做法包括:

  • 提前生成索引文档
  • 提前汇总统计结果
  • 提前构建用户候选集
  • 提前计算商品热度、质量分、CTR 预估特征
  • 提前准备详情页物化视图

这样做的本质是:

  • 把请求时延换成异步计算成本
  • 把复杂链路换成可治理的离线 / 准实时链路

8.3.4 多级缓存不是优化项,而是主设计对象

对搜索、推荐、广告、详情页来说,缓存通常不是“最后才加的加速器”,而是系统架构的一部分。

常见缓存层包括:

  • CDN / Edge Cache
  • 本地缓存
  • Redis 分布式缓存
  • 结果缓存
  • 特征缓存
  • 候选集缓存

设计重点不只是命中率,还包括:

  • 热点是否可控
  • 失效是否平滑
  • 是否会雪崩
  • 是否能防穿透
  • 陈旧数据是否在业务容忍范围内

8.3.5 延迟预算必须像资金预算一样被管理

一条低延迟读链路,不应该靠“感觉上挺快”来治理,而要把延迟拆解到各个阶段:

  • 网关预算
  • 检索预算
  • 特征读取预算
  • 排序预算
  • 拼装预算
  • 回退预算

只有这样,团队才能知道:

  • 慢在哪里
  • 哪一步最容易拖垮 P99
  • 哪些依赖必须预取、并行化或裁剪

8.4 方案选型:从搜索引擎、缓存到 CQRS 与向量检索

8.4.1 Elasticsearch / Meilisearch:复杂检索的主力

当场景涉及:

  • 关键词检索
  • 多维过滤
  • 相关性排序
  • 聚合统计
  • 近实时索引更新

这时专门的搜索引擎通常比直接查数据库更合适。

Elasticsearch 更适合复杂检索和大规模扩展;
Meilisearch 更轻量,适合中小型场景和更简单的搜索体验。

适用场景:

  • 商品搜索
  • 酒店搜索
  • 内容检索
  • 条件筛选页

限制:

  • 索引一致性不是瞬时强一致
  • 映射、分片、重建索引都有治理成本

8.4.2 Redis 多级缓存:高 QPS 热点读的基础设施

缓存最适合处理:

  • 高频重复读取
  • 热点详情
  • 热门查询结果
  • 会话级个性化上下文

常见组合是:

  • 本地缓存抗瞬时热点
  • Redis 抗跨实例热点
  • CDN 抗静态或半静态资源请求

适用场景:

  • 商品详情页
  • 首页配置
  • 热门榜单
  • 短时间窗口的搜索结果

限制:

  • 更新一致性难度高于读性能本身
  • 热点 key 和失效风暴需要专门治理

8.4.3 CQRS:把写模型和读模型彻底分开

当一个系统同时存在:

  • 复杂写入约束
  • 复杂读路径诉求

这时 CQRS 很有价值。

它的重点不是“时髦架构名词”,而是承认:

同一份业务数据,写入最优组织方式和读取最优组织方式往往不同。

适用场景:

  • 商品中心写入复杂,但搜索和详情读取压力大
  • 广告配置后台写入不频繁,但投放查询极高频
  • 推荐系统后台训练与线上读取完全不同节奏

8.4.4 Materialized View:详情页和聚合页的务实方案

如果页面最终展示逻辑相对稳定,但原始数据来源很多,那么物化视图通常是非常务实的解法。

适用场景:

  • 商品详情页
  • 酒店详情页
  • 店铺首页
  • 聚合展示页

优点:

  • 减少请求时拼装成本
  • 读路径更短
  • 更利于缓存

限制:

  • 视图更新链路需要治理
  • 变更传播延迟需要被业务接受

8.4.5 Fanout:Feed 与消息分发常见模式

在 Feed 场景里,常见问题是:

  • 是写时推送给所有用户
  • 还是读时按需拉取并排序

这就是经典的 Fanout-on-writeFanout-on-read 选择。

适用判断:

  • 关注关系稳定、读多写少,可偏向写扩散
  • 超大 V、热点内容多、关系图复杂,可偏向读扩散

成熟系统往往不是二选一,而是混合策略:

  • 普通用户走写扩散
  • 超级节点走读扩散

8.4.6 向量搜索:适合语义检索与召回,不适合作为唯一读模型

Milvus 及同类向量检索系统适合:

  • 语义搜索
  • 多模态召回
  • 推荐候选生成

但它通常不单独承担完整线上读链路,而是作为召回层的一部分,再和:

  • 规则过滤
  • 倒排检索
  • 精排模型
  • 缓存和特征层

一起组成完整方案。

8.4.7 一张务实选型表

场景特征推荐方案说明
关键词检索、多维过滤、相关性排序Elasticsearch / Meilisearch适合搜索主路径
热点高频读取、结果短期复用强Redis 多级缓存适合详情页、榜单、热门结果
读写模型差异大CQRS读写分离彻底化
展示数据需要多源拼装Materialized View缩短请求路径
Feed 大规模内容分发Fanout 混合策略平衡写放大和读放大
语义检索、候选召回向量搜索更适合作为召回层

8.5 以搜索、广告、推荐、Feed 和详情页为例

8.5.1 搜索:核心矛盾是召回、排序与时效性

搜索系统最常见的问题不是“查不到数据”,而是:

  • 查得慢
  • 排得不准
  • 新数据生效太慢
  • 过滤条件组合一多性能就崩

搜索的核心设计通常包括:

  • 检索索引单独维护
  • 热门过滤和聚合结果缓存
  • 索引近实时更新
  • 排序字段提前准备

8.5.2 广告:核心矛盾是时延预算极小,但决策逻辑很复杂

广告投放通常需要在极短时间内完成:

  • 候选召回
  • 预算过滤
  • 定向过滤
  • 出价与排序
  • 去重和频控

因此广告系统很依赖:

  • 特征预加载
  • 候选池缓存
  • 多层过滤前移
  • 精排计算预算控制

8.5.3 推荐:核心矛盾是个性化效果与在线成本的平衡

推荐系统常见的链路是:

  • 召回
  • 粗排
  • 精排
  • 重排

如果所有步骤都在线做,成本会非常高。
所以工程上往往会把大量工作前移到:

  • 离线训练
  • 准实时特征生成
  • 候选集预计算

线上只保留必须实时决策的部分。

8.5.4 Feed:核心矛盾是分发模型和热点控制

Feed 流的问题通常不是单次查询复杂,而是:

  • 用户规模大
  • 内容更新快
  • 热点扩散强
  • 顺序和去重要求高

它的架构重点往往在:

  • 写扩散还是读扩散
  • 游标分页和增量拉取
  • 热点内容缓存
  • 异步预聚合时间线

8.5.5 详情页:核心矛盾是多源拼装与高命中率

详情页常见痛点是数据来源太多:

  • 商品基础信息
  • 价格
  • 库存
  • 营销
  • 评价
  • 推荐搭配

如果全部请求时拼装,延迟和失败率都会显著上升。
所以成熟详情页通常依赖:

  • 物化视图
  • 局部字段独立缓存
  • 热点页预热
  • 动静分离

8.6 一致性与延迟如何取舍

这类系统不是不需要一致性,而是需要一种不同于交易系统的一致性治理方式。

8.6.1 先定义一致性窗口

不要笼统地说“允许一点延迟”,而要明确:

  • 搜索索引允许延迟多少秒
  • 推荐候选允许延迟多久
  • 缓存允许陈旧多久
  • 广告预算与投放状态允许多大偏差窗口

只有把窗口数字化,系统才能治理。

8.6.2 热数据优先收敛,冷数据延后收敛

不是所有数据都需要同样快地更新。

例如:

  • 热门商品价格变更要优先刷新
  • 长尾内容索引延迟几十秒可能完全可接受
  • 活动期广告预算状态要更高频同步

这类分层治理比“全量一视同仁”更现实。

8.6.3 读降级要比写降级更早准备

在复杂读系统里,系统最常见的救命手段不是暂停写入,而是读路径降级:

  • 关闭部分排序特征
  • 降低召回深度
  • 只展示缓存结果
  • 跳过部分非核心拼装字段
  • 返回默认推荐或兜底列表

读降级设计得越早,系统在高峰时越稳。

8.7 常见误区

8.7.1 直接拿交易库硬扛搜索和复杂筛选

短期可用,长期一定会遇到查询性能和资源争抢问题。

8.7.2 过度迷信缓存命中率,而忽视失效治理

高命中率不等于系统稳定。
如果热点失效后全量回源,系统一样会雪崩。

8.7.3 把所有计算都放在线上实时完成

这样最容易把链路做长、把 P99 做坏、把下游依赖拖死。

8.7.4 只谈检索,不谈排序和特征预算

很多低延迟系统真正慢的不是“查出来”,而是“排出来”。

8.7.5 没有为不一致设计解释机制

如果价格更新后搜索结果短时间未刷新,系统至少要知道:

  • 哪一层没更新
  • 延迟窗口是否超标
  • 是否需要热点强刷

否则“不一致”就只是不可定位的黑盒现象。

8.8 方法论沉淀:如何做出务实选型

8.8.1 五个判断句

  1. 只要读链路处在主路径、对时延高度敏感,就要优先围绕读模型而不是写模型来设计。
  2. 复杂读系统的关键不是“把数据查出来”,而是“把复杂结果在延迟预算内稳定产出”。
  3. 能预计算的不要现场算,能缓存的不要重复算,能分层的不要一层做完。
  4. 读系统的一致性治理重点不是瞬时全局一致,而是可控的一致性窗口、热点优先收敛和降级能力。
  5. 真正成熟的低延迟系统,拼的不只是搜索引擎或缓存,而是索引、缓存、读模型、排序链路和治理能力的整体配合。

8.8.2 一句话表达

如果把这一章压缩成一句可以直接复用的话,可以这样讲:

低延迟与复杂读系统设计的核心,不是让所有数据实时强一致地回源读取,而是围绕查询模式重建读模型,用索引、缓存、预计算、物化视图和近实时更新,把复杂结果稳定地压进延迟预算里。