个人技术总结——MySQL 与 Elasticsearch 双写一致性保障实践
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 的
IndexAPI 天然支持“存在则覆盖”,无需额外判断 - 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,服务暂时不可用。
解决方案:
- 短期:接受最终不一致,但增加监控
- 暴露
/health/es-sync接口,返回最近 10 条同步失败记录 - 在管理后台标注“ES 同步异常”状态
- 暴露
- 长期:规划本地消息表方案(见下文)。
问题 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
解决方案:
- 修复映射:在 ES 索引 mapping 中明确定义
is_active为boolean - 代码兜底:在
model.RecognizedEvent中使用自定义UnmarshalJSON处理类型转换
4. 未来优化:本地消息表方案
为彻底解决一致性问题,我设计了无外部依赖的最终一致性方案:
核心流程
- 事务内双写:MySQL 事务中同时写
recognized_events+es_sync_tasks - 后台任务:定时扫描
es_sync_tasks,重试失败任务 - 人工兜底:提供
/admin/es/rebuild接口,支持按 ID 手动重建
优势
- 零外部依赖:仅需 MySQL
- 最终一致:失败自动重试,最多 3 次
- 可观测:任务状态(pending/success/failed)可查
5. 总结与收获
通过本次实践,我深刻理解了分布式系统中的一致性权衡:
- 务实优先:在资源受限时,“可用性 > 强一致性” 是合理选择
- 可观测性是生命线:同步失败必须可监控、可告警、可修复
- 代码即防御:
sonic高性能解析、TermQuery精确匹配、Id幂等设计,都是可靠性的基石
参考资料
- olivere/elastic/v7 官方文档
https://github.com/olivere/elastic - bytedance/sonic 高性能 JSON 库
https://github.com/bytedance/sonic - 《数据密集型应用系统设计》第 11 章:复制 - Martin Kleppmann
- 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 大于上
es2csv:从Elasticsearch导出到CSV文件
es2csv:从Elasticsearch导出到CSV文件


