个人技术总结——MySQL 与 Elasticsearch 双写一致性保障实践

yyyyy_yyy_y yyyyy_yyy_y 墨衍会员 · AI 创作全网分发 墨衍智能分发 通过墨衍一键同步至各平台 可同步平台 CSDN微信公众号微博知乎掘金百家号博客园抖音小红书 你的文章也可以这样分发 墨衍会员 ¥399起/年,写一次发全网 我也要 + 加入社区

MySQL 与 Elasticsearch 双写一致性保障实践

目录

  • MySQL 与 Elasticsearch 双写一致性保障实践
  • 1. 技术概述
  • 2. 技术详述
  • 2.1 ES 客户端封装与核心操作
  • 文档写入(AddItem)
  • 文档删除(RemoveItem)
  • 搜索查询(SearchItems + BuildQuery)
  • 结果解析(高性能 JSON 处理)
  • 2.2 双写一致性实现:服务层集成
  • 在 MaintainService 中,认定事件创建流程如下:
  • 3. 遇到的问题与解决过程
  • 问题 1:ES 服务不可用导致搜索结果不全
  • 问题 2:搜索结果字段解析失败
  • 4. 未来优化:本地消息表方案
  • 核心流程
  • 优势
  • 5. 总结与收获
  • 参考资料

1. 技术概述

在本次软件工程课程项目中,我负责后端核心架构设计。系统需支持管理员全文检索认定赛事(如按赛事名称、主办方搜索),因此采用 MySQL + Elasticsearch 双存储架构:

  • MySQL:作为主数据库,保证事务性与强一致性;
  • Elasticsearch:提供高性能全文检索能力。

然而,在开发过程中,我遇到一个典型分布式系统难题:如何保证 MySQL 与 ES 的数据一致性?
本文总结我在 Beta 冲刺阶段采用的同步双写 + 日志告警方案,并给出完整代码实现;同时规划后续基于本地消息表的最终一致性优化路径。

2. 技术详述

2.1 ES 客户端封装与核心操作

项目中 es 包封装了索引管理、文档增删改查等核心能力,关键代码如下:

文档写入(AddItem)

func AddItem(ctx context.Context, indexName string, re *model.RecognizedEvent) error {
    _, err := els.Index().
        Index(indexName).
        Id(fmt.Sprintf("%s", re.RecognizedEventId)). // 使用业务ID作为ES文档ID
        BodyJson(re).Do(ctx)
    if err != nil {
        return errno.Errorf(errno.InternalESErrorCode, "Elastic.AddItem Error adding item: %v", err)
    }
    return nil
}
  • 覆盖策略:ES 的 Index API 天然支持“存在则覆盖”,无需额外判断
  • ID 设计:直接使用 RecognizedEventId(如 evt_123)作为文档 ID,确保幂等性

文档删除(RemoveItem)

func RemoveItem(ctx context.Context, indexName string, id string) error {
    _, err := els.Delete().Index(indexName).Id(fmt.Sprintf("%s", id)).Do(ctx)
    if err != nil {
        return errno.Errorf(errno.InternalESErrorCode, "Elastic.RemoveItem failed: %v", err)
    }
    return nil
}
  • 容错设计:ES 在删除不存在文档时返回 404,但 olivere/elastic/v7 默认将其视为错误。**实践中我们发现,Delete().Do() 在文档不存在时 err 可能为 nil**(取决于 ES 版本),因此调用方无需额外处理“不存在”场景。

搜索查询(SearchItems + BuildQuery)

func BuildQuery(req *model.ViewRecognizedRewardReq) *elastic.BoolQuery {
    query := elastic.NewBoolQuery()
    hasCondition := false

    // 赛事名称、主办方:分词搜索
    if req.EventName != nil && req.GetEventName() != "" {
        query = query.Must(elastic.NewMatchQuery("RecognizedEventName", req.GetEventName()))
        hasCondition = true
    }
    if req.OrganizerName != nil && req.GetOrganizerName() != "" {
        query = query.Must(elastic.NewMatchQuery("Organizer", req.GetOrganizerName()))
        hasCondition = true
    }

    // 认定ID:精确匹配
    if req.RecognizedEventId != nil && req.GetRecognizedEventId() != "" {
        query = query.Must(elastic.NewTermQuery("RecognizedEventId", req.GetRecognizedEventId()))
        hasCondition = true
    }

    if !hasCondition {
        query = query.Must(elastic.NewMatchAllQuery())
    }
    return query
}
  • 混合查询:MatchQuery 用于分词字段(赛事名、主办方),TermQuery 用于 keyword 字段(ID);
  • 空查询兜底:无条件时返回 MatchAllQuery,避免 ES 报错。

结果解析(高性能 JSON 处理)

for _, hit := range result.Hits.Hits {
    var re model.RecognizedEvent
    data, err := hit.Source.MarshalJSON() // 先转为 []byte
    if err != nil {
        return nil, 0, errno.Errorf(errno.InternalServiceErrorCode, "ES unmarshal failed: %v", err)
    }
    err = sonic.Unmarshal(data, &re) // 使用 bytedance/sonic 高性能解析
    if err != nil {
        return nil, 0, errno.Errorf(errno.InternalServiceErrorCode, "ES unmarshal failed: %v", err)
    }
    rets = append(rets, &re)
}
  • 性能优化:使用 bytedance/sonic 替代标准库 json,解析速度提升 3–5 倍
  • 安全解包:先 MarshalJSON 再 Unmarshal,避免 interface{} 类型断言风险

