20年架构师带你彻底搞懂查询分离的实现思路

本文详细阐述了查询分离的实现思路,包括触发查询分离的三种逻辑、使用MQ进行异步处理、查询数据的存储选择(Elasticsearch)、查询数据的使用注意事项以及历史数据的迁移方法。
摘要由CSDN通过智能技术生成

查询分离实现思路

如图2-2所示,查询分离的实现思路如下。

1)如何触发查询分离?

2)如何实现查询分离?

3)查询数据如何存储?

4)查询数据如何使用?

5)历史数据如何迁移?

• 图2-2 查询分离需要考虑的问题

下面针对以上5个问题的解决方案进行展开。

如何触发查询分离

这个问题是说应该在什么时候保存一份数据到查询数据库,即什么时候触发查询分离这个动作。

一般来说,查询分离的触发逻辑分为3种。

1)修改业务代码,在写入常规数据后同步更新查询数据。如图2-3所示,每次客服单击更新工单的按钮后,在处理该动作的请求线程当中,除了更新工单数据外,还要调用一个更新工单查询数据的操作。直到这些操作都完成以后,再返回请求结果给客服。

• 图2-3 修改业务代码同步更新查询数据

2)修改业务代码,在写入常规数据后,异步更新查询数据。如图2-4所示,客服单击更新工单的按钮后,在处理该动作的请求线程当中,更新工单数据,而后异步发起另外一个线程去更新工单数据到查询数据库。不用等到查询数据更新完成,就直接返回请求结果给客服。

• 图2-4 修改业务代码异步更新查询数据

3)监控数据库日志,如有数据变更,则更新查询数据。这个设计不会影响业务代码。如图2-5所示,监控主数据库的数据库日志文件(binlog),一旦发现有变更,就触发工单数据的更新操作,去更新查询数据。

• 图2-5 监控数据库日志更新查询数据

以上3种触发逻辑的优缺点见表2-1。

表2-1 3种触发逻辑的优缺点

为方便理解表2-1中的内容,下面就其中几个概念展开说明。

(1)业务逻辑灵活可控

一般来说,写业务代码的人能从业务逻辑中快速判断出何种情况下更新查询数据,而监控数据库日志的人并不能将全部的数据库变更分支穷举出来,再把所有的可能性关联到对应的查询数据更新逻辑中,最终导致任何数据的变更都需要重新建立查询数据。

(2)减缓写操作速度

更新查询数据的一个动作能减缓多少写操作速度?答案是很多。举个例子:当只是简单更新了订单的一个标识时,本来更新这个字段的时间只需要2毫秒,但是去更新订单的查询数据时,可能会涉及索引重建(比如使用Elasticsearch查询数据库时,会涉及索引、分片、主从备份,其中每个动作又细分为很多子动作,这些内容后面的场景会讲到),这时更新查询数据的过程可能就需要1秒了。

(3)查询数据更新前,用户可能查询到过时数据

这里结合第2种触发逻辑来讲。比如某个操作正处于订单更新状态,状态更新时会异步更新查询数据,更新完后订单才从“待审核”状态变为“已审核”状态。假设查询数据的更新时间需要1秒,这1秒中如果用户正在查询订单状态,这时主数据虽然已变为“已审核”状态,但最终查询的结果还是显示为“待审核”状态。

根据表1-2中的对比,可总结出3种触发逻辑的适用场景,见表2-2。

表2-2 3种触发逻辑的适用场景

在与客服的沟通中得知,她们的工作状态一般是一边接线一边修改工单,所以希望工单页面的反应要快一些,也就是说,她们对写操作的响应速度有要求;另外,项目组成员对业务代码比较熟悉,有办法找到所有修改工单的代码。

基于这两点考虑,项目组最后选择了第2种方案:修改所有与工单写操作有关的业务代码,在更新完工单数据后,异步触发更新查询数据的逻辑,而后不等查询数据更新完成,就直接返回结果给客服。

触发查询分离的方式考虑清楚了,接下来就要考虑如何实现查询分离。

 如何实现查询分离

项目组选择的是第2种触发方案:修改业务代码异步更新查询数据。最基本的实现方式是单独启动一个线程来创建查询数据,不过使用这种做法要考虑以下情况。

1)写操作较多且线程太多时,就需要加以控制,否则太多的线程最终会拖垮JVM。

2)创建查询数据的线程出错时,如何自动重试?如果要自动重试,是不是要有个地方标识更新失败的数据?

3)多线程并发时,很多并发场景需要解决。

面对以上3种情况,该如何处理?此时就可以考虑使用MQ(MessageQueue,消息队列)来解决这些问题了。

