MySQL同步到ES的方案探索
Tags: ElasticSearchESMySQL数据同步数据迁移
前言
有了MySQL为什么还要有ES?要解答这个问题,就要来看看MySQL的不足和ES的优势了!
MySQL的不足
MySQL 架构天生不适合海量数据查询(一千万行以内),它只适合海量数据存储,但无法应对海量数据下各种复杂条件的查询。
有人说加索引不是可以避免全表扫描,提升查询速度吗,为啥说它不适合海量数据查询呢?
加索引也没法提升查询速度
加索引确实可以提升查询速度,但在 MySQL 中加多个索引最终在执行 SQL 的时候它只会选择成本最低的那个索引。
如果没有索引满足搜索条件,就会触发全表扫描,而且即便你使用了组合索引,也要符合最左前缀原则才能命中索引。但在海量数据多种查询条件下很有可能不符合最左前缀原则而导致索引失效。
每加一个索引就会创建一颗B+树,如果是海量数据那将大大增加存储成本,所以索引绝对不是越多越好的!
模糊查询的能力弱
有些查询条件是 MySQL 加索引都解决不了的,比如我要查询商品中所有 title 带有「格力空调」的关键词,如果你用 MySQL 写,会写出如下代码:
1 | SELECT * FROM product WHERE title like '%格力空调%' |
这样的话无法命中任何索引,会触发全表扫描,而且你不能指望所有人都能输对他想要的商品,是人就会犯错误,我们经常会犯类似把「格力空调」记成「格空间」的错误。那么 SQL 语句就会变成:
1 | SELECT * FROM product WHERE title like '%格空调%' |
这种情况下就算你触发了全表扫描也无法查询到任何商品,综上所述,MySQL 的查询确实能力有限。
ES的优势
与其说上面列的这些点是 MySQL 的不足,倒不如说 MySQL 本身就不是为海量数据查询而设计的。术业有专攻,海量数据查询还得用专门的搜索引擎,这其中 ES 是当之无愧的王者。它的特点很明显:
- 每个字段都被索引(ES中一切字段皆为索引),而且采用的是高效的倒排索引
- 有丰富的分词插件,支持自定义打分和排序,能够实现任意复杂条件的全文检索
- 支持分布式存储,轻松处理 PB 级别的结构化或非结构化数据。高可用,容灾性能好。
ES大宽表设计
为什么会存在大宽表嘞?假设有这么一个场景:在电商 APP 里,用户输入关键词来查询相对应的商品了,服务器要返回商品信息!
一个商品会有多个 sku(sku 即同一个商品下不同规格的品类,比如苹果手机有 iPhone 6,iPhone 6s 等),会有其基本属性如价格,标题等,商品会有分类(居家,服饰等),品牌,库存等。
为了保证表设计的合理性,我们会设计几张表来存储这些属性:product_sku(sku 表),product_property(基本属性表),sku_stock(库存表),product_category(分类表)
如果我们直接在MySQL中做join,海量商品的join操作可能会压垮数据库,而且MySQL还不支持我们分词搜索商品名称!
这个时候ES大宽表就诞生了,我们不访在ES中建一个大宽表,MySQL只要能保证实时地将多表数据更改同步到ES大宽表内就OK了,这样我们就可以摆脱MySQL实现快速精确的搜索了!
大家应该都在各种电商网站检索过商品,检索商品一般都是通过什么实现呢?搜索引擎Elasticsearch。
那么问题来了,商品上架,数据一般写入到MySQL的数据库中,那么用于检索的数据又是怎么同步到Elasticsearch的呢?
接下来我们将会探索一下实现MySQL到ES数据同步的技术方案!
1. 同步双写
这是一种最为简单的方式,在将数据写到mysql时,同时将数据写到ES。
- 业务逻辑很简单、实现简单
缺点:
- 硬编码,有需要写入mysql的地方都需要添加写入ES的代码;
- 业务强耦合,商品的管理中耦合大量数据同步代码
- 存在双写失败丢数据风险;
- 性能较差:本来mysql的性能不是很高,再加一个ES,系统的性能必然会下降。
上面说的双写失败风险,包括以下几种:
- ES系统不可用;
- 程序和ES之间的网络故障;
- 程序重启,导致系统来不及写入ES等。
针对这种情况,有数据强一致性要求的,就必须双写放到事务中来处理,而一旦用上事务,则性能下降更加明显。
2. 异步双写(MQ方式)
由于MQ的性能基本比mysql高出一个数量级,所以性能可以得到显著的提高。
我们也很容易想到异步双写的办法,上架商品的时候,先把商品数据丢进MQ,为了解耦合,我们一般会拆分一个搜索服务,由搜索服务去订阅商品变动的消息,来完成同步。
优点:
- 性能高
- 不存在丢失数据的问题
缺点:
- 还存在硬编码、业务强耦合等问题;
- 系统中增加了mq的代码,复杂度增加;
- 可能存在时延问题,程序的写入性能提高了,但是由于MQ的消费可能由于网络或其它原因导致用户写入的数据不一定可以马上看到。
3. 异步双写(worker方式)
上面两种方案中都存在硬编码问题,也就是有任何对mysq进行增删改查的地方要么植入ES代码,要么替换为MQ代码,代码的侵入性太强,若是实时要求不高的情况下,可以考虑用定时器来处理,具体步骤如下:
- 数据库的相关表中增加一个字段为timestamp的字段,任何crud操作都会导致该字段的时间发生变化;
- 原来程序中的crud操作不做任何变化;
- 增加一个定时器程序,让该程序按一定的时间周期扫描指定的表,把该时间段内发生变化的数据提取出来;
- 逐条写入到ES中。
优点:
- 不改变原来代码,没有侵入性、没有硬编码;
- 没有业务强耦合;
- 不改变原来程序的性能;
- Worker代码编写简单不需要考虑增删改查。
缺点:
- 时效性较差,由于定时器工作周期不可能设在秒级,所以实时性没有上面2中好;
- 对数据库有一定的轮询压力,一种改进方法是将轮询放到压力不大的从库上。
前面说的,一些数据需要聚合处理成类似宽表的结构怎么办呢?例如商品库的商品品类、spu、sku表是分开的,但是查询是跨维度的,在ES里再聚合一次效率就低一些,最好就是把商品的数据给聚合起来,在ES里以类似大宽表的形式存储,这样一来查询效率就高一些。
这种其实没什么好办法,基本上还是得搜索服务直接查库,或者远程调用,再查询一遍商品的数据库,就是所谓的回查。
定时任务
假如我们要快速搞搞,数据量有没那么大,怎么办呢?定时任务也可以。
定时任务,最麻烦的一点是频率不好选,频率高的话,会非自然地形成业务的波峰,导致存储的CPU、内存占用波峰式上升,频率低的话实时性比较差,而且也有波峰的情况。
这种方式:
优点:实现比较简单
缺点:
- 实时性难以保证
- 对存储压力较大
4. Binlog同步方式(数据订阅)
上面三种方案要不有代码侵入、要不有硬编码、要不有时延,那么有没有一种更好的方法嘞?答案就是利用mysql的binlog!
具体步骤如下:
- 读取mysql的binlog日志,获取指定表的日志信息;
- 将读取的信息转为MQ;
- 编写一个MQ消费程序;
- 不断消费MQ,每消费完一条消息,将消息写入到ES中。
优点:
- 没有代码侵入、没有硬编码;
- 原有系统不需要任何变化,没有感知;
- 性能高
- 业务解耦,不需要关注原来系统的业务逻辑
缺点: