2023-08-12
原文作者:Ressmix 原文地址:https://www.tpvlog.com/article/187

我曾在分布式理论篇——服务化拆分中介绍过微服务,Spring Cloud就是微服务架构的集大成者,将一系列优秀的组件进行了整合。通过Spring Cloud,我们可以快速构建一套基于微服务架构的分布式系统。

Spring Cloud的组件相当繁多,其提供的核心组件如下:

  • Eureka:服务注册中心
  • Feign:服务调用
  • Ribbon:负载均衡
  • Zuul / Spring Cloud Gatway:网关

202308122222205051.png

纯粹的讲Spring Cloud的各个组件非常枯燥,对于很多初学者童鞋也不太友好。所以,本章我将引入一个电商系统作为案例背景,讲解Spring Cloud的各个核心组件的基本使用。

读者可以参考Spring Cloud的官网资料: https://spring.io/projects/spring-cloud ,对其进行更多的了解。我如果后面有时间,也会在实战篇中详细讲解Spring Cloud的核心应用。

一、案例背景

我们的案例是一个普通的电商系统,拆分成四个子系统:订单系统、库存系统、仓储系统、积分系统,看过分布式理论篇的童鞋肯定已经不陌生了。系统的逻辑架构图如下:

202308122222212382.png

一次下订单的请求需要多个子系统协作完成,每个子系统都完成一部分的功能,多个子系统分别完成自己负责的事情,最终这个请求就处理完毕 。

下面我们就来看下,如果引入Spring Cloud,如果对这些子系统进行管理,使他们能够有条不紊地相互协作。

二、服务注册中心(Eureka)

2.1 作用

首先,我们需要一个服务注册中心,当服务提供方启动后,需要向服务注册中心注册自己的服务信息(接口URI、机器地址、端口等),这一过程叫做服务注册

与此同时,服务消费方也要从 Eureka 拉取一份服务注册清单,方便自己后续调用其它服务使用,这一过程也叫做服务发现

在Spring Cloud中,由 Eureka 来担任注册中心的角色:

202308122222218153.png

2.2 使用方式

Eureka的使用方式很简单,首先,我们创建一个Spring Boot应用,配置application.yml文件:

    server:
      port: 8761
    
    eureka:
      instance:
        hostname: localhost
        leaseExpirationDurationInSeconds: 2
      client:
        registerWithEureka: false
        fetchRegistry: false
        serviceUrl:
          defaultZone: http://peer1:8761/eureka/
      server:
        enableSelfPreservation: false
        responseCacheUpdateIntervalMs: 1000
        evictionIntervalTimerInMs: 1000

上面各个参数的含义,我会在讲Eureka原理时详细讲解。这里注意下defaultZone这个参数,这个参数表示Eureka对外提供服务的一个地址。我们可以通过下面这样启动Eureka:

    @SpringBootApplication
    @EnableEurekaServer
    public class EurekaServer {
        public static void main(String[] args) {
            SpringApplication.run(EurekaServer.class, args);
        }
    }

2.3 服务注册

有了服务注册中心,我们就可以注册服务了。下面我们来创建库存、仓储、积分三个服务。

库存服务

    // 库存服务接口
    public interface InventoryApi {
        String deductStock(Long productId, Long stock);
    }
    
    // 库存API实现
    @RestController
    @RequestMapping("/inventory")
    public class InventoryService implements InventoryApi {
    
        @RequestMapping(value = "/deduct/{productId}/{stock}", method = RequestMethod.PUT)
        public String deductStock(@PathVariable("productId") Long productId, 
                @PathVariable("stock") Long stock) {
            System.out.println("对商品【productId=" + productId + "】扣减库存:" + stock);    
            return "{'msg': 'success'}";
        }
    }

然后是库存服务的yml配置:

    server:
      port: 8080
    spring:
      application:
        name: inventory-service
    eureka:
      instance:
        hostname: localhost
      client:
        serviceUrl:
          defaultZone: http://peer1:8761/eureka
        registryFetchIntervalSeconds: 1
        leaseRenewalIntervalInSeconds: 1
    
    ribbon:
      eager-load:
        enable: true
    
    feign:
      hystrix:
        enabled: false

最后,我们可以这样启动库存服务:

    @SpringBootApplication
    @EnableEurekaClient
    public class InventoryServiceApplication {
        public static void main(String[] args) {
            SpringApplication.run(InventoryServiceApplication.class, args);
        }
    }

仓储服务

    // 仓储服务接口
    public interface WmsApi {
        String delivery(Long productId);
    }
    
    // 仓储API实现
    @RestController
    @RequestMapping("/wms") 
    public class WmsService implements WmsApi {
    
        @RequestMapping(value = "/delivery/{productId}", method = RequestMethod.PUT)
        public String delivery(@PathVariable("productId") Long productId) {
            System.out.println("对商品【productId=" + productId + "】进行发货");    
            return "{'msg': 'success'}";
        }
    }

