500W数据动态同步到ES

关注公众号【1024个为什么】,及时接收最新推送文章!  

||  背景

| 表背景

阿里云 MySQL 5.7

trade表,数据量500W,占磁盘空间12G多,41列,平均每条数据2.5K左右

主键是orderId(19位,趋势递增),

status,取值范围 0-7

type,取值范围0-10

business_id,取值范围10个值左右

create_time, update_time 都有索引

| 业务背景

trade表要支撑两种业务场景:

一种是常规的订单交易流程,基本都是按orderId检索,不存在效率问题,可忽略。

另一种是管理后台(查询兼统计功能),很多检索条件,其中就包含很多像 status、type这种区分度很低、建索引也不起任何作用的检索条件,页面还带有分页功能,只要检索条件带有区分度低的条件,count语句就等同于全表扫描,耗时都是分钟级别。

目标:在不改变现有业务诉求的前提下,对管理后台进行优化改造,达到秒级响应的效果。

||  方案调研

上述场景很明显已经不是MySQL擅长的领域了,所以要换一个角度来解决问题。

Elasticsearch(ES)就很适合这种场景(不用分词技术有点大材小用了)。损失一些写性能、以空间换时间(同样的数据放在ES中占用的磁盘空间会翻很多倍)来提升读性能。

经过测试,上面区分度低的查询场景,响应都在200ms以内。

所以,大方向是常规业务流程还走MySQL,后台查询走ES。

接下来讨论的方案就基于这个方向,而这个方向最大的难题是,MySQL和ES的数据如何同步。

| 方案一,同步双写

写库的地方都写一次ES。


tradeDao.insert(trade);
esClient.save(trade);
return;

优点:易于理解,实现成本低,只要有插入/修改的地方,同步写入ES即可,实时性高,数据可以做到强一致(同一事务)

缺点:会增加接口的耗时,ES写入失败时难处理,回滚会影响正常的业务流程,不回滚会造成数据不一致。与业务耦合严重,扩展成本高,后期有新操作trade表的入口都要同步操作ES

| 方案二,异步双写

新起一个线程,执行写ES的操作。


tradeDao.insert(trade);
ThreadPool.getExecutor().execute(() -> {
    esClient.save(trade);
});
return;

优点:即不增加现有接口耗时,又可以保证实时性

缺点:插库成功,写ES失败,无法再通过事务回滚+重试保证数据一致性,写入ES失败造成数据丢失,想要再次触发写入ES,成本会很高

| 方案三,异步MQ

通过发、收MQ消息,实现数据同步。

tradeDao.insert(trade);
MqUtil.send(trade);
return;
MqUtil.receive(trade){
    Trade trade = tradeDao.selectByPk(trade.getOrderId());
    esClient.save(trade);
}

优点:可以弥补方案二的缺点,写ES失败,重发MQ就可以保证数据最终一致

缺点:时效性相比前两个方案会差一些,还有乱序、消息丢失的风险;引入了新的服务组件,使得系统架构变的复杂,涉及服务越多,出问题的概率就越高

这三个方案存在一个共同的问题,和业务代码耦合严重。

后期有新写库的场景都要考虑重写ES;

trade表列如果有调整,所有写ES的地方都要做出相应的修改。

| 方案四,定时任务

通过定时任务实现数据同步。

synJob{
    Trade trade = tradeDao.selectByPk(trade.getOrderId());
    esClient.save(trade);
}

优点:与业务解耦,实现成本也不高,扩展成本低

缺点:业务变更频率不固定导致任务的频率不好控制,频率高了浪费资源,频率低了时效性差或者单次同步的记录太多。

| 方案五,基于binlog

使用第三方的服务插件,监听binlog,实现数据同步。写入ES的动作可以放在server,也可以放在类似Job的共用服务中。

优点:与业务解耦,可以支撑高并发,扩展成本低

缺点:实现成本高,架构变得更加复杂

最后选择的是方案五,主要原因是团队已经搭建了DTS,我只需配置要监听的表,订阅对应的消息实现同步逻辑即可。由于省去了最初的搭建成本,所以实现成本相对低一些。

如果对实时性要求不太高,感觉定时任务是一个不错的选择。选择哪个方案还是要看具体的业务场景和团队的基础架构,能满足业务需求的前提下,选择一个综合成本低的。

||  方案实施

上面的方案主要是针对增量(本文所有增量均包含新增和修改)数据的,要实现历史数据和增量数据都能准确的同步到ES,还有很多问题需要解决。

| 如何在最短时间内同步完历史数据?

这个问题主要考虑查询MySQL和写入ES的速度。

limit 肯定是不能用的,开头提到了主键ID是趋势递增的,可以按主键ID分页查,每次查询记录最后一条记录的主键ID,作为下次查询的起始条件。经过测试(500、1000、1500、2000)得出,每次1000条,查询+写入的耗时是最短的。

SELECT * FROM trade WHERE orderId > #{lastPkId} limit 1000;

| 数据一直在增加,同步历史数据的结束条件是什么?

上面的1000条也不是随意定的,同步1000条的耗时大概是200ms,如果业务量大,200ms内新增数据一直>1000条,那么上面的任务就永远无法结束了。这个1000条对我们的业务量来说是绰绰有余,相信对绝大多数公司来说都是够用的。

假设业务量就是大,调成每次处理2000条又不是最快的怎么办?

可以在开启binlog同步通道(见下图)时记录下同步的第一条主键ID sysPkId,每次查询前比较一下 lastPkId < sysPkId,直到条件不成立,就代表历史数据同步结束。 

| 已同步的数据,又发生变更怎么办?

这一点需要通过上线、任务开启的顺序来保证。

要先开启binlog同步,这样就可以保证增量数据通过这个通道同步到ES。

再开启同步历史数据的任务Job1,这个问题就得到解决了。

| 取出了一批历史数据,这批数据未全部写入ES前,其中某条数据变更了怎么办?

这种情况会造成两边数据不一致,ES中本来通过binlog同步过来的新数据被任务里的旧数据覆盖掉。这里没必要每条记录都做比较,毕竟发生概率很低,新起一个任务Job2,保证最近某个时间段的(必须大于同步历史数据的耗时)数据一致性,就可以弥补这个问题。

| DTS通道问题,导致两边数据不一致了,如何发现、修复?

还是借助Job2,查询最近1天(业务量大也可以按小时)的增量数据,取出主键ID,再去ES查询,两边结果做比对,不一致的以MySQL为准直接覆盖ES(或者也可以加一个时间对比逻辑,以最后更新的为准),ES缺少的直接同步过去。既可以发现差异,又可以修复差异。

| MQ服务问题,导致乱序,如何保证新数据不被旧数据覆盖?

写入ES前,先从ES查询,比较最后更新时间,以最后更新的为准。

orderId维度,变更不会这么频繁,几乎不会出现这种情况。

先开启1,再开启2,等2结束了,再开启3,最后1、3并存,保证数据的一致性。

Job1要保留,万一trade表加了一列,ES也要加列,还需要依靠Job1再同步一次历史数据,再来一遍上面的流程。

||  总结

1、让技术用在它擅长的领域

2、选择最适合自己业务场景的方案

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值