114
社区成员
发帖
与我相关
我的任务
分享在本次软件工程课程项目中,我负责后端核心架构设计。系统需支持管理员全文检索认定赛事(如按赛事名称、主办方搜索),因此采用 MySQL + Elasticsearch 双存储架构:
然而,在开发过程中,我遇到一个典型分布式系统难题:如何保证 MySQL 与 ES 的数据一致性?
本文总结我在 Beta 冲刺阶段采用的同步双写 + 日志告警方案,并给出完整代码实现;同时规划后续基于本地消息表的最终一致性优化路径。
项目中 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
}
Index API 天然支持“存在则覆盖”,无需额外判断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
}
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 报错。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{} 类型断言风险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)
// 记录技术债,供后续补偿
}
现象:
认定上传成功,但搜索无结果。日志显示:
Elastic.AddItem Error adding item: elastic: Error 503 (Service Unavailable)
根因:
ES 内存不足 OOM,服务暂时不可用。
解决方案:
/health/es-sync 接口,返回最近 10 条同步失败记录现象: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
解决方案:
is_active 为 booleanmodel.RecognizedEvent 中使用自定义 UnmarshalJSON 处理类型转换为彻底解决一致性问题,我设计了无外部依赖的最终一致性方案:
recognized_events + es_sync_taskses_sync_tasks,重试失败任务/admin/es/rebuild 接口,支持按 ID 手动重建通过本次实践,我深刻理解了分布式系统中的一致性权衡:
sonic 高性能解析、TermQuery 精确匹配、Id 幂等设计,都是可靠性的基石