第二阶段迭代:外部系统数据接入

需求背景

业务推出了"大众图书点评"功能,由另一个团队独立开发维护,和我们的信息聚合系统完全解耦。

点评系统每日计算一次图书的评分情况,产出四个维度的数据:图书星级得分、评论数量、好评数、差评数。

现在,业务希望用户能根据这些数据来搜索图书——比如搜索"4星以上且评论数超过1000的图书"。

核心矛盾

这个需求看起来不难,但有一个关键约束:点评数据不归我们管

最简单的做法是把点评数据全量同步到我们的数据库,然后按自己的数据处理就行了。但这样做有几个问题:点评数据量可能很大,全量冗余存储成本高;两套数据之间的一致性需要我们自己维护;点评系统一旦有字段变更,我们也要跟着改。

另一个极端是每次查询时实时调用点评系统的接口。但这样会把我们的查询性能完全绑定在对方系统上,一旦对方接口抖动,我们的搜索就会受影响。

核心矛盾是:如何接入外部系统的数据,在保持系统解耦的同时,保障数据的一致性和查询性能?

方案设计

选择 MQ 实时监听 + ES 文档更新 的方案,核心思路是:

  • 不在本地数据库冗余存储外部数据,避免存储成本和双写一致性问题
  • 监听外部系统的数据变更 MQ,收到变更后异步更新 ES 文档
  • 更新 ES 时,通过 ID 反查外部系统接口获取最新数据,再聚合写入

为什么选 MQ 而不是定时轮询? 实时性更好,数据变更能立即感知;资源消耗更低,不需要定时全量扫描;MQ 消费天然支持重试,有助于保障最终一致性。

ES 文档更新流程

1. 点评系统数据变更,发送 MQ 消息(包含 book_id)
2. 本地 MQ 消费者收到消息
3. 通过 book_id 查询 book_base 基础信息
4. 通过 book_id 查询 book_extension_attribute 扩展属性
5. 通过 book_id 调用点评系统接口获取最新评分数据
6. 聚合所有数据,构造完整 ES 文档
7. 幂等更新 ES 索引

ES 文档结构

点评数据直接作为嵌套对象存入 ES 文档,不在 MySQL 冗余:

{
  "mappings": {
    "star_level": {
      "type": "object",
      "properties": {
        "level":             { "type": "keyword" },
        "comment_count":     { "type": "long" },
        "good_comment_count":{ "type": "long" },
        "bad_comment_count": { "type": "long" }
      }
    }
  }
}

数据一致性的挑战

这里有一个容易被忽视的时序问题:外部系统数据更新和 MQ 发送的顺序可能不一致。

T1: 点评系统更新星级:3星 → 4星
T2: 点评系统发送 MQ(book_id=1001)
T3: 我们收到 MQ,反查点评接口
T4: 接口返回的还是 3星(接口数据更新滞后)

这种情况下,我们更新到 ES 的数据是错的。

解决方案是两层兜底:幂等更新 + 延时重试。所有 ES 更新操作设计为幂等,允许重复执行;如果反查失败或数据异常,放入延时重试队列,指数退避后重新执行。这样即使某次更新写入了旧数据,后续的重试也能把它修正过来。

当然,这只是我们侧的兜底。更根本的解法是要求上游系统保证 MQ 发送和数据更新的顺序一致性,并提供高频查询能力(如 Redis 缓存)。这是一个需要和上游系统协商的架构约束。

这一步解决了什么

通过 MQ 监听 + ES 异步更新的方案,我们在不冗余存储外部数据的前提下,支持了对外部系统属性的搜索。两个系统保持解耦,各自独立演进。

但随着接入的外部系统越来越多,一个新的问题开始浮现:搜索项越来越多,前端页面开始变得臃肿,而且很多搜索条件之间的组合关系也越来越复杂

这引出了下一篇:搜索项和属性的解耦设计。