2.2 双写一致性实现:服务层集成

在 MaintainService 中,认定事件创建流程如下:

// 1. 写入 MySQL
eventID := "evt_" + s.idGen.Next().String()
req.RecognizedEventId = eventID
err := s.repo.CreateRecognizedEvent(ctx, req)
if err != nil {
    return nil, err
}

// 2. 同步写入 ES
if err := es.AddItem(ctx, "recognized_events", req); err != nil {
    // ⚠️ 关键:ES 失败不回滚 MySQL
    log.Printf("[WARN] ES sync failed for %s: %v", eventID, err)
    // 记录技术债,供后续补偿
}

3. 遇到的问题与解决过程

问题 1:ES 服务不可用导致搜索结果不全

现象:
认定上传成功,但搜索无结果。日志显示:

Elastic.AddItem Error adding item: elastic: Error 503 (Service Unavailable)

根因:
ES 内存不足 OOM,服务暂时不可用。

解决方案:

  1. 短期:接受最终不一致,但增加监控
    • 暴露 /health/es-sync 接口,返回最近 10 条同步失败记录
    • 在管理后台标注“ES 同步异常”状态
  2. 长期:规划本地消息表方案(见下文)。

问题 2:搜索结果字段解析失败

现象:
SearchItems 返回内部错误,日志:

ES unmarshal failed: json: cannot unmarshal number into Go struct field RecognizedEvent.is_active of type bool

根因:
ES 中 is_active 存储为 0/1(number),但 Go 结构体为 bool

解决方案:

  1. 修复映射:在 ES 索引 mapping 中明确定义 is_active 为 boolean
  2. 代码兜底:在 model.RecognizedEvent 中使用自定义 UnmarshalJSON 处理类型转换

4. 未来优化:本地消息表方案

为彻底解决一致性问题,我设计了无外部依赖的最终一致性方案:

核心流程

  1. 事务内双写:MySQL 事务中同时写 recognized_events + es_sync_tasks
  2. 后台任务:定时扫描 es_sync_tasks,重试失败任务
  3. 人工兜底:提供 /admin/es/rebuild 接口,支持按 ID 手动重建

优势

  • 零外部依赖:仅需 MySQL
  • 最终一致:失败自动重试,最多 3 次
  • 可观测:任务状态(pending/success/failed)可查

5. 总结与收获

通过本次实践,我深刻理解了分布式系统中的一致性权衡:

  • 务实优先:在资源受限时,“可用性 > 强一致性” 是合理选择
  • 可观测性是生命线:同步失败必须可监控、可告警、可修复
  • 代码即防御:sonic 高性能解析、TermQuery 精确匹配、Id 幂等设计,都是可靠性的基石

参考资料

  1. olivere/elastic/v7 官方文档
    https://github.com/olivere/elastic
  2. bytedance/sonic 高性能 JSON 库
    https://github.com/bytedance/sonic
  3. 《数据密集型应用系统设计》第 11 章:复制 - Martin Kleppmann
  4. Elasticsearch Mapping 设计最佳实践
    https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html
...全文

相关推荐

mysql+es实现双写方案代码

mysql+es实现双写方案代码,通过此代码,可以直接运行并测试完整的流程,里面包括非常多的测试用例,拿来可以简单调整下配置参数即可使用

小码农叔叔

mysql-es:将MySQL同步到ElasticSearch,支持关系

MySQL-ElasticSearch同步 一个具有实时,无损失,一对一关系MySQL-ElasticSearch同步工具。 基于, 。 运河是Bin-log解析器,也是阿里巴巴的订户 版本 1.0-beta:2018-09-04 说明书 ,请点击这里 要求 Java 1.8以上 2 GB内存+ 2核CPU + 100M可用空间(用于日志) 特征 支持ElasticSearch 5.x〜6.x. 支持没有启用彬日志之前MySQL。 如果MySQL之前没有启用bin-log,NO PROM,请立即启用它。 该工具将通过“ mysqldump”转储历史数据。 请参阅。 支持一对一关系。 原始表 用户表:| id | 昵称| xxx | 职位表: id | user_id | 标题| 内容| 使用简单的设置将它们全部同步,例如: ES-index帖子:| id | u

苏鲁定

基于SpringBoot技术开发的个人博客系统

Thymeleaf模板引擎,Elasticsearch全文搜索,启动Elasticsearch 和mysql,更改配置文件即可运行

渡船夫

基于 MySQL Binlog 的 Elasticsearch 数据同步实践

一、为什么要做 随着马蜂窝的逐渐发展,我们的业务数据越来越多,单纯使用 MySQL 已经不能满足我们的数据查询需求,例如对于商品、订单等数据的多维度检索。 使用 Elasticsearch 存储业务数据可以很好的解决我们业务中的搜索需求。而数据进行异构存储后,随之而来的就是数据同步的问题。 二、现有方法及问题 对于数据同步,我们目前的解决方案是建立数据中间表。把需要检索的业务数据,统一放到一张MySQL 表中,这张中间表对应了业务需要的Elasticsearch 索引,每一列对应索引中的一个Mapping 字段。通过脚本以 Crontab 的方式,读取 MySQL 中间表中 UTime 大于上

weixin_38557068

es2csv:从Elasticsearch导出到CSV文件

es2csv:从Elasticsearch导出到CSV文件

Mika.w
评论
收藏 已收藏
分享
返回顶部