微服务拆分策略

第一章 逃离单体地狱

第二章 拆分策略

识别系统操作

第一步创建由关键类组成的抽象领域模型,这些关键类提供用于描述系统操作的词汇表。

第二步确定系统操作,并根据领域模型描述每个系统操作的行为。

    领域模型主要源自用户故事中提及的名词,系统操作主要来自用户故事中提及的动词。你还可以使用名为事件风暴(Event Storming)的技术定义领域模型

创建抽象领域模型

    定义系统操作的第一步是为这个应用程序描绘一个抽象的领域模型。注意这个模型比我们最终要实现的简单很多。尽管非常简单,抽象的领域模型仍旧有助于在开始阶段提供帮助,因为它定义了描述系统操作行为的一些词语。

    创建领域模型会采用一些标准的技术,例如通过与领域专家沟通后,分析用户故事和场景中频繁出现的名词。例如Place Order用户故事,我们可以把它分解为多个用户场景,例如这个:

    在这个用户场景中的名词,如Consumer、Order、Restaurant和CreditCard,暗示了这些类都是需要的。  

    同样,Accept Order用户故事也可以分解为多个场景,如下:

    这个场景暗示需要Courier类和Delivery类。在经过几次迭代分析之后,结果显然就是这个领域模型应该包括一些类,如MenuItem和Address等。下图显示了核心类的类图。

    每一个类的作用如下:

  • Consumer:下订单的用户。
  • Order:用户下的订单,它用来描述订单并跟踪状态。
  • OrderLineItem:Order中的一个条目。
  • DeliveryInfo:送餐的时间和地址。
  • Restaurant:为用户准备生产订单的餐馆,同时也要发起送货。
  • MenuItem:餐馆菜单上的一个条目。
  • Courier:送餐员负责把订单送到用户手里。可跟踪送餐员的可用性和他们的位置。
  • Address:Consumer或Restaurant的地址。
  • Location:Courier当前的位置,用经纬度表示。

    类似图2-7这种类图描述了应用程序架构的一个方面。但如果没有对应的场景,这个图也就是仅仅好看而已,并不实用。下一步开始定义对应架构场景的系统操作。

定义系统操作

    当定义了抽象的领域模型之后,接下来就要识别系统必须处理的各种请求。我们并不讨论具体的用户界面,但是你能够想象在每一个用户场景下,前端的用户界面向后端的业务逻辑发出请求,后端的业务逻辑进行数据的获取和处理。FTGO是一个Web应用,这意味着它的大部分请求都是基于HTTP的。但也有可能一些客户端会使用消息。相比绑定到具体的通信协议,使用抽象的词汇来描述跟系统操作有关的请求更为合理。

系统操作有以下两种类型

  1. 命令型:创建、更新或删除数据的系统操作。

  2. 查询型:查询和读取数据的系统操作。

    从根本上说,这些系统操作都会对应到具体的REST、RPC或消息端口。但现阶段我们不必在意这些实现细节。让我们先开始识别一些指令。

    识别系统指令的切入点是分析用户故事和场景中的动词。例如Place Order用户故事,它非常明确地告诉架构师,这个系统必须提供一个Create Order操作。很多用户故事都会直接对应或映射为系统命令。下图列出了一些关键的系统命令。

    命令规范定义了命令对应的参数、返回值和领域模型类的行为。行为规范中包括前置条件(即当这个操作被调用时必须满足的条件)和后置条件(即这个操作被调用后必须满足的条件)。例如,以下就是createOrder()系统操作的规范。

    前置条件对应着Place Order用户场景中的givens,后置条件对应着场景中的Then。当系统操作被调用时,它会检查前置条件,执行操作来完成和满足后置条件。

下面是acceptOrder()的系统操作规范:

    前置条件和后置条件对应着之前用户场景中的描述。

    多数与系统操作相关的架构元素是命令。查询虽然仅仅是简单地获取数据,但是也同样重要。

    应用程序除了实现指令以外,也必须实现查询。查询为用户决策提供了用户界面。在目前阶段,我们并没有开始为FTGO应用程序构思任何用户界面,但是需要注意,当消费者下订单时往往是如下所示的过程。

  • 1.用户输入送餐地址和期望的送餐时间;
  • 2.系统显示当前可用的餐馆;
  • 3.用户选择餐馆;
  • 4.系统显示餐馆的菜单;
  • 5.用户点餐并结账;
  • 6.系统创建订单。

    这个用户场景包含了以下的查询型操作:

  • findAvailableRestaurants(deliveryAddress,deliveryTime):获取所有能够送餐到用户地址并满足送餐时间要求的餐馆。
  • findRestaurantMenu(id):返回餐馆信息和这家餐馆的菜单项。

    抽象的领域模型和系统操作能够回答这个应用“做什么”这一问题。这有助于推动应用程序的架构设计。每一个系统操作的行为都通过领域模型的方式来描述。每一个重要的系统操作都对应着架构层面的一个重大场景,是架构中需要详细描述和特别考虑的地方。现在我们来看看如何定义应用程序的微服务架构。  

    系统操作被定义后,下一步就是完成应用服务的识别。如之前提到的,这并不是一个机械化的流程,相反,有多种拆分策略可供选择。每一种都是从一个侧面来解决问题,并且使用它们独有的一些术语。但是殊途同归,这些策略的结果都是一样的:一个包含若干服务的架构,这样的架构是以业务而不是技术概念为中心。

