最后一个架构问题
写入调优解决了「怎么写得快」,但「数据从哪来」还没系统化:商品主数据在 MySQL,ES 里的副本怎么保持一致?改个价格,ES 什么时候跟上?这是所有搜索系统的必答题。候选答案四条路:同步双写、异步双写、定时扫表、binlog 订阅。逐条走一遍,坑都标出来。
第一条路:同步双写,最直观也最脆
在业务代码里,写完 MySQL 紧接着写 ES。直观,但暗坑三连:一是失败处理难——ES 写失败要不要回滚 MySQL?跨库事务代价太大,不回滚就是脏数据;二是延迟耦合——ES 抖一下,下单主流程跟着抖;三是到处散落——每个写商品的入口都得记得写 ES,漏一处就不同步。老王第一版就是这条路,线上第一个客诉就是这么来的。
第二条路:异步双写,用 MQ 解耦
把「同步进 ES」改成「发一条商品变更消息」,由消费者去写 ES——这正好是消息队列系列里异步解耦的教科书场景。主流程只依赖 MySQL 和 MQ,ES 挂了消息堆着,恢复后接着消费。最终一致性替代了实时强一致,代价是引入了消息组件和「重试加幂等」的心智:消费者必须幂等(回扣第 12 篇的业务主键 upsert),消息顺序丢了就得靠带版本更新兜底。团队已有 MQ 基建时,这是性价比很高的默认选项。
第三条路:定时扫表,笨但稳
每分钟扫一次 update_time 大于上次水位的记录,推给 ES。实现一个下午就能上线,不侵入业务代码,失败重扫就行。短板也明显:延迟等于扫描间隔,分钟级起步;扫表靠 update_time 索引,大表翻页要小心深分页(第 10 篇的教训在这里重演);被物理删除的行扫不到,得配软删除标记。中小数据量、容忍分钟级延迟的报表型索引,这条路其实很够用。
第四条路:binlog 订阅,准实时的终极答案
MySQL 主从复制的原理第 10 篇(MySQL 系列)讲过:主库把变更写进 binlog,从库重放。既然 binlog 记录了一切变更,那就假装自己是个从库——Canal、Flink CDC 这类工具伪装成 MySQL 的从节点,拉 binlog 解析成结构化变更,投递给 ES。业务代码零侵入,延迟秒级,连物理删除都能捕获。代价是引入一套订阅组件的运维:位点管理、数据投递的幂等与顺序、binlog 格式必须 ROW——都是实打实的运维成本。
| 方案 | 延迟 | 侵入性 | 适用 |
|---|---|---|---|
| 同步双写 | 实时 | 业务代码强侵入 | 小系统,能接受一致风险 |
| 异步双写 | 秒级 | 只侵入发消息一处 | 已有 MQ 基建的团队 |
| 定时扫表 | 分钟级 | 零侵入 | 报表型索引,延迟不敏感 |
| binlog 订阅 | 秒级 | 零业务侵入,组件运维成本高 | 准实时大表,删除也要同步 |
老王的选择与不变的法宝
老王最后的选择是组合拳:全量重建走 _reindex 加导入三件套(第 12 篇),增量用定时扫表起步,等数据量翻倍再切 binlog 订阅。无论走哪条路,两条不变的法宝:一,ES 文档 ID 用业务主键,写入天然 upsert 幂等,重试乱序都不怕;二,定期比对 MySQL 与 ES 的数量和抽样内容,对账任务每小时跑一次,发现漂移自动触发局部修复——一致性不是设计出来的,是校验出来的。
小结
四条路一句话:同步双写别裸奔,异步双写配幂等,扫表稳但慢,binlog 零侵入但重运维。没有最好的方案,只有配得上你数据量和团队能力的方案。
数据管道通了,零件全齐了:分词、mapping、查询、评分、聚合、集群、分片、同步。最后一道工序是把它们组装成产品。下一篇讲搜索产品化:从搜索框到搜索系统的完整链路设计。
评论 (0)