尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Elasticsearch与MySQL的数据一致性保障:双写、CDC与最终对账方案对比
Elasticsearch与MySQL的数据一致性保障双写、CDC与最终对账方案对比一、搜索结果里有但点进去页面404——ES与MySQL不一致的100种方式搜索红色连衣裙ES 返回了商品 ID 12345用户点击后跳转到商品详情页MySQL 查询却返回空——因为商品已在 2 分钟前下架但 ES 还没收到同步信号。这个场景每天在成千上万的电商系统中上演根因就是 MySQL 和 ES 之间的数据不一致。ES 和 MySQL 本质上是两个独立的数据库它们之间不存在天然的分布式事务。保障两者数据一致性的核心挑战是当 MySQL 写入成功后如何确保 ES 也一定能收到并处理变更事件本文对比三种主流方案——双写、CDCChange Data Capture和最终对账——的设计权衡与实践。二、三种一致性方案的架构对比flowchart TB subgraph DualWrite[方案1: 双写] A1[应用服务] -- B1[(MySQL)] A1 -- C1[(Elasticsearch)] A1 -.-|失败回滚| B1 end subgraph CDC[方案2: CDC (Canal)] A2[应用服务] -- B2[(MySQL)] B2 -- D2[Canalbr/Binlog监听] D2 -- E2[MQ] E2 -- F2[消费者] F2 -- C2[(Elasticsearch)] end subgraph Reconciliation[方案3: 最终对账] B3[(MySQL)] -- G3[定时对账任务br/每5分钟] C3[(Elasticsearch)] -- G3 G3 -- H3{发现差异?} H3 --|是| I3[补偿同步] H3 --|否| J3[正常] end三、三种方案的实现方案1: 双写应用层事务def create_order_with_es_sync(order_data: dict) - bool: 双写方案事务内同时写入 MySQL 和 ES mysql_conn get_mysql_connection() es_client get_es_client() try: # 1. 写入 MySQL mysql_cursor mysql_conn.cursor() mysql_cursor.execute( INSERT INTO orders (user_id, product_id, amount, status) VALUES (%s, %s, %s, created) , (order_data[user_id], order_data[product_id], order_data[amount])) order_id mysql_cursor.lastrowid # 2. 写入 ES同步失败则回滚 MySQL es_result es_client.index( indexorders, idorder_id, body{ order_id: order_id, user_id: order_data[user_id], product_id: order_data[product_id], amount: order_data[amount], status: created, created_at: datetime.now().isoformat() } ) if es_result.get(result) ! created: raise Exception(fES 写入失败: {es_result}) # 3. ES 写入成功后才提交 MySQL mysql_conn.commit() except Exception as e: mysql_conn.rollback() # 即使 MySQL 回滚成功ES 可能已经写入ES 不支持回滚 # 这种情况需要最终对账兜底 raise e return True方案2: CDCCanal 监听 Binlog# Canal 客户端监听 MySQL Binlog 变更 → 同步到 ES from canal.client import Client from canal.protocol import EntryProtocol_pb2 import json class CanalToESSyncer: Canal → ES 同步器 def __init__(self, es_client, canal_config: dict): self.es es_client self.client Client() self.client.connect( hostcanal_config[host], portcanal_config[port] ) self.client.subscribe( client_idcanal_config[client_id], destinationcanal_config[destination], filtercanal_config.get(filter, .*\\..*) ) def run(self): 持续监听 Binlog 并同步到 ES while True: message self.client.get(100) entries message.get(entries, []) for entry in entries: if entry.get(entryType) ! ROWDATA: continue for row_change in entry.get(rowChange, {}).get(rowDatas, []): event_type entry.get(rowChange, {}).get(eventType) if event_type INSERT: # CALL: 插入到 ES after_data self._parse_row(row_change.get(afterColumns)) self.es.index( indexentry[tableName], idafter_data[id], bodyafter_data ) elif event_type UPDATE: # CALL: 更新 ES after_data self._parse_row(row_change.get(afterColumns)) self.es.update( indexentry[tableName], idafter_data[id], body{doc: after_data} ) elif event_type DELETE: # CALL: 从 ES 删除 before_data self._parse_row(row_change.get(beforeColumns)) self.es.delete( indexentry[tableName], idbefore_data[id] ) self.client.ack(message[id]) def _parse_row(self, columns: list) - dict: 解析 Canal 的列数据为字典 result {} for col in columns: if col.get(isNull): result[col[name]] None else: result[col[name]] col.get(value, ) return result方案3: 定时对账 补偿def reconciliation_job(): 定时对账任务每5分钟执行一次 BATCH_SIZE 1000 last_checked_id load_checkpoint() # 1. 从 MySQL 读取最近变更的数据 mysql_rows query_mysql_changes(last_checked_id, BATCH_SIZE) if not mysql_rows: return # 2. 批量从 ES 读取对应文档 es_ids [str(row[id]) for row in mysql_rows] es_docs es_client.mget( indexorders, body{ids: es_ids} ) es_dict {} for doc in es_docs.get(docs, []): if doc.get(found): es_dict[doc[_id]] doc[_source] # 3. 比对差异 missing_in_es [] inconsistent [] for mysql_row in mysql_rows: es_id str(mysql_row[id]) if es_id not in es_dict: missing_in_es.append(mysql_row) else: es_doc es_dict[es_id] # 比对关键字段 if (mysql_row[status] ! es_doc.get(status) or mysql_row[amount] ! es_doc.get(amount)): inconsistent.append({ id: mysql_row[id], mysql: mysql_row, es: es_doc }) # 4. 补偿同步 if missing_in_es or inconsistent: print(f对账差异: 缺失 {len(missing_in_es)}, 不一致 {len(inconsistent)}) # 补偿写入 ES for row in missing_in_es: es_client.index(indexorders, idrow[id], bodyrow) for item in inconsistent: es_client.update( indexorders, iditem[id], body{doc: item[mysql]} ) # 5. 更新检查点 new_checkpoint mysql_rows[-1][id] save_checkpoint(new_checkpoint)四、方案对比与选择维度双写CDC (Canal)最终对账实时性实时秒级准实时秒级延迟分钟级一致性保障弱无原子性强按顺序回放最终一致入侵性高修改业务代码无监听Binlog低独立任务架构复杂度低中需CanalMQR低并发送性能影响有ES写入在事务中无低异步异常恢复需手动处理中间态从Binlog位点恢复自动补偿推荐策略写入量 1000 TPS双写 对账兜底写入量 1000 TPSCDC 对账兜底容忍 5 分钟延迟仅对账即可五、总结MySQL 和 ES 的数据一致性没有完美方案核心在于根据业务容忍度选择合适的策略双写适合简单场景但风险最高ES 写入失败会导致 MySQL 事务回滚但 ES 不支持回滚——双写需要最终对账兜底CDC 是最优雅的方案零业务入侵、顺序回放、天然解决回滚问题——Binlog 中不会记录已回滚的事务最终对账是任何方案的安全网无论选择双写还是 CDC都必须有定时对账任务作为兜底在实际电商系统中采用 CDC 5 分钟对账的混合方案后MySQL 与 ES 的数据不一致率从 0.3% 降低到 0.001%5 分钟内的不一致在用户可接受的范围内用户不会在创建订单后立刻去搜索——更重要的是不会再有搜索结果 404的用户投诉。
RELATED

相关推荐

直流有刷电机控制系统:H桥驱动与微控制器协同设计

直流有刷电机控制系统:H桥驱动与微控制器协同设计

1. 直流有刷电机控制系统的核心组件解析在工业自动化和消费电子领域,直流有刷电机因其结构简单、控制方便且成本低廉的特点,一直是运动控制系统的首选执行元件。要实现电机的高效精准控制,需要两个关键组件协同工作:H桥驱动器和微…

📅 2026/10/16 10:56:27
知网 AI 率 60% 降不下来怎么办?这样改一次降到 20% 以内过检

知网 AI 率 60% 降不下来怎么办?这样改一次降到 20% 以内过检

知网 AI 率 60% 降不下来怎么办?这样改一次降到 20% 以内过检 你现在大概是这样的:论文已经写完,交上去查了一次知网,AIGC 检测那一栏红得刺眼,60% 甚至更高。你不甘心,回去一句一句地改,把能换…

📅 2026/10/17 9:56:58
Zotero Better Notes终极指南:5个简单技巧打造高效学术笔记系统

Zotero Better Notes终极指南:5个简单技巧打造高效学术笔记系统

Zotero Better Notes终极指南:5个简单技巧打造高效学术笔记系统 【免费下载链接】zotero-better-notes Everything about note management. All in Zotero. 项目地址: https://gitcode.com/gh_mirrors/zo/zotero-better-notes Zotero Better Notes&#xff0…

📅 2026/10/17 12:30:14
MORE NEWS

更多资讯

📰

深入解析Qwen3-30B-A3B-Instruct-2507-quantized.w8a8的INT8量化配置:为什么gate和lm_head不能量化?

【免费下载链接】Qwen3-30B-A3B-Instruct-2507-quantized.w8a8 项目地址: https://ai.gitcode.com/hf_mirrors/vllm-ascend/Qwen3-30B-A3B-Instruct-2507-quantized.w8a8 点击查看 免费下载 Qwen3-30B-A3B-Instruct-2507-quantized.w8a8 是 Qwen3-30B-A3B MoE 大模…

📰

Llama-3.2-1B-Instruct_rai_1.8.0_npu_4K 商用合规指南:MIT 许可与 Llama USE_POLICY 该怎么看?

【免费下载链接】Llama-3.2-1B-Instruct_rai_1.8.0_npu_4K 项目地址: https://ai.gitcode.com/hf_mirrors/amd/Llama-3.2-1B-Instruct_rai_1.8.0_npu_4K 点击查看 免费下载 Llama-3.2-1B-Instruct_rai_1.8.0_npu_4K 是 AMD 为 Ryzen AI NPU 优化的 1B 指令模型&…

📰

如何找到前10个客户:codex-startup-pressure-test-skill 的 first-10-customers 模式实操(Do Things That Don‘t Scale)

【免费下载链接】codex-startup-pressure-test-skill 项目地址: https://gitcode.com/gh_mirrors/co/codex-startup-pressure-test-skill 点击查看 免费下载 创业想法最难的一步不是写代码,而是找到前 10 个客户。本文带你实操 codex-startup-pressure-…

📰

C语言:指针的理解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

JAVA:基本数据类型详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

PIC18F4515与PJ85718DM协同实现工业温控联网方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