🗒️MySQL、ES双写一致性
00 min
2024-4-28
2024-5-9
type
status
date
slug
summary
tags
category
icon
password

场景引入

如果你设计了一个类似 B 站的视频网站中的搜索系统,或许在初期 MySQL 的 like 模糊搜已经能满足你对视频的标题、简介的搜索需求。
如果后期系统视频的量级上来了,对性能有一定要求了,你或许已经开始考虑 ES 了。后面如果你还想让与搜索关键词相关的创作者名称、视频评论、关联标签也能级联视频搜出来,甚至弄一个各种业务维度的融合公式,那么 ES 这样的搜索引擎再适合不过了!
但是,如果数据写一份 MySQL,还要落一份 ES 来支撑筛选/搜索,要怎么写入两个异构的存储系统、并保证它们的一致性呢?

方案一:同步双写

优点

  • 简单粗暴
  • ES 同步写入快

缺点

  • 业务耦合,扩展性差,代码侵入性强(要在写 MySQL 的地方后写 ES 的代码,要在代码设计上做好收敛)
  • 影响性能,写入两个存储,响应时间变长(本来 MySQL 的写入性能不是很高,再加一个 ES,系统的性能必然会下降),吞吐也会下降
  • 存在双写失败丢数据风险(比如写完MySQL,ES 就挂了)

方案二:异步双写

优点

  • 性能高
  • 不易出现数据丢失问题,主要基于 MQ 消息的消费保障机制,比如 ES 宕机或者写入失败,还能重新消费 MQ 消息
  • 多源写入之间相互隔离(业务侧只管发消息),解耦数据生产者和消费者,便于扩展更多的数据源写入

缺点

  • 同样存在业务耦合的问题,写完 DB 之后,还需要发个 MQ(代码设计上做好收敛,也可以关注一下所用 orm 框架有没有 hook 之类的能力,支持你“写后 xxx”)
  • 接入新的数据源需要实现新的消费者代码
  • 系统复杂度增加,引入了消息中间件
  • MQ是异步消费模型,存在延迟问题
📌
MQ 可以缓冲和 DB 的负载能力不一致问题。

方案 2.1:使用内存队列

如 golang 的 channel
  1. 把数据写入DB
  1. 把数据写入内存队列
  1. 消费线程异步从队列中消费数据写入ES(可以做批量优化,定时+定量)

优点

简单易实现,无需引入新的外部依赖

缺点

可能会丢消息;存在阻塞风险,容量有限

方案 2.2:使用分布式消息队列

如 RocketMQ、RabbitMQ、Kafka 一类的 MQ 中间件
  1. 把数据写入DB
  1. 把数据写入MQ
  1. 消费线程异步从MQ中消费数据写入ES

优点

数据可靠性高,支持数据持久化,不会出现数据丢失

缺点

系统架构更复杂,需要额外的 MQ 运维成本

方案三:定时同步

开定时任务回扫

优点

  • 实现简单
  • 适合作为其他方案的补充兜底,用于处理双写中的失败情况

缺点

  • 无法保证数据的实时性,存在较大的同步延迟
  • 对数据库的存储和计算压力较大,尤其是在数据量大时

方案四:数据订阅

如阿里的 Canal,订阅 MySQL 的 binlog(伪装成DB从节点),发 MQ 消息

优点

  • 业务入侵较少
  • 实时性较好

缺点

  • 系统复杂度增加,引入了消息中间件和额外组件,有一定维护成本(至少得保证 Canal 不能挂掉)