怎么保证MySQL和ES的一致性?
怎么保证MySQL和ES的一致性?
月伴飞鱼MySQL 和 Elasticsearch(ES)的“一致性”本质是两套存储在不同一致性模型下做数据冗余。
MySQL 以事务与行级更新为中心,ES 以近实时(NRT, near real-time)索引刷新为中心。
目标通常不是强一致,而是把不一致窗口压到可控范围,并且让不一致可检测、可修复、可回放。
一致性目标分层:先把口径定清。
工程上常见三档目标,选错档位会导致方案过重或掩盖风险。
- 读一致(展示一致):用户查询结果与 MySQL 最终一致,允许秒级延迟;适合搜索、列表、推荐召回。
- 写一致(业务一致):关键业务写入后立刻可被检索到;通常需要同步写或写后校验,成本高,吞吐低。
- 可恢复一致(可运维一致):允许出现不一致,但必须满足可追踪(有日志)、可重放(可补偿)、可校验(可对账)。
大多数“MySQL + ES”场景追求“读一致 + 可恢复一致”。
主流方案一:CDC 异步同步(推荐基线)
CDC(Change Data Capture,变更数据捕获)基于 MySQL Binlog 把增量变更流式投递到 ES。
典型链路:MySQL → Binlog → 消费端(如 Debezium/Canal)→ MQ(可选)→ Indexer → ES。
关键收益:
- 不侵入业务事务,不要求业务代码在事务内同时写 ES。
- 按 Binlog 顺序消费,天然具备可重放能力。
- 可做断点续传与延迟监控。
落地要点:
- 表必须有稳定主键,ES 文档
_id通常直接用主键,避免重复文档。 - 消费幂等:同一条变更重复投递时结果不变。
- ES
index(覆盖写)天然幂等;更新用update时需要脚本或完整文档覆盖,避免部分字段回滚。
- ES
- 顺序与乱序控制:同一主键的更新必须按顺序落 ES。
- 常用做法是按主键分区(MQ 分区或消费端内部队列)确保同 key 串行。
- 删除一致:MySQL 删除必须映射为 ES
delete;- 软删除要统一语义(例如
is_deleted字段),避免 MySQL 物理删但 ES 仍保留。
- 软删除要统一语义(例如
- DDL 处理:字段新增、类型变更要有治理流程;
- ES mapping 变更不可随意(尤其是类型冲突会导致写入失败并积压)。
适用边界:
- 秒级延迟可接受。
- 读多写少、以检索为主的场景。
主流方案二:Outbox 模式(事务内落库,库外投递)
Outbox 把“需要同步到 ES 的事件”与业务数据放在 同一个 MySQL 事务里提交,避免“业务写成功但事件没发出去”的裂缝。
基本形态:
- 业务事务:写业务表 + 写 outbox 表(事件表)
- 异步 worker 扫描 outbox 表或通过 Binlog CDC 读取 outbox,再写 ES
- 成功后标记 outbox 事件已处理(或用状态机)
关键收益:
- 事务级保证“业务数据与事件”一致。
- 不依赖业务在事务里直接调用 MQ 或 ES,避免分布式事务复杂度。
落地要点:
- 事件去重键:
event_id(UUID/雪花)+ 唯一索引,保证重复扫描不会重复产生副作用。 - 投递语义:通常做到“至少一次”(at-least-once),靠 ES 幂等覆盖写达成最终一致。
- 堆积治理:outbox 表分区/归档,扫描按时间窗口或自增 ID,避免全表扫。
适用边界:
- 业务对“写后可检索”要求高,但仍可容忍短延迟。
- 需要更清晰的审计与补偿链路。
不推荐但常见:双写(业务同时写 MySQL + ES)
双写看似简单,实则最容易出现一致性缝隙:MySQL 成功、ES 失败;或反过来;或重试导致乱序覆盖。
若历史包袱必须双写,最低限度的控制:
- 以 MySQL 为唯一真源,ES 失败不回滚 MySQL;失败记录必须可重放。
- 写 ES 使用幂等覆盖(
_id=主键的index),避免重试产生重复。 - 写入顺序用版本号:文档携带
version(可用 MySQL 自增版本/更新时间戳/全局递增序列),ES 更新时做版本比较。- 拒绝旧版本覆盖新版本(可用外部版本 external versioning 思路实现)。
- 统一重试与死信:写 ES 失败进入重试队列,超过阈值进入死信,人工或批任务修复。
双写适合低并发、低复杂度、容忍少量不一致且有补偿能力的场景;在高并发更新下很难长期稳定。
写入模型:增量同步之外要有“全量重建”
仅有增量链路不足以“保证一致”,因为 ES 可能因 mapping 冲突、节点抖动、批量写失败等产生缺口。
稳定系统一定包含全量能力。
常用组合:
- 全量重建索引:新建 index(带版本号或日期),全量从 MySQL 导入,追平增量后切 alias(别名)完成无感切换。
- 增量追平:重建期间仍消费 Binlog/Outbox,把变更写到新 index;切换前做一次对账校验。
- 回滚通道:切 alias 失败可快速切回旧 index。
这套机制把“长期漂移”变成“可周期清零”的问题。
校验与对账:让不一致可被发现
一致性不是只靠写链路,更靠监控与对账闭环。
可操作的校验方式:
- 按时间窗口抽样对账:取最近 5–30 分钟 MySQL 变更主键集合,与 ES 按
_id查询比对缺失/字段差异。 - 按业务版本号对账:MySQL 表增加
row_version(递增)或updated_at;- ES 文档同步同字段。对比版本不一致即可定位乱序与漏写。
- 消费位点监控:CDC 的 Binlog 位点(offset)与当前 Binlog 差距,形成延迟指标;超过阈值告警。
- 失败写入告警:Bulk API 错误率、mapping 冲突、429/503 等必须可观测并自动降速或熔断。
常见坑:一致性失败往往不是“没用对方案”
- ES mapping 漂移:字段类型在不同批次被推成 text/keyword/date 冲突,导致部分写入永久失败,形成“暗洞”。
- 更新是局部字段:MySQL 更新了字段 A,ES 用部分更新脚本覆盖时字段 B 被旧值写回,出现“回滚式不一致”。
- 用全量文档覆盖或用版本控制避免。
- 同主键并发更新:消息乱序导致 ES 落了旧版本。解决靠分区顺序 + 版本拒写。
- 删除语义不统一:MySQL 软删、ES 物理删或反之,查询侧过滤条件不一致。
- 刷新与可见性误判:ES 默认刷新间隔导致“写入成功但搜索不到”在 1s 级别出现;
- 业务若要求写后立见,需改查询策略或走 MySQL 回源。
一套可落地的“强韧”组合
实践中稳定且成本可控的组合通常是:
- MySQL 为真源
- CDC(Debezium/Canal)或 Outbox 做增量
- ES 写入 幂等覆盖 + 版本控制
- 全量重建 + alias 切换
- 延迟、失败、对账三类监控闭环
这套设计不追求分布式强一致,而是把不一致窗口、发生概率与修复成本压到可运维的范围,并且在故障时能快速恢复到统一口径。