MQ的具体操作思路为,每次程序处理主数据写操作请求时,都会发一个通知给MQ,MQ收到通知后唤醒一个线程来更新查询数据,如图2-6所示。

• 图2-6 MQ触发查询数据更新示意图

了解MQ的具体操作思路后,还应该考虑以下5个问题。

问题1:MQ如何选型?

如果公司已经使用MQ,那选型问题也就不存在了,毕竟技术部门不会同时维护两套MQ中间件,如果公司还没有使用MQ,就需要考虑选型的问题了。

MQ的选型建议如下。

1)召集技术中心所有能做技术决策的人共同投票选型。

2)在并发量不高的情况下,不管选择哪个MQ,最终都能实现想要的功能,只不过存在是否易用、业务代码多少的区别,因此从易用性考量即可。当然,前提是支持自己使用的编程语言。

Rabbit MQ、Rocket MQ、Kafka、Active MQ、Redis都有实际应用,当然每一家公司都会有一份很严谨的选型报告,证明它们的选型是最正确的。

问题2:MQ宕机了怎么办?

考虑MQ宕机的情况有以下场景。

1)工单A更新后要通知MQ,但是MQ宕机了,于是MQ没有这条消息,出现消息丢失的情况。

2)MQ收到消息,然后消费者读到消息,但是MQ宕机了,于是MQ不知道消费者是不是已经消费成功,可能造成消息重复投递。

如果MQ宕机了,项目组只需要保证主流程正常进行,且MQ恢复后数据正常处理即可,具体方案分为3步。

1)每次进行写操作时,在主数据中加标识NeedUpdateQueryData=true,这样发到MQ的消息就很简单,只是一个简单的信号来告知更新数据,并不包含更新的数据ID。

2)MQ的消费者获取信号后,先批量查询待更新的主数据,然后批量更新查询数据,更新完成后查询数据的主数据标识NeedUpdateQueryData更新为false。

3)若存在多个消费者同时有迁移动作的情况,就涉及并发性的问题,这与前一场景冷热分离中的并发性处理逻辑类似,这里不再赘述。

结合以上处理过程,再分析一下前面的两个MQ宕机场景。

1)工单A更新后要通知MQ,但是MQ宕机了,于是MQ没有这条消息,出现消息丢失的情况。等MQ恢复后,假设工单B也更新了,此时触发了一个消费者线程,这个线程会查询NeedUpdateQueryData=true的数据,结果工单A和B都被查询到了。这两个工单都将被同步到查询数据库。

2)MQ收到消息,然后消费者读到消息,但是MQ宕机了,于是MQ不知道消费者是不是已经消费成功,可能造成消息重复投递。假设工单A更新后,MQ收到一条消息,然后消费者消费了这条消息,同步了工单A,但是在回调给MQ告知消费成功时,MQ宕机了,于是MQ不知道这条消息已经被消费,它恢复后又投递了同步工单的消息。此时消费者收到消息后,去查询数据库,但是其实工单A已经同步,NeedUpdateQueryData标识改成了false,待更新工单不再包含工单A,所以消息重复投递问题也解决了。

问题3:更新查询数据的线程失败了怎么办?

如果更新的线程失败了,NeedUpdateQueryData标识就不会更新,后面的消费者会再次将有NeedUpdateQueryData标识的数据拿出来处理。但如果一直失败,可以在主数据中添加一个尝试迁移次数,每次尝试迁移时将其加1,成功后就清零,以此监控那些尝试迁移次数过多的数据。

问题4:消息的幂等消费。

再梳理一下同步步骤。

1)更新工单并且将工单的NeedUpdateQueryData改为true。

2)连接MQ生产消息。

3)MQ投递消息给消费者。

4)消费者获取NeedUpdateQueryData为true的工单。

5)消费者将前面获取的工单同步到查询数据库。

6)消费者将主数据库中相应工单的字段NeedUpdateQueryData改为false。

7)消费者回调给MQ,告知消息已经被消费。

为什么要考虑幂等的情况呢?举一个例子,当执行完上面的步骤5)之后,突然网络出问题了,接下来的步骤6)、7)就没有被执行。这种情况下,经过一定时间后,这条消息就会被重试,那么上面的步骤5)就会重复执行。

这里的幂等就是要保证步骤5)可以重复执行多次,而且得到的最终结果是一致的。

问题5:消息的时序性问题。

比如某个订单A更新了一次数据变成A1,线程甲将A1的数据迁移到查询数据中。不一会儿,后台订单A又更新了一次数据变成A2,线程乙也启动工作,将A2的数据迁移到查询数据中。

这里的时序性问题是,如果线程甲启动比乙早,但迁移数据的动作比线程乙还要慢,就有可能导致查询数据最终变成过期的A1,如图2-7所示,动作前面的序号代表实际动作的先后顺序。

