本文首发于公众号:托尼学长,立个写 1024 篇原创技术面试文章的flag,欢迎过来视察监督~
话说在技术面试过程中,虽然候选人在都在历数单体架构的不足之处,然后吹嘘微服务架构如何牛逼,但这都是为了迎合面试官的。
如果开发一个中小型的系统,我们通过在单体架构写各种多表关联的 SQL 语句方式,来完成业务逻辑的开发工作,那当真就是一个爽啊,开发效率贼 TM 高。
当然,那个中小型系统随着业务复杂度越来越高,研发人员也越来越多,不得已进行微服务拆分的时候,我们就能体会到过程中的痛苦与艰辛了。
其体现在,不但要对以前一个数据库事务搞定的写操作要做分布式事务,还要为以前一个多表关联搞定的读操作出解决方案。
本文就来讲讲后者的解决方案,目前主流的技术方案有两种,API 组合模式和 CQRS 模式。
API 组合模式
在 API 组合模式中有两种角色,分别是数据提供方服务和 API 组合器。
数据提供方服务:顾名思义,是一个能够为 API 提供部分或全部数据的服务。
API 组合器:负责获取一或多个数据提供方服务的数据,并将数据进行加工组合,以实现查询操作。
这样讲概念还是过于晦涩了,下面我们以在线教育场景学生端查询课表的场景为例:
在这个业务场景中,学生端服务需要调用下游的预习服务、课程服务、教师服务和作业服务,来完成学生课表的展示逻辑。
如果按照 API 组合模式来进行实现,那各服务所扮演的角色如下图:
API 组合模式的特点是使用简单,开发效率高,缺点是对一些复杂查询并不适用,比如:多服务过滤 + 限制记录数量的业务场景。
我们以常见的电商场景举例,如果需要查询十个订单,要求商品品类为生鲜,且订单的所属用户等级为 VIP,那应该如何查询呢?
如果我们先从订单服务查询出来十个订单,然后去商品服务判断是否为生鲜,判断并过滤后十个订单变成了七个,最后再去用户服务判断该订单的所属用户是否为 VIP,判断并过滤后七个订单变成了五个,这不就不满足需求了吗?
有人说,你可以写个 while { true }循环啊,如果没有凑齐十个订单,那就继续循环上述逻辑,直到满足要求为止。
但在极端情况下,可能把全部订单都查一遍也凑不齐十个呢,而系统中的订单数据量达到几百万或千万级别,这还不把CPU给刷爆了吗?
还有的同学说,如果不先查询订单服务,改为查询商品服务行不行,把所有生鲜类的商品全部查出来,然后带入到订单服务去查询对应的订单,最后再去用户服务判断该订单的所属用户是否为 VIP 呢?
这样做也存在两个问题,一个是在大一些的电商平台中,生鲜类的商品数量较多的情况下,这种查询的性能会很差。就算只查询商品 ID,但别忘了 MySQL InnoDB 是行式存储的, 在数据库层面还是会读取整行数据。
另一个则还是之前的问题,按批次把订单数据带入到用户服务进行判断过滤的问题。
说到这里,大家是不是怀念单体架构的数据库多表关联查询了?明明一条SQL就解决的问题,竟然在这里纠结这么久。
没关系,我们继续看下面的 CQRS 模式。
CQRS 模式
CQRS(命令查询职责隔离)模式,是一种将数据存储的读操作和写操作分离的模式,使用事件来维护从多个服务复制数据的只读视图,借此实现对来自多个服务的数据查询。
目前实现“只读视图”最主流的方式,就是ElasticSearch搜索引擎了,至于从多个服务同步复制数据的方式,可以通过程序代码中双写或 Alibaba Canal 同步 Binlog 的方式进行实现。
我们还是以电商场景举例,查询十个订单,要求商品品类为生鲜,且用户等级为 VIP,看看如何通过 CQRS 模式进行实现。
如下图所示,我们将用户服务、订单服务和商品服务的业务数据,通过 Canal 同步到 ElasticSearch 中形成大宽表,并在“订单复杂查询服务”中查询出来满足商品品类为生鲜、所属用户等级为VIP的十个订单。
通过 ElasticSearch 进行查询操作非常简单,只需要输入查询条件和限制数量即可,连关联表的操作都不需要了。
POST /_search?from=0&size=10 Content-Type: application/json { "query": { "match": { "user_level": "VIP", "category": "fresh" } } }
为避免产生歧义,我解释一下,上图中标注的“写操作”仅针对于当前的“订单复杂查询”场景,如果是 By ID 查询用户信息,或者是查询商品列表并根据时间排序等场景,肯定还是在用户服务和商品服务中查询的。
甚至包括一些简单的订单查询,一样是可以在订单服务中完成。
使用 CQRS 模式解决关联查询的优点是,它的查询性能要明显高于 API 组合模式,因为后者可能会将大量数据读取到应用服务器内存中进行过滤和连接。
并且,不同于 API 组合模式的查询场景局限性,CQRS 模式适用于任何场景、任何方式的查询操作。
CQRS 模式的缺点在于,由于引入了Canal、ElasticSearch和订单复杂查询服务的原因,整体的系统架构变得更加复杂,并且系统的硬件成本也会攀升。
除此之外,如果通过 Canal 进行数据同步操作,也会增加数据时延性,且在默认情况下,ElasticSearch本身的数据展示也存在一秒的时延性(index.refresh.interval": "1s")。
结语
至于 API 组合模式和 CQRS 模式两者的技术选型,我的建议是优先选择前者,前者实在搞不定再启用后者,毕竟其需要投入的研发和硬件成本是不可忽视的。
对了,再嘱咐一句,如果单体架构用着不难受,那就别强行演进成微服务架构,别赶着技术时髦,没意义的,最后折腾的是自己。
因为,世界上解决一个计算机问题最简单的方法是 —— “恰好” 不需要解决它。