15 如何设计支持微服务的技术中台? 有了技术中台对领域驱动的支持,那如何应用于微服务架构呢?支持 DDD 与微服务的技术中台应当具备以下几个方面的能力:

  • 解决当前微服务架构的技术不确定性,使得微服务项目可以用更低的成本应对日后技术架构的更迭;
  • 可以更加容易地将领域驱动设计应用到微服务架构中,包括:领域建模、限界上下文的微服务拆分、事件通知机制等;
  • 需要解决领域对象在仓库与工厂进行装配时,如何将本地查询替换为远程接口调用,实现微服务间数据装配的问题。

解决技术不确定性的问题

如今的微服务架构,基本已经形成了 Spring Cloud 一统天下的局势。然而,在 Spring Cloud 框架下的各种技术组件依然存在诸多不确定性,如:注册中心是否采用 Eureka、服务网关是采用 Zuul 还是 Gateway,等等。同时,服务网格 Service Mesh 方兴未艾,不排除今后所有的微服务都要切换到 Service Mesh 的可能。在这种情况下如何决策微服务的技术架构?代码尽量不要与 Spring Cloud 耦合,才能在将来更容易地切换到 Service Mesh。那么,具体又该如何做到呢?

Drawing 0.png

单 Controller、单 Dao 的设计在微服务架构的应用

如上图所示,当前端通过服务网关访问微服务时,首先要访问聚合层的微服务。这时,在聚合层的微服务中,采用单 Controller 接收前端请求。这样,只有该 Controller 与 MVC 框架耦合,后面所有的 Service 不会耦合,从而实现了业务代码与技术框架的分离

同样的,当 Service 执行各种操作调用原子服务层的微服务时,不是通过 Ribbon 进行远程调用,而是将原子服务层的微服务开放的接口,在聚合层微服务的本地编写一个 Feign 接口。那么,聚合层微服务在调用原子微服务时,实际调用的是自己本地的接口,再由这个接口通过加载 Feign 注解,去实现远程调用。

这样,聚合层微服务中的各个 Service 就不会与 Spring Cloud 各个组件发生任何耦合,只有那些 Feign 接口与 Spring Cloud 耦合去实现远程调用,让业务代码与技术框架实现了解耦。

同样的道理,原子服务层的微服务在对外开放接口时,不是由各个 Service 直接开放 API 接口。因为如果让 Service 直接开放 API 接口,就需要编写相关注解,使得 Service 与 Spring Cloud 耦合。因此,由统一的 Controller 开放接口,再由它去调用内部的 Service。这样,所有的 Service 就是纯洁的,不与 Spring Cloud 技术框架耦合,只有 Controller 与其耦合。

有了以上这些设计,当未来需要从 Spring Cloud 框架迁移到 Service Mesh 时,只需要将那些纯洁的、不与 Spring Cloud 耦合的 Service,与领域对象中的业务代码迁移到新的框架中,就可以以非常低的成本,在各种技术框架中自由地切换,从而快速跟上技术发展的步伐。通过这种方式,就能很好地应对未来的技术不确定性问题,更好地开展架构演化。

支持微服务远程调用的架构设计

此外,微服务架构设计最大的难题是微服务的合理拆分,拆分要体现单一职责原则,即微服务间低耦合,微服务内高内聚。那么,在软件项目中如何做到这些呢?业界最佳的实践无疑是基于领域的设计,即先按照领域业务建模,然后基于限界上下文进行微服务拆分。这样设计出来的微服务系统,都可以保证每次变更落到某个微服务上。微服务变更完了,自己独立升级,就能达到降低维护成本、快速交付的目的。

基于这样的思路,每个微服务在设计时,都采用支持领域驱动的技术中台。这样,每个微服务都是基于领域驱动建模和设计,然后在该技术中台中编码实现,既提高了开发速度,又降低了维护成本。

然而,转型为微服务后,有一个技术难题亟待解决,那就是跨库的数据操作。当一个单体应用拆分成多个微服务后,不仅应用程序需要拆分,数据库也需要拆分。譬如,经过微服务拆分,订单有订单数据库,用户有用户数据库。这时,当查询订单,需要补填其对应的用户信息时,就不能从自己本地的数据库中查询了,而是调用“用户”微服务的远程接口,在用户数据库中查询,然后返回给“订单”微服务。这时,原有的技术中台就必须做出调整。

如何调整呢?通用 DDD 仓库在执行查询或者装载操作时,查询完订单补填用户信息,不是通过 DAO 去查询本地数据库,而是改成调用远程接口,去调用用户微服务。这时,可以先在订单微服务的本地编写一个用户 Service 的 Feign 接口,订单仓库与工厂调用这个接口就可以了。然后通过 Feign 接口实现对用户微服务的远程调用。

采用 Feign 接口实现远程调用

每个微服务都是一个独立的进程,运行在各自独立的 JVM,甚至不同的物理节点上,通过网络访问。因此,微服务与微服务之间的调用必然是远程调用。以往,我们对微服务间的调用采用 Ribbon 的方式,在程序中的任意一个位置,只要注入一个 restTemplate,就可以进行远程调用。

这样的代码过于随意,会越来越难于阅读与变更维护。比如,原来某个微服务中有两个模块 A 与 B,都需要调用模块 C。随着业务变得越来越复杂,需要进行微服务拆分,将模块 C 拆分到了另外一个微服务中。这时,原来的模块 A 与 B 就不能像原来一样调用模块 C,否则就会报错。