然后是仓储服务的yml配置:

    server:
      port: 8081
    
    spring:
      application:
        name: wms-service
    
    eureka:
      instance:
        hostname: localhost
      client:
        serviceUrl:
          defaultZone: http://localhost:8761/eureka
        registryFetchIntervalSeconds: 1
        leaseRenewalIntervalInSeconds: 1
    
    ribbon:
      eager-load:
        enable: true
    
    feign:
      hystrix:
        enabled: false

最后,我们可以这样启动仓储服务:

    @SpringBootApplication
    @EnableEurekaClient
    public class WmsServiceApplication {
        public static void main(String[] args) {
            SpringApplication.run(WmsServiceApplication.class, args);
        }
    }

积分服务

    // 积分服务接口
    public interface CreditApi {
        String add(Long userId, Long credit);
    }
    
    // 积分API实现
    @RestController
    @RequestMapping("/credit") 
    public class CreditService implements CreditApi {
    
        @RequestMapping(value = "/add/{userId}/{credit}", method = RequestMethod.PUT)
        public String add(@PathVariable("userId") Long userId, 
                @PathVariable("credit") Long credit) {
            System.out.println("对用户【userId=" + userId + "】增加积分:" + credit);    
            return "{'msg': 'success'}";
        }
    }

然后是积分服务的yml配置:

    server:
      port: 8082
    
    spring:
      application:
        name: credit-service
    
    eureka:
      instance:
        hostname: localhost
      client:
        serviceUrl:
          defaultZone: http://localhost:8761/eureka
        registryFetchIntervalSeconds: 1
        leaseRenewalIntervalInSeconds: 1
    
    ribbon:
      eager-load:
        enable: true
    
    feign:
      hystrix:
        enabled: false

最后,我们可以这样启动积分服务:

    @SpringBootApplication
    @EnableEurekaClient
    public class CreditServiceApplication {
        public static void main(String[] args) {
            SpringApplication.run(CreditServiceApplication.class, args);
        }
    }

三、服务调用(Feign)

3.1 作用

服务之间需要能够互相调用,我们曾经介绍过Dubbo的使用及其原理,Spring Cloud也有类似的分布式服务调用框架—— Feign 。如下图所示,订单系统如果要调用其它系统提供的服务,在Spring Cloud中实际是通过Feign组件完成的:

202308122222234674.png

3.2 使用方式

我们以订单服务为例,看下如何使用Feign。首先,订单服务要通过Feign远程调用库存、积分、仓库服务,所以我们需要定义三个接口,加上@Feign注解。这些接口的签名需要与服务提供方完全一致,包含请求的URI:

    /**
     * 库存服务Feign
     */
    @FeignClient(value = "inventory-service")
    @RequestMapping("/inventory")
    public interface InventoryServiceFeign {
        @RequestMapping(value = "/deduct/{productId}/{stock}", method = RequestMethod.PUT)
        String deductStock(Long productId, Long stock);
    }
    
    /**
     * 仓储服务Feign
     */
    @FeignClient(value = "wms-service")
    @RequestMapping("/wms") 
    public interface WmsFeignService {
        @RequestMapping(value = "/delivery/{productId}", method = RequestMethod.PUT)
        String delivery(Long productId);
    }
    
    /**
     * 积分服务Feign
     */
    @FeignClient(value = "credit-service")
    @RequestMapping("/credit")
    public interface CreditFeignService {
        @RequestMapping(value = "/add/{userId}/{credit}", method = RequestMethod.PUT)
        String add(Long userId, Long credit);
    }

上面的@FeignClient注解的value表示要消费的服务名称,是在服务提供方的yml文件中配置的。Feign会自动生成访问后台服务的代理接口服务,后续我们讲Feign原理时会专门讲解。

3.3 服务调用

订单服务的实现,主要用来接受客户端请求,然后创建订单,最后发起对下游依赖服务的调用:

    @RestController
    @RequestMapping("/order")
    public class OrderController {
    
        @Autowired
        private InventoryServiceFeign inventoryService;
        @Autowired
        private WmsServiceFeign wmsService;
        @Autowired
        private CreditServiceFeign creditService;
    
        @RequestMapping(value = "/create", method = RequestMethod.GET)
        public String greeting(
                @RequestParam("productId") Long productId,
                @RequestParam("userId") Long userId,
                @RequestParam("count") Long count,
                @RequestParam("totalPrice") Long totalPrice) {
    
            System.out.println("创建订单");
    
            inventoryService.deductStock(productId, count);
            wmsService.delivery(productId);
            creditService.add(userId, totalPrice);
    
            return "success";
        }
    
    }

