返回文章列表
高并发项目
SpringCloudNacosOpenFeign服务拆分

02拆分项目并使用nacos注册、OpenFeign实现项目间访问

微服务是一种软件架构风格,它以专注于单一职责的很多小型项目为基础,组合出复杂的大型应用。本文将围绕单体架构的不足、微服务的拆分思想、Nacos 注册中心以及 OpenFeign 远程调用展开,最终把黑马商城从单体项目拆分为五个独立的微服务。

一、认识微服务

1.1 单体架构

单体架构将业务的所有功能集中在一个项目中开发,打成一个包部署。以黑马商城为例,商品管理、用户管理、购物车管理、交易管理等功能都集中在同一个 Project 中。

单体架构 - 黑马商城

单体架构的优缺点比较明显:

  • 优点:架构简单、部署成本低
  • 缺点:团队协作成本高、系统发布效率低、系统可用性差

总结来说,单体架构适合开发功能相对简单、规模较小的项目。

1.2 微服务架构

微服务架构是服务化思想指导下的一套最佳实践架构方案。服务化,就是把单体架构中的功能模块拆分为多个独立项目,每个微服务具备粒度小、团队自治、服务自治的特点。

微服务架构 - 黑马商城拆分

拆分后形成的是一个微服务集群,集群内还需要服务治理、远程调用、请求路由、配置管理、服务保护等能力共同支撑,最终对外提供完整的业务能力。

微服务集群全景

1.3 SpringCloud

SpringCloud 是目前国内使用最广泛的微服务框架,官网地址 https://spring.io/projects/spring-cloud。它集成了各种微服务功能组件,并基于 SpringBoot 实现了这些组件的自动装配,从而提供了良好的开箱即用体验:

  • 服务注册发现:Eureka、Nacos、Consul
  • 服务远程调用:OpenFeign、Dubbo
  • 服务链路监控:Zipkin、Sleuth
  • 统一配置管理:SpringCloudConfig、Nacos
  • 统一网关路由:SpringCloudGateway、Zuul
  • 流控、降级、保护:Hystrix、Sentinel

SpringCloud 组件总览

SpringCloud 基于 SpringBoot 实现了微服务组件的自动装配,但对 SpringBoot 的版本也有要求,常见版本对应关系如下:

SpringCloud 版本 SpringBoot 版本
2022.0.x aka Kilburn 3.0.x
2021.0.x aka Jubilee 2.6.x, 2.7.x (Starting with 2021.0.3)
2020.0.x aka Ilford 2.4.x, 2.5.x (Starting with 2020.0.3)
Hoxton 2.2.x, 2.3.x (Starting with SR5)
Greenwich 2.1.x
Finchley 2.0.x

SpringCloud 与 SpringBoot 版本对应关系

二、微服务拆分

2.1 服务拆分原则

什么时候拆分?需要区分项目类型:

  • 创业型项目:先采用单体架构,快速开发、快速试错。随着规模扩大,再逐渐拆分。
  • 确定的大型项目:资金充足、目标明确,可以直接选择微服务架构,避免后续拆分的麻烦。

怎么拆分?从目标上来说要做到:

  • 高内聚:每个微服务的职责要尽量单一,包含的业务相互关联度高、完整度高。
  • 低耦合:每个微服务的功能要相对独立,尽量减少对其它微服务的依赖。

从拆分方式来说,一般包含两种:

  • 纵向拆分:按照业务模块来拆分。
  • 横向拆分:抽取公共服务,提高复用性。

2.2 拆分黑马商城

黑马商城包含用户、商品、购物车、订单、支付五大模块,工程结构通常有两种方式:独立 Project 或 Maven 聚合。本项目中采用 Maven 聚合的方式,在父工程 hmall 下分别建立各业务 module。

按照需求,将 hm-service 中与商品管理相关的功能拆分到 item-service,与购物车相关的功能拆分到 cart-service,再依次建立 user-service、trade-service、pay-service。

2.3 远程调用

服务拆分后立刻会遇到一个问题:购物车服务在填充购物项的商品信息时,需要查询商品服务的数据,但两者已经不在同一个进程内,无法直接调用本地方法。