Drawing 1.png

Ribbon 的远程调用方式

如何解决以上问题呢?需要同时改造模块 A 与 B,分别加入 restTemplate 实现远程调用,来调用模块 C。也就是说,这时所有调用模块 C 的程序都需要改造,改造的成本与风险就会比较高。

因此,在实现微服务间调用时,我们通常会采用另外一个方案:Feign。Feign 不是另起炉灶,而是对 Ribbon 的封装,目的是使代码更加规范、变更更加易于维护。采用的方案是,不修改模块 A 与 B 的任何代码,而是在该微服务的本地再制作一个模块 C 的接口 C′。该接口与模块 C 一模一样,拥有模块 C 的所有方法,因此模块 A 与 B 还可以像以前一样在本地调用接口 C′。但接口 C′ 只是一个接口,什么都做不了,因此需要通过添加 Feign 的注解,实现远程调用,去调用模块 C。这个方案,既没有修改模块 A 与 B,又没有修改模块 C,而仅仅添加了一个接口 C′,使维护成本降到了最低。

Drawing 2.png

Feign 的远程调用方式

如何通过 Feign 实现微服务的远程调用呢?

首先,创建项目时,在 POM.xml 文件中添加 Eureka Client、Hystrix 与 Actuator 等组件以外,将 ribbon 改为 feign:

org.springframework.cloud spring-cloud-starter-openfeign

org.springframework.cloud spring-cloud-starter-netflix-hystrix org.springframework.boot spring-boot-starter-actuator

接着,在启动类 FeignApplication 中,不仅添加 Discovery Client,还要添加 Feign Client:

import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.netflix.hystrix.EnableHystrix; import org.springframework.cloud.openfeign.EnableFeignClients; /// /* @author fangang // @SpringBootApplication @EnableDiscoveryClient @EnableFeignClients @EnableHystrix public class FeignApplication { ///* /* @param args /*/ public static void main(String[] args) { SpringApplication.run(FeignApplication.class, args); } }

用 Feign 实现调用时,首先在消费者这端编写一个与生产者开放的 API 一模一样的接口,然后添加 Feign 的注解:

/// /* The service of suppliers. /* @author fangang // @FeignClient(value=”service-supplier”, fallback=SupplierHystrixImpl.class) public interface SupplierService { ///* /* @param id /* @return the supplier // @RequestMapping(value = “orm/supplier/loadSupplier”, method = RequestMethod.GET) public Supplier loadSupplier(@RequestParam(“id”)Long id); ///* /* @param ids /* @return the list of suppliers /*/ @PostMapping(“orm/supplier/loadSuppliers”) public List loadSuppliers(@RequestParam("ids")List ids); //*/* /* @return the list of suppliers /*/ @GetMapping("orm/supplier/listOfSuppliers") public List listOfSuppliers(); }

在这一代码例子中,具体的流程是这样的:

  • 在生产者那一端有个 SupplierService 的类,因此首先在消费者这端编写一个一模一样的SupplierService 的接口;
  • 接着,在 interface 前面添加注解 @FeignClient;
  • 这里的 value 为生产者在 Eureka 注册中心中注册的名称;
  • 在每个方法前添加注解,GET 请求就写 @GetMapping,POST 请求就写 @PostMapping,名称就是生产者那端开放的接口名称;
  • 然后,如果需要将参数加入 url 中,就在参数前添加注解 @RequestParam;
  • 有了以上注解,Feign 就可以从接口中取出相应的数据,拼装成 url,最后去执行 ribbon 实现微服务远程调用。

采用 Ref 标签调用 Feign 接口

采用 Feign 实现微服务间的远程调用以后,在 vObj.xml 中进行建模时,也需要做出改变,将join 标签改为 ref 标签。其配置如下所示: <?xml version=”1.0” encoding=”UTF-8”?>

在这一配置中,将 supplier 由 join 标签改为 ref 标签,其中:

  • bean代表在“产品”微服务本地调用“供应商”微服务的 Feign 接口;
  • method是指定要调用这个 Feign 接口的方法;
  • listMethod是在批量查询“产品”数据集时,进行批量补填的优化措施。

通过这样的配置,在查询产品过程中,通用仓库补填供应商信息,就不会去调用本地的 DAO,而是调用 SupplierService 的 Feign 接口,由它实现对“供应商”微服务的远程调用,从而实现跨微服务的数据装配。

总结

本讲提出了支持 DDD + 微服务架构的技术中台设计思想。通过以上的设计,既实现了业务代码与技术框架解耦的整洁架构思想,使得系统在日后更易于架构演化,又实现了领域模型在微服务间的数据装配,解决了 DDD 转型微服务架构的关键技术难题。开发团队有了这样的技术中台,就能将 DDD 运用起来,在项目中真正的落地实践。

下一讲将在 GitHub 上分享代码,进一步讲解这个技术中台的代码设计与项目实践。

点击 GitHub 链接,查看源码。

参考资料

https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/DDD%20%e5%be%ae%e6%9c%8d%e5%8a%a1%e8%90%bd%e5%9c%b0%e5%ae%9e%e6%88%98/15%20%20%e5%a6%82%e4%bd%95%e8%ae%be%e8%ae%a1%e6%94%af%e6%8c%81%e5%be%ae%e6%9c%8d%e5%8a%a1%e7%9a%84%e6%8a%80%e6%9c%af%e4%b8%ad%e5%8f%b0%ef%bc%9f.md