2. 搜索服务架构:整体架构图、服务间通信方式、Redis在搜索服务中的位置

搜索服务作为电商、内容平台或企业级应用的核心中间层,其架构设计直接影响查询性能、数据一致性与可观测性。本节从整体架构切入,逐步拆解服务间通信机制,并明确 Redis 在搜索链路中的关键定位。

2.1 搜索服务整体架构图

搜索服务通常采用 分层 + 异步管道 的架构模式,将数据写入、索引构建、查询路由与结果缓存解耦。下图展示了标准搜索服务的核心组件与数据流向:

搜索服务整体架构图 搜索服务整体架构图 数据源层 MySQL Binlog | 消息队列(Kafka / RocketMQ) | 全量 Dump 文件 索引构建层 全量索引 Builder | 增量索引 Consumer | 索引 Merge & Swap 存储层 倒排索引(Lucene / 自研) | 正排存储(HBase / TiKV) | 向量索引(Faiss / HNSW) 查询层 Query Parser | 召回 & 粗排 | 精排 & 重排 | 结果聚合 缓存层(Redis Cluster)
架构要点: 数据从源层单向流入索引构建层,构建完成后写入存储层;查询层接收用户请求后,先查询缓存层(Redis),未命中则回源存储层进行召回与排序。缓存层是查询性能的第一道防线。

2.2 服务间通信方式

搜索服务内部各层之间、以及搜索服务与外部依赖(如用户中心、商品中心)之间的通信,主要采用以下三种方式:

通信方式 协议 / 载体 典型场景 延迟要求
同步 RPC gRPC / Thrift / Dubbo 查询请求转发、用户鉴权、实时价格校验 < 50ms
异步消息 Kafka / RocketMQ / RabbitMQ 数据变更通知、索引增量更新、日志采集 < 500ms(端到端)
缓存 / 状态同步 Redis 协议(RESP) 热点结果缓存、分布式锁、限流计数器 < 5ms(单次操作)
契约测试关注点: 在同步 RPC 和 Redis 缓存通信中,错误码断言是契约测试的核心。例如,当 Redis 返回 MOVED(集群重定向)或 BUSY(脚本执行超时)时,搜索服务必须按照契约定义返回标准化的业务错误码,而非直接透传 Redis 原生错误。

2.3 Redis 在搜索服务中的位置

Redis 在搜索架构中并非存储主路径,而是作为 加速层 和 协调层 存在。其具体职责可归纳为以下四个维度:

  • 查询结果缓存: 对高频、低变更的搜索请求(如热门关键词、类目导航)进行结果缓存,TTL 通常设置为 30s~300s,命中率目标 > 60%。
  • 分布式限流与熔断: 使用 Redis 的 INCR + 过期时间实现滑动窗口限流;结合 SETNX 实现熔断开关,保护下游索引服务。
  • 索引构建状态协调: 记录全量索引版本号、增量消费位点、索引 Swap 状态,确保多副本索引构建的一致性。
  • 错误码映射与断言缓存: 在契约测试框架中,Redis 存储 错误码 → 业务提示 的映射表,测试用例通过断言 Redis 中的错误码值来验证服务行为。
Redis 在搜索链路中的位置 Redis 在搜索链路中的位置与交互 客户端 / API Gateway 搜索查询服务 索引存储 Redis 集群 缓存查询 / 限流 / 错误码断言 缓存命中 / 限流结果 契约测试中的 Redis 角色 测试框架通过 Redis 存储预期错误码映射表,断言搜索服务返回的错误码是否与契约一致
常见误区: 不要将 Redis 作为搜索的主数据源。Redis 只适合缓存 可容忍最终一致性 的数据。对于需要强一致的搜索结果(如库存、价格),必须回源索引存储层读取,避免因缓存未及时更新导致数据错误。

2.4 契约测试视角下的通信链路

在契约测试中,搜索服务的通信链路被抽象为以下三个可断言接口:

  1. 客户端 → 搜索查询服务(RPC): 断言请求格式、超时行为、错误码映射。
  2. 搜索查询服务 → Redis(缓存协议): 断言 Redis 错误码(如 MOVED、ASK、BUSY)是否被正确转换为业务错误码。
  3. 搜索查询服务 → 索引存储(内部 RPC): 断言索引不可用时的降级策略与错误码。
// 契约测试示例:Redis 错误码断言
// 当 Redis 返回 MOVED 重定向时,搜索服务应返回 3001(缓存路由异常)
@Test
public void testRedisMovedErrorMapping() {
    // 模拟 Redis 返回 MOVED 错误
    redisMock.willReturn(new RedisException("MOVED 12345 192.168.1.1:6379"));
    
    SearchResponse response = searchService.search("keyword");
    
    // 断言业务错误码为 3001,而非直接透传 MOVED
    assertThat(response.getCode()).isEqualTo(3001);
    assertThat(response.getMessage()).contains("缓存路由异常");
}
最佳实践: 在契约测试中,为每个 Redis 错误码(MOVED、ASK、BUSY、NOSCRIPT、READONLY)编写独立的断言用例,确保搜索服务在 Redis 集群发生拓扑变更或异常时,仍能返回符合契约的业务错误码。