历史上许多科学家所认可的熵增定律揭示了许多自然现象的本质:任何孤立的系统,在没有外力作用的情况下,其总混沌(熵)将不断增加。当然,软件系统也不例外。随着软件系统功能的不断增加,系统的混乱程度也越来越大。为了降低软件系统混乱的速度,必须对其施加外力(重构)。重构系统与重构代码是一样的。首先,您必须识别系统的异味。DavideTaibi和ValentinaLenarduzzi在文章《On the Definition of Microservice Bad Smells》中定义了微服务的难闻气味。今天我们就来聊一聊如何消除??其中一种恶臭——循环依赖。循环依赖的危害微服务之间的循环依赖类似于类之间的循环依赖。当依赖形成循环时,会带来很多危害:第一,微服务之间的耦合性很强,服务很难独立部署,严重违背了微服务的初衷;这种情况往往是由于服务之间的调用缺乏约束造成的。为了方便访问或更新数据,可以随意调用服务,以“微服务”为设计目标,系统将逐渐演变成一个大的分布式单元。下图展示了三个服务之间的循环依赖:另外,循环依赖很可能会造成一些循环调用或者并发问题,从而导致一些复杂且难以定位的问题。上图以订单与客户的循环依赖为例:订单服务依赖客户服务获取客户信息。为了满足根据客户名称查询订单的需求,除了记录客户ID外,订单数据中还多了客户名称。更新客户信息时,为保证订单数据的准确性,客服会调用订单服务更新订单客户信息。循环依赖使事情变得复杂,不同的实现会有不同的问题。我们来看这个场景:当订单状态发生变化时,客户状态也随之发生变化。订单服务会更新客户状态,客户信息的变化又需要更新订单上多余的客户信息;假设在订单上加了乐观锁,两个订单同时更新会因为乐观锁而失败,在排查的过程中会优先考虑并发的问题,但事实恰恰相反,见下图顺序:循环依赖数据冗余的原因数据冗余是导致服务之间循环依赖的主要原因,为了保证多个服务之间的数据一致性,一个服务修改为数据必须同步到其他服务,以确保其他服务冗余字段中的信息是准确的。上述订单上出现冗余客户名称的场景就是一个很好的例子。数据冗余在一定程度上增强了业务实现的简洁性,但是为了保证数据的一致性,也增加了服务之间不必要的调用。业务概念缺失、数据冗余过多,是相对于软件设计中缺乏必要的业务概念而言的,也会导致循环依赖的发生;一个典型的场景是上游系统的数据状态需要从下游系统的数据中计算出来。当需要上游系统的数据状态时,上游服务会调用下游服务的接口进行计算。这种场景也很常见。继续用订单和客户的例子来帮助理解:如果一个客户的所有订单要么被取消,要么已经完成开票结算流程,那么这个客户就是成功客户,否则就是服务中客户。如果在软件设计时没有为客户定义状态来表达这个业务概念,那么每次需要获取客户状态时,只能通过订单服务进行查询计算。滥用同步调用以上两种场景一旦产生循环依赖,往往伴随着滥用同步调用。但也有一些场景,既不存在过多的数据冗余,也不缺乏业务概念,但同步调用仍然可能被滥用。上图中用户辞职更新客户所有者信息的场景就是一个很好的例子。虽然从实现结果来看使用同步调用是一种非常直接有效的方法,但是从长远来看,这种无脑滥用同步调用让系统变得非常复杂。当我们要替换或重构某个服务时,我们很难根据业务分析知道哪些服务会受到影响。消除循环依赖的方法通过上面对循环依赖产生原因的分析(这里不考虑微服务划分不合理的场景),可以看出循环依赖通常是为了满足某些业务需求,在服务之间加入不合理的链接称呼。要解决循环依赖,需要在微服务之间建立一些原则来约束微服务之间的通信,通过这些原则定期检查我们的系统,发现问题并进行重构。这些原则应该包括:定义服务的上下游下游服务可以直接依赖上游服务,否则上游服务的变化会影响下游服务。应该使用领域事件(异步)实现数据ID(或类Id,可以唯一表示数据和Unchanging属性)关联,尽量不要做太多的数据冗余一旦上游服务需要调用下游服务来完成业务,需要考虑上游服务是否缺少满足前端逻辑的业务概念。放在BFF(Backendforfrontend)中,而不是在服务之间添加调用才可以完成;受限于交付压力,当对架构的理解不是很透彻时,开发者倾向于采用更简单(熵增)的方式来实现功能。在这种情况下,循环依赖是一种很容易产生的不良味道,对系统的健康是一个巨大的危害。我们可以通过一些技术手段来发现系统中的循环依赖问题,比如通过链接跟踪系统(如Zipkin)可视化服务之间的依赖关系;我们还可以在发现问题时将有问题的流程时序图画出来并可视化,这样的方法可以很方便地帮助我们发现系统中循环依赖的问题,进而对症下药,消除恶臭。
