你好,我是韦木,曾是硅谷上市公司的一名技术总监,拥有 15 年互联网技术研发经验。
随着社会节奏的日益加快,碎片化学习逐渐成了我们获取知识的主要方式。在技术学习上,我也是一个碎片化学习的人,也因此我发现学习到的东西往往零散琐碎、不系统,只是让我感觉好像学到了很多东西。
犹记得刚学习 Spring 时,每当看到 Spring 的代码示例,我先是恍然大悟:“哦,原来 Spring 还有这个功能啊”,然后赶紧把这段代码示例拷贝放入自己的代码库里。
琢磨一番后:“哎呀,不行,我还是得完整掌握 Spring”。于是乎,我又在网络上大肆寻找完整的 Spring 学习文档,不过利用碎片化时间看完一半后,我还是决定放弃了。
主要碎片化学习知识时,我往往追求实用,用得上的知识我记得快,而那些用不到或不理解使用场景的知识我完全记不住,每次看完就忘光,一直这样循环往复。我相信大部分人也都跟我一样,往往是真正遇到问题时才会去想对应的解决方案。
来看看我自身的学习经历。我是什么时候开始能完整看完 Spring 的官方文档的呢?是在明白了 Spring 大部分功能的使用场景后。
同样的事情也发生在我的 Spark 学习之路上,我曾有过多次 Spark 从入门到放弃的经历,直到有一天碰到了一个实际业务问题——需要定期分析大量数据并生成分析结果,在解决这个问题的过程中,我才真正深入地了解了 Spark。
这就跟有些人一直不明白架构师到底是做什么的一样,直到有一天,他们遇到了一个具体的问题,摸索出了一个可行的方案,才明白:原来架构师是这样解决问题的。
因此,如果想要学好软件架构,基于场景的学习方式最有效。一旦理解了业务场景,我们就能轻易看懂某个解决方案,并理解解决方案背后的实现原理。
于是乎,我就想,市面上有没有这样一门课程:
它没有教条,没有理论,就像讲故事一样,将个人实战架构经历娓娓道来;
它先讲清楚需要解决的问题,然后诉说个人架构心路历程,再将实现思路串起来阐述整体方案是什么样的,最终引申出解决方案的不足及其他延伸思路。
然而,做了大量市场调研后,我就在想,我可不可以做一门这样的课程,来填补这块空缺呢?
我深耕职场 15 余载,大厂、小厂、大型民企、外企都待过,其间涉猎过不少互联网项目,大大小小的项目至少 30 个,10 余次从 0~1 搭建完整的技术架构方案,架构升级改造 20 多次。
几十次的架构经历中,有些因与业务紧密结合无法单独拿出来,有些可以从业务特殊需求中剥离出来变成技术思路上通用的解决方案,其中可以抽取归纳的架构经历共 16 次,我在这里已将这 16 次真实的架构经历整理成一套知识体系,方便你更加系统化地理解内容,最终内化为自己的知识。
根据架构设计的立足点,我将本专栏划分为了 6 大模块。
模块一:数据持久化层场景实战。 这部分主要讲解存储的数据量太大影响读写性能时,如何在存储层做文章解决性能问题,学完这个模块后,一旦你遇到数据量大的问题,可以直接从我的 16 次架构经历中找到参考答案。
模块二:缓冲层场景实现。 这部分主要讲解大流量时,如何避免流量直接压垮数据库层,学完这个模块后,当你遇到缓存层场景问题,你就知道如何进行架构设计了。
模块三:基于常用组件的微服务场景实战。 这部分主要讲解业务逻辑分布在不同的服务时,如何使用市面上一些常见的组件解决碰到的各种问题。学完这个模块,你能快速掌握一些微服务的基本原理,并灵活地组合市面上一些常见组件,或结合自研的一些框架解决微服务场景问题。
模块四:实际场景解说微服务的痛。 在你学完基于常用组件的微服务场景实战内容后,这个模块将用各种真实经历,让你提前体会在大公司使用微服务时会面临的一些问题。
模块五:无常用组件可用的微服务场景实战。 这部分内容主要讲解当一些场景没有常用组件可用时,该如何解决?这个模块将通过真实的架构经历帮助你解决在大公司使用无常用组件可用的微服务所面临的一些问题。
模块六:开发运维场景实战。 这部分主要讲解如何通过一些解决方案加快开发效率和测试微服务的效率。
其中,我还会穿插一些课时,专门讲解在解决方案中使用相应技术会碰到的问题。比如使用 ES 时,分页、延时等问题如何解决?再比如使用微服务时,整个团队会面临什么样的问题?为什么大家都在说康威定律?这些问题在面试中时常会被问到,因此这部分内容对架构面试的帮助非常大,你有必要好好体会。
工作至今,我带领团队已 10 余年,10 人以下或者 100 多人的团队我都带过。
不开玩笑,程序员的现实世界里,不想当架构师的程序员不是好程序员,况且就算你未曾主动想去当架构师,现实有时也会把你推到那个位置,而提前设计好自己的职业发展路径,远好过被动等待。
而如果你想晋升为一名软件架构师,需要同时具备架构思维和架构经历。 那这两个要素如何快速积累?前者可以通过学习,而后者需要机会。
在带团队的过程中,我发现不同的程序员,其提交的代码质量及功能交付速度各有不同,后来我发现他们之间的差距在于看问题的视角不同,即所谓的“架构思维”。比如有些程序员只知道自己设计的一些功能,或者只知道自己负责的那几个类,而那些优秀的程序员清楚整个架构的全局如何运作,以及个人负责的代码在架构全局中起到什么关键作用。
一个人的架构设计思维一旦形成,会对其系统架构设计能力产生重大影响,也直接决定着一个架构师解决问题域的复杂性和规模大小。
前面我们提及架构经历必须靠机会,那机会如何而来呢?
我来举个小例子,比如:某天,CTO 碰巧遇到一个架构问题需要找人突破,而团队中碰巧有一个人研究过类似场景,懂得如何使用一些组合技术解决这个问题,你猜这位 CTO 会选谁来试一下?
再比如:在架构师面试过程中,面试官往往会让你聊聊实际开发经历,旨在考察你对业务场景的理解、解决问题的思路、考虑问题的全面性及解决方案的熟悉度。如果在此之前,你已将相关架构经历做了归纳总结,那回答时肯定得心应手,侃侃而谈,Offer 也是手到擒来。
所以,机会就是这样来的,并不会凭空而降,因为机会都是留给有准备的人。
本专栏,我将结合自身 16 次真实架构经历,完完整整地将架构设计实现过程呈现出来,变相地为你增加 16 次架构经历,并且通过各种场景帮你巩固架构的一些实现原理和设计知识。使你学完本专栏后,不仅可以获得更多解决架构问题的机会,面试架构师的成功率也会高一些,离架构师这个目标也就越来越近。
我们只有先懂场景才能学好架构,相信你学完这个专栏之后,无论是全局的架构思维上,还是面试思路展现上,抑或工作难点突破上都会得到全面的提升。
让我们一起学好软件架构,尽早地升职加薪吧。
今天,我要跟你分享的内容是冷热分离,也许你对这个概念并不陌生,对其使用场景也比较熟悉,但涉及锁的内容时仍然需要认真思考,这部分内容在我们实际开发中的“坑”还是不少的。
我曾经做过供应链相关的架构优化,当时我们平台有一个订单功能,里面的主表有几千万的数据量,加上关联表,数据量达到上亿。
这么庞大的数据量,让平台的查询订单变得格外迟缓,查询一次都要二三十秒,而且多点击几次就会出现宕机。比如业务员多次查询时,数据库的 CPU 会立马狂飙,服务器线程也降不下来。
当时,我们尝试了优化表结构、业务代码、索引、SQL 语句等办法来提高响应速度,但这些方法治标不治本,查询速度还是很慢。
考虑到我们手头上还有其他优先级高的需求需要处理,为此,我们跟业务方反馈:“这功能以后你们能不用就不用,暂时先忍受一下。”可经过一段时间后,业务方实在受不了了,直接跟我们放狠话,无奈之下我们屈服了。
最终,我们决定采用一个性价比高的解决方案,简单方便地解决了这个问题。在处理数据时,我们将数据库分成了冷库和热库 2 个库,不常用数据放冷库,常用数据放热库。
通过这样的方法处理后,因为业务员查询的基本是近期常用的数据,常用的数据量大大减少了,就再也不会出现宕机的情况了,也大大提升了数据库响应速度。
其实上面这个方法,就是“冷热分离”。
冷热分离就是在处理数据时将数据库分成冷库和热库 2 个库,冷库指存放那些走到了终态的数据的数据库,热库指存放还需要修改的数据的数据库。
假设你的业务需求出现了如下情况,就可以考虑使用冷热分离的解决方案:
数据走到终态后,只有读没有写的需求,比如订单完结状态;
用户能接受新旧数据分开查询,比如有些电商网站默认只让查询 3 个月内的订单,如果你要查询 3 个月前的订单,还需要访问另外的单独页面。
在实际操作过程中,冷热分离整体实现思路如下:
(一)如何判断一个数据到底是冷数据还是热数据?
(二)如何触发冷热数据分离?
(三)如何实现冷热数据分离?
(四)如何使用冷热数据?
冷热分离解决方案实施步骤鱼骨图
接下来,我们针对以上 4 个问题点进行详细的讲解。
(一)如何判断一个数据到底是冷数据还是热数据?
一般而言,在判断一个数据到底是冷数据还是热数据时,我们主要采用主表里的 1 个或多个字段组合的方式作为区分标识。其中,这个字段可以是时间维度,比如“下单时间”这个字段,我们可以把 3 个月前的订单数据当作冷数据,3 个月内的当作热数据。
当然,这个字段也可以是状态维度,比如根据“订单状态”字段来区分,已完结的订单当作冷数据,未完结的订单当作热数据。
我们还可以采用组合字段的方式来区分,比如我们把下单时间 > 3 个月且状态为“已完结”的订单标识为冷数据,其他的当作热数据。
而在实际工作中,最终究竟使用哪种字段来判断,还是需要根据你的实际业务来定。
关于判断冷热数据的逻辑,这里还有 2 个注意要点必须说明:
如果一个数据被标识为冷数据,业务代码不会再对它进行写操作;
不会同时存在读冷/热数据的需求。
(二)如何触发冷热数据分离?
了解了冷热数据的判断逻辑后,我们就要开始考虑如何触发冷热数据分离了。一般来说,冷热数据分离的触发逻辑分 3 种。
1.直接修改业务代码,每次修改数据时触发冷热分离(比如每次更新了订单的状态,就去触发这个逻辑),如下图所示:
触发逻辑一:直接修改业务代码
2.如果不想修改原来的业务代码,可通过监听数据库变更日志 binlog 的方式来触发,如下图所示:
触发逻辑二:监听数据库变更日志
3.通过定时扫描数据库的方式来触发,如下图所示:
触发逻辑三:定时扫描数据库
针对以上 3 种触发逻辑,我们到底选哪种比较好呢?待我分析完各自优缺点后,你心里就会有答案了。
冷热分离 3 种触发逻辑优缺点对比表
根据以上表格内容对比,我们可以得出每种触发逻辑的建议场景。
修改写操作的业务代码:建议在业务代码比较简单,并且不按照时间区分冷热数据时使用。
监听数据库变更日志:建议在业务代码比较复杂,不敢随意变更,并且不按照时间区分冷热数据时使用。
定时扫描数据库:建议在按照时间区分冷热数据时使用。
我做架构那会儿就是按照时间区分冷热数据,所以选用了定时扫描数据库的触发方式。因此,到底选择哪种触发方式,还是需要根据你的具体业务需求决定。
(三)如何分离冷热数据?
在讲解如何分离冷热数据之前,我们先来了解下分离冷热数据的基本逻辑,只有掌握了基本原理,才能真正理解事物本质。
分离冷热数据的基本逻辑如下:
判断数据是冷是热;
将要分离的数据插入冷数据库中;
再从热数据库中删除分离的数据。
冷热数据分离示意图
这个逻辑看起来简单,而实际做方案时,以下 3 点我们都得考虑在内,这就一点不简单了。
(1)一致性:同时修改多个数据库,如何保证数据的一致性?
这里提到的一致性要求,指我们如何保证任何一步出错后数据还是一致的,解决方案为只要保证每一步都可以重试且操作都有幂等性就行,具体逻辑分为四步。
在热数据库中,给要搬的数据加个标识: ColdFlag=WaittingForMove。(实际处理中标识字段的值用数字就可以,这里是为了方便理解。)
找出所有待搬的数据(ColdFlag=WaittingForMove):这步是为了确保前面有些线程因为部分原因失败,出现有些待搬的数据没有搬的情况。
在冷数据库中保存一份数据,但在保存逻辑中需加个判断以此保证幂等性(这里需要用事务包围起来),通俗点说就是假如我们保存的数据在冷数据库已经存在了,也要确保这个逻辑可以继续进行。
从热数据库中删除对应的数据。
(2)数据量:假设数据量大,一次性处理不完,该怎么办?是否需要使用批量处理?
前面讲了 3 种冷热分离的触发逻辑,前 2 种基本不会出现数据量大的问题,因为每次只需要操作那一瞬间变更的数据,但如果采用定时扫描的逻辑就需要考虑数据量这个问题了。
这个实现逻辑也很简单,在搬数据的地方我们加个批量逻辑就可以了。为方便理解,我们来看一个示例。
假设我们每次可以搬 50 条数据:
a. 在热数据库中给要搬的数据加个标识:ColdFlag=WaittingForMove;
b. 找出前 50 条待搬的数据(ColdFlag=WaittingForMove);
c. 在冷数据库中保存一份数据;
d. 从热数据库中删除对应的数据;
e. 循环执行 b。
(3)并发性:假设数据量大到要分到多个地方并行处理,该怎么办?
在定时搬运冷热数据的场景里(比如每天),假设每天处理的数据量大到连单线程批量处理都来不及,我们该怎么办?这时我们就可以开多个线程并发处理了。(虽然大部分情况下多线程较快,但我曾碰到过这种情况:当单线程 batch size 到一定数值时效率特别高,比多线程任何 batch size 都快。所以,希望同学们到时多留意下,如果遇到多线程速度不快,我们就考虑控制单线程。)
当多线程同时搬运冷热数据,我们需要考虑如下实现逻辑。
第 1 步:如何启动多线程?
因为我们采用的是定时器触发逻辑,这种触发逻辑性价比最高的方式是设置多个定时器,并让每个定时器之间的间隔短一些,然后每次定时启动一个线程就开始搬运数据。
还有一个比较合适的方式是自建一个线程池,然后定时触发后面的操作:先计算待搬动的热数据的数量,再计算要同时启动的线程数,如果大于线程池的数量就取线程池的线程数,假设这个数量为 N,最后循环 N 次启动线程池的线程搬运冷热数据。
第 2 步:某线程宣布某个数据正在操作,其他线程不要动(锁)。
关于这个逻辑,我们需要考虑 3 个特性。
获取锁的原子性: 当一个线程发现某个待处理的数据没有加锁,然后给它加锁,这 2 步操作必须是原子性的,即要么一起成功,要么一起失败。实际操作为先在表中加上 LockThread 和 LockTime 两个字段,然后通过一条 SQL 语句找出待迁移的未加锁或锁超时的数据,再更新 LockThread=当前线程,LockTime=当前时间,最后利用 MySQL 的更新锁机制实现原子性。
获取锁必须与开始处理保证一致性: 当前线程开始处理这条数据时,需要再次检查下操作的数据是否由当前线程锁定成功,实际操作为再次查询一下 LockThread= 当前线程的数据,再处理查询出来的数据。
释放锁必须与处理完成保证一致性: 当前线程处理完数据后,必须保证锁释放出去。
第 3 步:某线程正常处理完后,数据不在热库,直接跑到了冷库,这是正常的逻辑,倒没有什么特别需要注意的点。
第 4 步:某线程失败退出了,结果锁没释放怎么办(锁超时)?
锁无法释放: 如果锁定这个数据的线程异常退出了且来不及释放锁,导致其他线程无法处理这个数据,此时该怎么办?解决方案为给锁设置一个超时时间,如果锁超时了还未释放,其他线程可正常处理该数据。
设置超时时间时,我们还应考虑如果正在处理的线程并未退出,因还在处理数据导致了超时,此时又该怎么办?解决方案为尽量给超时的时间设置成超过处理数据的合理时间,且处理冷热数据的代码里必须保证是幂等性的。
最后,我们还得考虑一个极端情况:如果当前线程还在处理数据,此时正在处理的数据的锁超时了,另外一个线程把正在处理的数据又进行了加锁,此时该怎么办?我们只需要在每一步加判断容错即可,因为搬运冷热数据的代码比较简单,通过这样的操作当前线程的处理就不会破坏数据的一致性。
考虑到前面逻辑比较复杂,这里我们特地画了一个分离的流程图,如下图所示:
学到这,冷热分离的 4 个问题,我们已经解决了 3 个,解决最后 1 个问题——如何使用冷热数据,我们就算大功告成了。
(四)如何使用冷热数据?
在功能设计的查询界面上,一般都会有一个选项供我们选择需要查询冷数据还是热数据,如果界面上没有提供,我们可以直接在业务代码里区分。(说明:在判断是冷数据还是热数据时,我们必须确保用户不允许有同时读冷热数据的需求。)
如何使用冷热数据示意图
课程讲到这,我们再把 01 讲整个知识点串起来下,串完后就形成了一个整体解决方案,如下图所示:
冷热分离整体方案示意图
一步一步学到这,思路是不是感觉瞬间清晰了很多?
总结一下,01 讲知识架构主要分为 4 个部分:冷热数据判断逻辑、冷热数据的触发逻辑、冷热数据分离实现思路、冷热数据库使用。学到这里,你有没有感觉还缺少点什么?没错,还缺少了历史数据的迁移。
一般而言,只要跟持久化层有关的架构方案,我们都需要考虑历史数据的迁移问题,即如何让旧架构的历史数据适用于新的架构?
因为前面的分离逻辑在考虑失败重试的场景时,刚好覆盖了这个问题,所以这个问题的解决方案也很简单,我们只需要给所有的历史数据加上标识:ColdFlag=WaittingForMove 后,程序就会自动迁移了。
不得不说,冷热分离解决方案确实能解决写操作慢和热数据慢的问题,但仍然存在诸多不足。
不足一: 用户查询冷数据速度依旧很慢,如果查询冷数据的用户比例很低,比如只有 1%,那么这个方案就没问题。
不足二: 业务无法再修改冷数据,因为冷数据多到一定程度时,系统承受不住。(这点可以通过冷库再分库来解决,为什么在 01 讲我没讲,因为后面还有分表分库的课时内容。)
另外,可能有部分同学对通过业务代码联结或监听 binlog 触发冷热分离的触发逻辑很感兴趣,而我并没有展开说明,别着急,02 讲中我会进一步阐述,记得继续往下学哦。
各位亲爱的同学,今天的课程就讲到这里。01 讲讲解的方案,肯定还存在一些遗漏的问题没有考虑,如果你有更好的方案,欢迎在评论区留言,喜欢本专栏的同学也欢迎分享给更多好友看到哦。
01 讲中我们提到过,冷热分离解决方案的性价比高,但它并不是一个最优的方案,仍然存在诸多不足,比如:查询冷数据慢、业务无法再修改冷数据、冷数据多到一定程度系统依旧扛不住,我们如果想把这些问题一一解决掉,可以用另外一种解决方案——查询分离。(注意:查询分离与读写分离还是有区别的。)
我曾做过 SaaS 客服系统的架构优化,系统里有一个工单查询功能,工单表中存放了几千万条数据,且查询工单表数据时需要关联十几个子表,每个子表的数据也是超亿条。
面对如此庞大的数据量,跟前面的冷热分离一样,每次客户查询数据时几十秒才能返回结果,即便我们使用了索引、SQL 等数据库优化技巧,效果依然不明显。
加上工单表中有些数据是几年前的,客户说这些数据涉及诉讼问题,需要继续保持更新,因此我们无法将这些旧数据封存到别的地方,也就没法通过前面的冷热分离方案来解决。
最终我们采用了查询分离的解决方案,才得以将这个问题顺利解决:将更新的数据放在一个数据库里,而查询的数据放在另外一个系统里。因为数据的更新都是单表更新,不需要关联也没有外键,所以更新速度立马得到提升,数据的查询则通过一个专门处理大数据量的查询引擎来解决,也快速地满足了客户的需求。
通过这种解决方案处理后,每次客户查询数据时,500ms 内就可得到返回结果,客户再也不抱怨了。
通过上面这个例子,我想你对查询分离的业务场景已经有了一定认知,但如果想掌握整个业务场景,还请继续跟着往下学哦。
关于查询分离的概念,从简单的字面意思上我们也好理解,即每次写数据时保存一份数据到另外的存储系统里,用户查询数据时直接从另外的存储系统里获取数据,示意图如下:
查询分离示意图
当你在实际业务中遇到以下情形,则可以考虑使用查询分离解决方案。
数据量大;
所有写数据的请求效率尚可;
查询数据的请求效率很低;
所有的数据任何时候都可能被修改;
业务希望我们优化查询数据的功能。
很多人对查询分离这个概念特别熟悉,但是对于查询分离的使用场景一无所知,这可不行,我们只有了解了查询分离的真正使用场景,才能在遇到实际问题时采取最正确的解决方案。
在实际工作中,如果业务要求必须使用查询分离的解决方案,我们就务必掌握查询分离的实现思路。也只有这样,我们真正遇到问题时才能有条不紊地开展工作。
查询分离解决方案的实现思路如下:
如何触发查询分离?
如何实现查询分离?
查询数据如何存储?
查询数据如何使用?
查询分离实现思路鱼骨图
针对以上四个问题,我们一点一点展开讲解。
(一)如何触发查询分离?
这个问题说明的是我们应该在什么时候保存一份数据到查询数据中,即什么时候触发查询分离这个动作。
一般来说,查询分离的触发逻辑分为 3 种。
(1) 修改业务代码:在写入常规数据后,同步建立查询数据。
逻辑三:同步建立查询数据
(2) 修改业务代码:在写入常规数据后,异步建立查询数据。
逻辑三:异步建立查询数据
(3) 监控数据库日志:如有数据变更,更新查询数据。
逻辑三:监控数据库日志
通过观察以上 3 种触发逻辑示意图,你发现了什么吗?3 种触发逻辑的优缺点对比表如下:
查询分离 3 种触发逻辑优缺点对比表
为方便你理解表中的内容,我将其中几个概念展开说明。
什么叫业务逻辑灵活可控? 举个例子:一般来说,写业务代码的人能从业务逻辑中快速判断出何种情况下更新查询数据,而监控数据库日志的人并不能将全部的数据库变更分支穷举,再把所有的可能性关联到对应的更新查询数据逻辑中,最终导致任何数据的变更都需要重新建立查询数据。
什么叫减缓写操作速度? 建立查询数据的一个动作能减缓多少写操作速度?答案:很多。举个例子:当你只是简单更新了订单的一个标识,本来查询数据时间只需要 2ms,而在查询数据时可能会涉及重建(比如使用 ES 查询数据时会涉及索引、分片、主从备份,其中每个动作又细分为很多子动作,这些内容后面我们会讲到),这时建立查询数据的过程可能就需要 1s 了,从 2ms 变成 1s,你说减缓幅度大不大?
查询数据更新前,用户可能查询到过时数据。 这里我们结合第 2 种触发逻辑来讲,比如某个操作正处于订单更新状态,状态更新时会通过异步更新查询数据,更新完后订单才从“待审核”状态变为“已审核”状态。假设查询数据的更新时间需要 1 秒,这 1 秒中如果用户正在查询订单状态,这时主数据虽然已变为“已审核”状态,但最终查询的结果还是显示“待审核”状态。
根据前面的对比表,总结每种触发逻辑的适用场景如下:
这里,我再结合个人的实战案例说明下:在一个真实业务场景中,虽然我们对业务的代码比较熟悉,但是业务要求每次修改工单请求时响应速度快,我们最终就选择了修改业务代码异步建立查询数据这种触发逻辑。
(二)如何实现查询分离?
以上共谈到 3 种触发逻辑,第 1 种是同步建立查询数据的过程比较简单,这里就不展开说明,第 3 种监控数据库日志我会在 13 讲具体讲解,所以这部分内容我们主要围绕第 2 种讲解。
关于第 2 种触发方案:修改业务代码异步建立查询数据,最基本的实现方式是单独起一个线程建立查询数据,不过这种做法会出现如下情况:
写操作较多且线程太多,最终撑爆 JVM;
建查询数据的线程出错了,如何自动重试;
多线程并发时,很多并发场景需要解决。
面对以上三种情况,我们该如何处理?此时使用 MQ 管理这些线程即可解决。
MQ 的具体操作思路为每次主数据写操作请求处理时,都会发一个通知给 MQ,MQ 收到通知后唤醒一个线程更新查询数据,示意图如下:
MQ 的实现流程
了解了 MQ 的具体操作思路后,我们还应该考虑以下 5 大问题。
问题一:MQ 如何选型?
如果公司已使用 MQ,那选型问题也就不存在了,毕竟技术部门不会同时维护 2 套 MQ 中间件,而如果公司还没使用 MQ,这就需要考虑选型问题了。
这里我分享两点选型原则,希望对你有帮助。
(1)召集技术中心所有能做技术决策的人共同投票选型。
(2)不管我们选择哪个 MQ ,最终都能实现想要的功能,只不过是易用不易用、多写少写业务代码的问题,因此我们从易用性和代码工作量角度考量即可。
问题二:MQ 宕机了怎么办?
如果 MQ 宕机了,我们只需要保证主流程正常进行,且 MQ 恢复后数据正常处理即可,具体方案分为三大步骤。
每次写操作时,在主数据中加个标识:NeedUpdateQueryData=true,这样发到 MQ 的消息就很简单,只是一个简单的信号告知更新数据,并不包含更新的数据 id。
MQ 的消费者获取信号后,先批量查询待更新的主数据,然后批量更新查询数据,更新完后查询数据的主数据标识 NeedUpdateQueryData 就更新成 false 了。
当然还存在多个消费者同时搬运动作的情况,这就涉及并发性的问题,因此问题与 01 讲冷热分离中的并发性处理逻辑类似,这里我就不讲了。
问题三:更新查询数据的线程失败了怎么办?
如果更新的线程失败了,NeedUpdateQueryData 的标识就不会更新,后面的消费者会再次将有 NeedUpdateQueryData 标识的数据拿出来处理。但如果一直失败,我们可以在主数据中多添加一个尝试搬运次数,比如每次尝试搬运时 +1,成功后就清零,以此监控那些尝试搬运次数过多的数据。
问题四:消息的幂等消费
在编程中,一个幂等操作的特点是多次执行某个操作均与执行一次操作的影响相同。
举个例子,比如主数据的订单 A 更新后,我们在查询数据中插入了 A,可是此时系统出问题了,系统误以为查询数据没更新,又把订单 A 插入更新了一次。
所谓幂等,就是不管更新查询数据的逻辑执行几次,结果都是我们想要的结果。因此,考虑消费端并发性的问题时,我们需要保证更新查询数据幂等。
问题五:消息的时序性问题
比如某个订单 A 更新了 1 次数据变成 A1,线程甲将 A1 的数据搬到查询数据中。不一会儿,后台订单 A 又更新了 1 次数据变成 A2,线程乙也启动工作,将 A2 的数据搬到查询数据中。
所谓的时序性就是如果线程甲启动比乙早,但搬运数据动作比线程乙还晚完成,就有可能出现查询数据最终变成过期的 A1。如下图(动作前面的序号代表实际动作的先后顺序):
此时解决方案为主数据每次更新时,都更新上次更新时间 last_update_time,然后每个线程更新查询数据后,检查当前订单 A 的 last_update_time 是否跟线程刚开始获得的时间一样,且 NeedUpdateQueryData 是否等于 false,如果都满足的话,我们就将 NeedUpdateQueryData 改为 true,然后再做一次搬运。
学到这,你心中可能有个疑问:MQ 在这里的作用只是一个触发信号的工具,如果不用 MQ 好像也没啥问题啊,这你就大错特错了,MQ 的作用还不少呢,不信你往下看。
服务的解耦: 这样主业务逻辑就不会依赖更新查询数据这个服务了。
控制更新查询数据服务的并发量: 如果我们直接调用更新查询数据服务,因写操作速度快,更新查询数据速度慢,写操作一旦并发量高,会给更新查询数据服务造成超负荷压力。如果通过消息触发更新查询数据服务,我们就可以通过控制消息消费者的线程数来控制负载。
(三)查询数据如何存储?
我们应该使用什么技术存储查询数据呢?目前,市面上主要使用 Elasticsearch 实现大数据量的搜索查询,当然还可能会使用到 MongoDB、HBase 这些技术,这就需要我们对各种技术的特性了如指掌,再进行技术选型。
关于技术选型这个问题,我觉得很多时候我们不能单单只考虑业务功能的需求,还需要考虑组织结构。比如当初我们设计架构方案时,为什么选择用 Elasticsearch,除 ES 对查询的扩展性支持外,最关键的一点是我们团队对 Elasticsearch 很熟悉。
(四)查询数据如何使用?
因 ES 自带 API,所以使用查询数据时,我们在查询业务代码中直接调用 ES 的 API 就行。
不过,这个办法会出现一个问题:**数据查询更新完前,查询数据不一致怎么办?**这里我分享 2 种解决思路。
在查询数据更新到最新前,不允许用户查询。(我们没用过这种设计,但我确实见过市面上有这样的设计。)
给用户提示:您目前查询到的数据可能是 1 秒前的数据,如果发现数据不准确,可以尝试刷新一下,这种提示用户一般比较容易接受。
以上,我们已经把四个问题都讨论完了,我们再一起看下查询分离的整体方案,如下图所示:
查询分离整体方案示意图
总结一下,02 讲关于查询分离的架构主要分为四个部分:如何触发查询分离?如何实现查询分离?查询数据如何存储?查询数据如何使用?
接下来,我们就进入架构方案中必备的一个知识点——历史数据迁移,且往下学。
新的架构方案上线后,旧的数据如何适用新的架构方案?这是实际业务中需要我们考虑的问题。
在这个方案里,我们只需要把所有的历史数据加上这个标识:NeedUpdateQueryData=true,程序就会自动处理了。
查询分离这个解决方案虽然能解决一些问题,但我们也要清醒地认识到它的不足。
不足一: 使用 Elasticsearch 存储查询数据时,注意事项是什么(此方案并未详细展开)?
不足二: 主数据量越来越大后,写操作还是慢,到时还是会出问题。
不足三: 主数据和查询数据不一致时,假设业务逻辑需要查询数据保持一致性呢?
各位亲爱的同学,以上就是 02 讲的全部内容,方案中肯定还有一些遗漏的问题没有考虑,如果你有更好的建议欢迎在留言区与我互动。
另外,对本专栏感兴趣的同学,可以将 02 讲分享给更多有需要的朋友看到哦。
接下来 03 讲的核心讲解要点:使用 Elasticsearch 做查询数据的存储系统时需要注意哪些问题,我强烈建议你好好学习下,因为这个问题不管是面试还是实际工作中,我们都会碰到。一个技术使用起来并不难,难的是使用这个技术时你会碰到什么问题,你又是如何解决的?