拆分后的远程调用问题

Spring 提供了 RestTemplate 工具,可以方便地发送 HTTP 请求。使用步骤如下:

  1. 注入 RestTemplate 到 Spring 容器:
@Bean
public RestTemplate restTemplate() {
    return new RestTemplate();
}
  1. 调用 exchange 方法发起远程调用:
public <T> ResponseEntity<T> exchange(
    String url,                              // 请求路径
    HttpMethod method,                       // 请求方式
    @Nullable HttpEntity<?> requestEntity,   // 请求实体,可以为空
    Class<T> responseType,                   // 返回值类型
    Map<String, ?> uriVariables              // 请求参数
)

例如,查询商品信息:

ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
        "http://localhost:8081/items?id={id}",
        HttpMethod.GET,
        null,
        new ParameterizedTypeReference<List<ItemDTO>>() {},
        Map.of("id", "1")
);

但这种方式存在明显缺陷:URL 中写死了 localhost:8081,当 item-service 部署多实例时无法选择,这就引出了服务治理的需求。

三、服务治理

3.1 注册中心原理

当 item-service 部署了多个实例时,购物车服务作为调用方无法知道该访问哪一个。注册中心正是用来解决这一问题:服务提供者启动时把自身信息注册到注册中心,消费者从注册中心订阅和拉取服务列表,再通过负载均衡挑选一个实例发起调用。

服务治理中有三个角色:

  • 服务提供者:暴露服务接口,供其它服务调用。
  • 服务消费者:调用其它服务提供的接口。
  • 注册中心:记录并监控微服务各实例状态,推送服务变更信息。

其工作机制如下:

  1. 服务提供者启动时注册自己信息到注册中心。
  2. 消费者从注册中心订阅和拉取服务信息。
  3. 服务提供者通过心跳机制向注册中心报告自己的健康状态;当心跳异常时注册中心会将异常服务剔除,并通知订阅了该服务的消费者。
  4. 消费者通过负载均衡算法,从多个实例中选择一个发起远程调用。

3.2 Nacos 注册中心

Nacos 是目前国内企业中占比最多的注册中心组件,由阿里巴巴出品,目前已加入 SpringCloudAlibaba 体系。

Nacos 注册中心

3.2.1 服务注册

服务注册的步骤如下:

  1. 引入 nacos discovery 依赖:
<!--nacos 服务注册发现-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
  1. 在 application.yaml 中配置 Nacos 地址:
spring:
  application:
    name: item-service # 服务名称
  cloud:
    nacos:
      server-addr: 192.168.150.101:8848 # nacos地址

启动项目后,可在 Nacos 控制台看到 item-service 的服务实例信息。

3.2.2 服务发现

消费者需要连接 Nacos 以拉取和订阅服务,因此服务发现的前两步与服务注册一致(引入依赖、配置地址),最后再加上服务调用即可。SpringCloud 已经帮我们自动装配了 DiscoveryClient,可直接注入使用:

private final DiscoveryClient discoveryClient;
 
private void handleCartItems(List<CartVO> vos) {
    // 1.根据服务名称,拉取服务的实例列表
    List<ServiceInstance> instances = discoveryClient.getInstances("item-service");
    // 2.负载均衡,挑选一个实例
    ServiceInstance instance = instances.get(RandomUtil.randomInt(instances.size()));
    // 3.获取实例的IP和端口
    URI uri = instance.getUri();
    // ... 后续借助 RestTemplate 发起调用
}

四、OpenFeign

4.1 快速入门

虽然借助 RestTemplate + DiscoveryClient 已经能完成远程调用,但代码相对繁琐。OpenFeign 是一个声明式的 HTTP 客户端,是 SpringCloud 在 Eureka 公司开源的 Feign 基础上改造而来。其作用就是基于 SpringMVC 的常见注解,帮我们优雅地实现 HTTP 请求的发送。

OpenFeign 概念图

一次完整的远程调用需要四个要素:服务名称、请求方式和路径、返回值类型、请求参数。OpenFeign 已经被 SpringCloud 自动装配,实现起来非常简单:

  1. 引入依赖,包括 OpenFeign 和负载均衡组件 SpringCloud LoadBalancer:
<!--OpenFeign-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--负载均衡-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
  1. 通过 @EnableFeignClients 注解启用 OpenFeign 功能:
@EnableFeignClients
@SpringBootApplication
public class CartApplication { // ... 略 }
  1. 编写 FeignClient:
@FeignClient(value = "item-service")
public interface ItemClient {
 
    @GetMapping("/items")
    List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
}
  1. 使用 FeignClient 实现远程调用,调用方式与本地方法一致:
List<ItemDTO> items = itemClient.queryItemByIds(List.of(1, 2, 3));

OpenFeign 会自动从注册中心拉取 item-service 的实例列表,并通过 SpringCloud LoadBalancer 进行负载均衡,最终以 HTTP 方式发起请求。

4.2 连接池

OpenFeign 对 HTTP 请求做了优雅的伪装,但其底层发起 HTTP 请求依赖于其它框架。这些框架可以自行选择,主要包括三种:

  • HttpURLConnection:默认实现,不支持连接池
  • Apache HttpClient:支持连接池
  • OKHttp:支持连接池

OpenFeign 整合 OKHttp 的步骤如下:

  1. 引入依赖:
<!--ok-http-->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
</dependency>
  1. 在 application.yaml 中开启连接池功能:
feign:
  okhttp:
    enabled: true # 开启OKHttp连接池支持

4.3 最佳实践

随着服务数量增多,每个服务都各自维护一份 FeignClient 会导致重复代码和数据传输对象。最佳实践是由服务提供者编写独立 module,将 FeignClient 及 DTO 抽取到一个公共 API 模块(如 hm-api),消费方只需引入该模块即可调用。

OpenFeign 最佳实践结构

当定义的 FeignClient 不在 SpringBootApplication 的扫描包范围时,可以通过两种方式解决:

  • 方式一:指定 FeignClient 所在包:
@EnableFeignClients(basePackages = "com.hmall.api.clients")
  • 方式二:指定 FeignClient 字节码:
@EnableFeignClients(clients = {UserClient.class})

4.4 日志

OpenFeign 只会在 FeignClient 所在包的日志级别为 DEBUG 时才会输出日志,且日志级别有 4 级:

  • NONE:不记录任何日志信息,这是默认值。
  • BASIC:仅记录请求的方法、URL 以及响应状态码和执行时间。
  • HEADERS:在 BASIC 的基础上,额外记录请求和响应的头信息。
  • FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。

由于 Feign 默认的日志级别就是 NONE,所以默认看不到请求日志。要自定义日志级别,需要声明一个类型为 Logger.Level 的 Bean:

public class DefaultFeignConfig {
    @Bean
    public Logger.Level feignLogLevel() {
        return Logger.Level.FULL;
    }
}

如果只想配置某个 FeignClient 的日志,可以在 @FeignClient 注解中声明:

@FeignClient(value = "item-service", configuration = DefaultFeignConfig.class)

如果想要全局配置,让所有 FeignClient 都按照这个日志配置,则在 @EnableFeignClients 注解中声明:

@EnableFeignClients(defaultConfiguration = DefaultFeignConfig.class)

总结

回顾本文,主要完成了以下几件事:

  • 什么时候拆分微服务?初创型公司或项目尽量采用单体项目,快速试错,随着项目发展到达一定规模再做拆分。
  • 如何拆分微服务?目标是高内聚、低耦合;方式是纵向拆分和横向拆分。
  • 拆分后碰到的第一个问题是什么,如何解决?拆分后某些数据在不同服务,无法直接调用本地方法查询,可利用 RestTemplate 发送 HTTP 请求实现远程调用。
  • 如何优雅地实现服务间调用?使用 Nacos 作为注册中心管理服务实例,使用 OpenFeign 声明式客户端简化 HTTP 请求的发送。
  • 如何进一步优化 OpenFeign?通过 OKHttp 连接池提升性能,通过抽取公共 API 模块复用 FeignClient,通过 Logger.Level 配置日志级别方便调试。