• 图2-7 时序性问题示意图

此 时 解 决 方 案 为 主 数 据 每 次 更 新 时 , 都 更 新 上 次 更 新 的 时 间last_update_time,然后每个线程更新查询数据后,检查当前工单A的last_update_time 是 否 与 线 程 刚 开 始 获 得 的 时 间 相 同 , 以 及NeedUpdateQueryData 是 否 等 于 false , 如 果 都 满 足 , 就 将NeedUpdateQueryData改为true,然后再做一次迁移。

此处读者心中可能有个疑问:MQ在这里的作用只是一个触发信号的工具,如果不用MQ似乎也可以。其实不然,这里MQ的作用如下。

1)服务的解耦:这样主业务逻辑就不会依赖更新查询数据这个服务了。

2)控制更新查询数据服务的并发量:如果直接调用更新查询数据服务,因写操作速度快,更新查询数据速度慢,写操作一旦并发量高,就会造成更新查询数据服务的超载。如果通过消息触发更新查询数据服务,就可以通过控制消息消费者的线程数来控制负载。

接下来再看一下,查询数据如何存储。

 查询数据如何存储

应 该 使 用 什 么 技 术 来 存 储 查 询 数 据 呢 ? 目 前 开 发 者 们 主 要 使 用Elasticsearch实现大数据量的搜索查询,当然还可能用到MongoDB、HBase这些技术,这就需要开发者对各种技术的特性了如指掌后再进行技术选型。

前面已经介绍了HBase。它可以存储海量数据,但是其设计初衷并不是用来做复杂查询,即使可以做到,效率也不高。而此处的工单查询复杂度很高,所以项目组最后锁定的两个选项是MongoDB和Elasticsearch。

关于技术选型这个问题,很多时候不能只考虑业务功能的需求,还需要考虑 人 员 的 技 术 结 构 。 比 如 在 这 个 项 目 中 , 设 计 架 构 方 案 时 选 用 了Elasticsearch,之所以这样,除Elasticsearch对查询的扩展性支持外,最关键的一点是团队对Elasticsearch很熟悉,但是没有人熟悉MongoDB。运维人员也没有MongoDB的运维经验。

现在查询分离中的写部分已经完成了,接下来考虑读的部分。

 查询数据如何使用

数据存到Elasticsearch以后,就要查询了。那查询的时候要注意什么呢?

因Elasticsearch自带API,所以使用查询数据时,在查询业务代码中直接调用Elasticsearch的API即可。至于Elasticsearch的API怎么用,这里就不讲了。

不过要考虑一个场景:数据查询更新完前,查询数据不一致怎么办?举一个例子:假设更新工单的操作可以在100毫秒内完成,但是将新的工单同步到Elasticsearch需要2秒,那么在这2秒内,如果用户去查询,就可能查询到旧的工单数据。

这里分享两种解决思路。

1)在查询数据更新到最新前,不允许用户查询。笔者团队没用过这种方案,但在其他实际项目中见到过。

2)给用户提示:“您目前查询到的数据可能是2秒前的数据,如果发现数据不准确,可以尝试刷新一下。”这种提示用户一般都能接受。

历史数据迁移

新的架构方案上线后,旧的数据如何适应新的架构方案?这是实际业务中需要考虑的问题。

在 这 个 方 案 里 , 只 需 要 把 所 有 的 历 史 数 据 加 上 标 识NeedUpdateQueryData=true,程序就会自动处理了。

MQ+Elasticsearch的整体方案

以上小节已经把5个问题都讨论完了,再一起看下查询分离的整体方案。

整个方案的要点如下。

1)使用异步方式触发查询数据的同步。当工单修改后,会异步启动一个线程来同步工单数据到查询数据库。

2)通过MQ来实现异步的效果。MQ还做了两件事:①服务的解耦,将工单主业务系统和查询系统的服务解耦;②削峰,当修改工单的并发请求太多时,通过MQ控制同步查询数据库的线程数,防止查询数据库的同步请求太大。

3)将工单的查询数据存储在Elasticsearch中。因为Elasticsearch是一个分布式索引系统,天然就是用来做大数据的复杂查询的。

4)因为查询数据同步到Elasticsearch会有一定的延时,所以用户可能会查询到旧的工单数据,所以要给用户一些提示。

5)关于历史数据的迁移,因为是用字段NeedUpdateQueryData来标识工单是否需要同步,所以只要把所有历史数据的标识改成true,系统就会自动批量将历史数据同步到Elasticsearch。整个方案如图2-8所示。

• 图2-8 整体方案示意图

这个整体方案看似简单,但是其中有一些陷阱必须注意。下篇着重介绍一下使用Elasticsearch时的注意事项。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值