然后是Spring Boot启动类:

    @SpringBootApplication
    @EnableEurekaClient
    @EnableFeignClients
    public class OrderServiceApplication {
        public static void main(String[] args) {
            SpringApplication.run(OrderServiceApplication.class, args);
        }
    }

最后是订单服务的yml配置:

    server:
      port: 9090
    
    spring:
      application:
        name: order-service
    
    eureka:
      instance:
        hostname: localhost
      client:
        serviceUrl:
          defaultZone: http://localhost:8761/eureka
        registryFetchIntervalSeconds: 1
        leaseRenewalIntervalInSeconds: 1
    
    ribbon:
      eager-load:
        enable: true
    
    # 默认先关掉的hystrix(设计好完整高可用方案再打开),防止因为hystrix的默认行为影响对程序结果的判断
    feign:
      hystrix:
        enabled: false

四、负载均衡(Ribbon)

如果某个子系统部署了多个对等服务,那也会在注册中心注册多个相同的服务,以订单系统来说,那可能拉取到的注册表中有多个库存系统服务,此时调用方就需要选择一个服务发起调用,这其实就是客户端负载均衡。

Dubbo提供了Cluster路由层来完成客户端负载均衡,而Spring Cloud提供了 Ribbon 作为客户端负载均衡组件:

202308122222249805.png

4.1 超时配置

上述的库存、积分、仓库服务,第一次被请求的时候,都会去初始化一个Ribbon组件,初始化组件需要耗费一定的时间,很容易会导致超时问题。所以,生产环境一般都会对服务进行配置,使服务启动时就初始化Ribbon相关的组件,避免第一次请求时再初始化。

以库存服务为例,可以在application.yml中做加上如下Ribbon配置:

    # 启用饥饿加载,服务启动时即加载ribbon组件
    ribbon:
      eager-load:
          enable: true
      ConnectTimeout: 1000
      ReadTimeout: 1000
      OkToRetryOnAllOperations: true
      # 对于当前调用服务的重试次数 
      MaxAutoRetries: 1
      # 其它对等服务的重试次数
      MaxAutoRetriesNextServer: 1

上述除了饥饿加载外,还加上了超时时间和重试次数的配置,这样就可以通过Ribbon来控制服务调用方发生调用超时时的行为。

五、API网关(Zuul)

客户端一般通过Nginx与后端服务进行交互,每个微服务一般会有不同的网络地址,当服务越来越多后,客户端可能需要调用多个服务的接口才能完成一次业务请求,如果让客户端直接与各个微服务通信,会有以下的问题:

  1. 认证复杂,每个服务都需要独立认证;
  2. 客户端会多次请求不同的微服务,增加了客户端的复杂性;
  3. 难以重构,随着项目的迭代,可能需要重新划分微服务,如果客户端直接与微服务通信,那么重构将会很难实施。

所以,我们需要一个统一的构件来处理上述问题。Spring Cloud Gateway 或者Zuul作为API网关可以解决上述问题。

5.1 作用

以Zuul为例,Zuul是介于客户端和服务器端之间的中间层,所有的客户端请求都会先经过网关这一层。网关提供了API全托管服务,辅助管理大规模的API,还包括灰度发布、统一熔断、统一降级、统一缓存、统一限流、统一授权认证等功能。

202308122222261166.png

5.2 使用方式

我们使用Zuul来作为API网关,可以新建一个Spring Boot应用,做如下配置:

    server:
      port: 9000
    
    spring:
      application:
        name: zuul-gateway
    
    eureka:
      instance:
        hostname: localhost
      client:
        serviceUrl:
          defaultZone: http://localhost:8761/eureka/
        registryFetchIntervalSeconds: 1
        leaseRenewalIntervalInSeconds: 1
    
    zuul:
      retryable: true
      routes:
        order-service:
          path: /order/**
    
    ribbon:
      eager-load:
          enable: true
      ConnectTimeout: 3000
      ReadTimeout: 3000
      OkToRetryOnAllOperations: true
      # 对于当前调用服务的重试次数 
      MaxAutoRetries: 1
      # 其它对等服务的重试次数
      MaxAutoRetriesNextServer: 1

网关的启动类如下:

    @SpringBootApplication
    @EnableZuulProxy
    public class ZuulGatewayApplication {
        public static void main(String[] args) {
            SpringApplication.run(ZuulGatewayApplication.class, args);
        }
    }

上述9000是网关的服务端口,处理/order路径的所有请求。

六、总结

综上,一套最简单的微服务系统就搭建起来了,整个项目的模块划分如下图:

202308122222284347.png

客户端要进行下单操作时,直接访问以下接口:
http://localhost:9000/order/order/create?productId=1&userId=1&count=3&totalPrice=300

其中 http://localhost:9000 是Zuul网关的地址,通过/order可以映射到后端的订单服务。

阅读全文