场景引入

如果你设计了一个类似 B 站的视频网站中的搜索系统,或许在初期 MySQL 的 like 模糊搜已经能满足你对视频的标题、简介的搜索需求。

SELECT * FROM videos
WHERE title LIKE '%'||:search_term||'%'
OR description LIKE '%'||:search_term||'%';

如果后期系统视频的量级上来了,对性能有一定要求了,你或许已经开始考虑 ES 了。后面如果你还想让与搜索关键词相关的创作者名称、视频评论、关联标签也能级联视频搜出来,甚至弄一个各种业务维度的融合公式,那么 ES 这样的搜索引擎再适合不过了!

但是,如果数据写一份 MySQL,还要落一份 ES 来支撑筛选/搜索,要怎么写入两个异构的存储系统、并保证它们的一致性呢?

[!NOTE] 如果你有更好的场景欢迎提出/补充~

方案一:同步双写

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

方案二:异步双写

  • 优点
    • 性能高
    • 不易出现数据丢失问题,主要基于 MQ 消息的消费保障机制,比如 ES 宕机或者写入失败,还能重新消费 MQ 消息
    • 多源写入之间相互隔离(业务侧只管发消息),解耦数据生产者和消费者,便于扩展更多的数据源写入
  • 缺点
    • 同样存在业务耦合的问题,写完 DB 之后,还需要发个 MQ(代码设计上做好收敛,也可以关注一下所用 orm 框架有没有 hook 之类的能力,支持你“写后 xxx”)
    • 接入新的数据源需要实现新的消费者代码
    • 系统复杂度增加,引入了消息中间件
    • MQ是异步消费模型,存在延迟问题

[!tip] MQ 可以缓冲和 DB 的负载能力不一致问题

方案 2.1:使用内存队列

如 golang 的 channel

  1. 把数据写入DB
  2. 把数据写入内存队列
  3. 消费线程异步从队列中消费数据写入ES(可以做批量优化,定时+定量)
  • 优点:简单易实现,无需引入新的外部依赖
  • 缺点:可能会丢消息;存在阻塞风险,容量有限

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

如 RocketMQ、RabbitMQ、Kafka 一类的 MQ 中间件

  1. 把数据写入DB
  2. 把数据写入MQ
  3. 消费线程异步从MQ中消费数据写入ES
  • 优点:数据可靠性高,支持数据持久化,不会出现数据丢失
  • 缺点:系统架构更复杂,需要额外的 MQ 运维成本

方案三:定时同步

开定时任务回扫

  • 优点
    • 实现简单
    • 适合作为其他方案的补充兜底,用于处理双写中的失败情况
  • 缺点
    • 无法保证数据的实时性,存在较大的同步延迟
    • 对数据库的存储和计算压力较大,尤其是在数据量大时

方案四:数据订阅

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

  • 优点
    • 业务入侵较少
    • 实时性较好
  • 缺点
    • 系统复杂度增加,引入了消息中间件和额外组件,有一定维护成本(至少得保证 Canal 不能挂掉)