第三章 服务拆分的策略

第四章 服务拆分的规范

    微服务拆分之后,工程会比较的多,如果没有一定的规范,将会非常混乱,难以维护。
    首先人们经常问的一个问题是,服务拆分之后,原来都在一个进程里面的函数调用,现在变成了A调用B调用C调用D调用E,会不会因为调用链路过长而使得相应变慢呢?

服务拆分的规范一:服务拆分最多三层,两次调用

    服务拆分是为了横向扩展,因而应该横向拆分,而非纵向拆成一串的。也即应该将商品和订单拆分,而非下单的十个步骤拆分,然后一个调用一个。


纵向的拆分最多三层:

  • 基础服务层:用于屏蔽数据库,缓存层,提供原子的对象查询接口,有这一层,为了数据层做一定改变的时候,例如分库分表,数据库扩容,缓存替换等,对于上层透明,上层仅仅调用这一层的接口,不直接访问数据库和缓存。
  • 组合服务层:这一层调用基础服务层,完成较为复杂的业务逻辑,实现分布式事务也多在这一层
  • Controller层:接口层,调用组合服务层对外

服务拆分的规范二:仅仅单向调用,严禁循环调用

    微服务拆分后,服务之间的依赖关系复杂,如果循环调用,升级的时候就很头疼,不知道应该先升级哪个,后升级哪个,难以维护。

因而层次之间的调用规定如下:

  • 基础服务层主要做数据库的操作和一些简单的业务逻辑,不允许调用其他任何服务。
  • 组合服务层,可以调用基础服务层,完成复杂的业务逻辑,可以调用组合服务层,不允许循环调用,不允许调用Controller层服务
  • Controller层,可以调用组合业务层服务,不允许被其他服务调用

服务拆分的规范三:将串行调用改为并行调用,或者异步化

    如果有的组合服务处理流程的确很长,需要调用多个外部服务,应该考虑如何通过消息队列,实现异步化和解耦。

    例如下单之后,要刷新缓存,要通知仓库等,这些都不需要再下单成功的时候就要做完,而是可以发一个消息给消息队列,异步通知其他服务。

    而且使用消息队列的好处是,你只要发送一个消息,无论下游依赖方有一个,还是有十个,都是一条消息搞定,只不过多几个下游监听消息即可。

    对于下单必须同时做完的,例如扣减库存和优惠券等,可以进行并行调用,这样处理时间会大大缩短,不是多次调用的时间之和,而是最长的那个系统调用时间。

服务拆分的规范四:接口应该实现幂等

    微服务拆分之后,服务之间的调用当出现错误的时候,一定会重试,但是为了不要下两次单,支付两次,需要所有的接口实现幂等。

    幂等一般需要设计一个幂等表来实现,幂等表中的主键或者唯一键可以是transaction id,或者business id,可以通过这个id的唯一性标识一个唯一的操作。

    也有幂等操作使用状态机,当一个调用到来的时候,往往触发一个状态的变化,当下次调用到来的时候,发现已经不是这个状态,就说明上次已经调用过了。

    状态的变化需要是一个原子操作,也即并发调用的时候,只有一次可以执行。可以使用分布式锁,或者乐观锁CAS操作实现。

服务拆分的规范五:接口数据定义严禁内嵌,透传

    微服务接口之间传递数据,往往通过数据结构,如果数据结构透传,从底层一直到上层使用同一个数据结构,或者上层的数据结构内嵌底层的数据结构,当数据结构中添加或者删除一个字段的时候,波及的面会非常大。

    因而接口数据定义,在每两个接口之间约定,严禁内嵌和透传,即便差不多,也应该重新定义,这样接口数据定义的改变,影响面仅仅在调用方和被调用方,当接口需要更新的时候,比较可控,也容易升级。

服务拆分的规范六:规范化工程名

  • 微服务拆分后,工程名非常多,开发人员,开发团队也非常多,如何让一个开发人员看到一个工程名,或者jar的名称,就大概知道是干什么的,需要一个规范化的约定。
  • 例如出现pay就是支付,出现order就是下单,出现account就是用户。
  • 再如出现compose就是组合层,controller就是接口层,basic就是基础服务层。
  • 出现api就是接口定义,impl就是实现。
  • pay-compose-api就是支付组合层接口定义。
  • account-basic-impl就是用户基础服务层的实现。

第五章 进程通信