Java微服务应用开发(简版)实战之SpringCloud

微服务核心模块

Java微服务应用开发(简版)实战之SpringCloud

这是微服务的基本架构图,不同终端可以通过网关调用我们的核心服务,每个服务可以独立水平扩展,它们各自管辖自己的数据库。下面是SpringCloud相关常见技术栈(模块),我们将通过一个简化后的真实案例来串联起它们:

Eureka/Nacos:服务注册中心,后者由阿里巴巴开源

Ribbon:负载均衡组件

Hystrix:熔断器组件

Feign:请求客户端组件

SpringCloud GateWay:网关组件,提供路由、过滤等功能

1. 准备工作

下面我们通过一个案例来整体介绍这些组件。案例背景:B2C商城里,用户在购物时会生成订单,除了支付业务本身的订单状态处理之外,系统还会围绕这些订单分别给商家、用户端做些处理。最典型的比如,商家端要做订单统计、用户端要做订单查询、积分计算等等。为了将不同端的订单处理分层解耦,通常会划分多个服务,最简单的方案是分为商家服务和用户服务,商家服务管理商家订单、用户服务管理用户订单。

当用户下单后,前端通过调用平台聚合层来分别调用商家、用户服务。

所以,我们可以新建三个服务项目,PlatformDemo、MerchantDemo、UserDemo。按照微服务的理论,每个服务管控自己的数据库,所以可以新建两个单独的库,分别是merchantdb、userdb,然后分别新建各自的订单表merchantorder、userorder。(其实就是垂直分库)

编译相关命令

clean compile package -Dmaven.test.skip=true

PlatformDemo怎么调用MerchantDemo和UserDemo呢?两种方式:

  1. platform直接通过http调用merchant和user,优点是:简单,缺点是:假如merchant和user是多实例的,那么platform需要手动维护每个实例的地址;
  2. 将merchant、user注册到一个服务注册中心,然后platform仅通过单一的【服务名称】来路由到不同的merchnt、user服务实例。优点是:服务实例水平扩展很方便,不需要在platform维护实例地址。缺点是:要安装单独的服务注册中心。

在实际场景中,肯定会选2,原因就在于,微服务的意义就是让服务实例更方便的水平扩展,假如每次还得在调用层手动维护实例地址,会非常麻烦。另外,注册中心只需要安装一次,也不存在其他太复杂的操作。

2. Nacos基本介绍

微服务比较常见的注册中心有Eureka、ZK、Consul、Nacos等。Nacos由阿里巴巴开源,它提供了服务注册、配置管理等功能。其简单易用的风格,越来越受到大家的关注,我们的生产级项目都已采用,目前运行良好。
Java微服务应用开发(简版)实战之SpringCloud

实际上Nacos思路非常简单,它提供中心服务器(可集群扩展,消除单点)及控制台,服务提供者(比如Merchant服务)首先主动注册到中心服务,中心服务轮询其存活状态。服务消费者(比如Platform)根据固定的服务名从中心服务器调用目标服务。这种架构的优点是:服务提供者的水平扩展可以对服务消费者完全透明,后者不需要手动维护前者服务列表。

下面我们以Nacos为例,来对注册中心做个演示。

Nacos服务安装

安装过程可以看这里:https://nacos.io/zh-cn/docs/quick-start.html

我这里是按照源码方式安装,相关nacos命令在 distribution/target/nacos-server-$version/nacos/bin目录下。

启动命令:

sh startup.sh -m standalone

关停命令:

sh shutdown.sh

控制台页面: http://localhost:8848/nacos/ 默认密码:nacos/nacos

使用Nacos进行服务注册

首先引入nacos依赖:

<dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
      <version>>0.2.1.RELEASE</version>
</dependency>

这里我们采用生产验证过的0.2.1版本

代码及配置方面的变动:

在主类上加上@EnableDiscoveryClient注解
在配置文件中新增如下内容:

server.port=0
spring.application.name=merchant-service
spring.cloud.nacos.discovery.server-addr=localhost:8848

这里将port设置为0,意味着每次启动都会使用随机端口号,这主要是因为同一类的微服务实例通常会有多个,使用同样的固定端口会造成端口占用的问题。

Nacos控制台初探

启动主类后,即可在控制台的【服务列表】中看到merchant-service 服务:

Java微服务应用开发(简版)实战之SpringCloud

这里我们启动了3个实例,点击详情后,我们可以看到实例的权重及运行情况:

Java微服务应用开发(简版)实战之SpringCloud

在这里,我们可以直接编辑实例的权重,也可以直接上下线实例,后面我们会对此进行演示。

借助Nacos进行微服务调用

如之前所说,我们需要在Platform中调用Merchant服务,完成订单入库的操作。由于Merchant已经注册在了Nacos,所以Platform必须借助Nacos来完成服务的调用。

Platform项目的配置和前面类似,这里不再赘述,我们直接看怎么轮询调用Merchant服务。为了更清楚的演示轮询过程,我们直接采用LoadBalancerClient+RestTemplate的方案手动调用服务。LoadBalancerClient用于通过服务名选取服务信息(ip地址、端口号),RestTemplate用于做Http请求。

下面首先配置RestTemplate:

@Configuration
public class RestTemplateConfig {
    @Bean
    public RestTemplate restTemplate(ClientHttpRequestFactory factory){
        return new RestTemplate(factory);
    }

    @Bean
    public ClientHttpRequestFactory simpleClientHttpRequestFactory(){
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setReadTimeout(3000);//单位为ms
        factory.setConnectTimeout(3000);//单位为ms
        return factory;
    }
}

然后新建测试类,核心测试代码如下:

ServiceInstance serviceInstance = loadBalancerClient.choose("merchant-service");
String url = String.format("http://%s:%s/merchant/saveOrder",serviceInstance.getHost(),serviceInstance.getPort());
System.out.println("request url:"+url);
Object value=restTemplate.postForObject(url,null,String.class);

代码解释:首先通过ServiceInstance根据权重获取服务信息,该信息包括ip+端口,然后拼接服务地址信息,最后通过RestTemplate进行Http请求。

注意:在test之前,先启动多个merchant服务实例。大家不妨测试一下,假如请求多次,是能看到均衡负载的效果的。

上面这种方式比较手工一点,实际上,我们可以直接让RestTemplate集成Ribbon,实现LoadBalance的效果,做法很简单:

  1. 在构建RestTemplate时加上@LoadBalanced注解:
@LoadBalanced
@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory factory){
    return new RestTemplate(factory);
}
  1. 请求服务时,直接使用服务名而非IP+端口:
restTemplate.postForObject("http://merchant-service/merchant/saveOrder",null,String.class);

3. 微服务调用之Feign

Feign是SpringCloud中非常常用的一个HTTP客户端组件,它提供了接口式的微服务调用API。

首先确保项目中已经导入了Feign依赖:

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-openfeign</artifactId>
  <version>2.0.0.RELEASE</version>
</dependency>

然后创建目标服务的接口,比如我们这里需要调用Merchant服务,那么可以新建MerchantService接口专门来处理与之相关的服务调用:

@FeignClient(value="merchant-service")
public interface MerchantService {

    @PostMapping("/merchant/saveOrder")
    public String saveMerchantOrder();
}

这个接口非常容易理解:使用@FeignClient将接口定义为服务接口,使用SpringMVC的@PostMapping、@GetMapping注解将接口方法定义为服务映射方法。就这样,调用微服务的方式就和普通方法调用的方式没太大区别(至少感觉上是这样)。

有时候,我们需要在发起Feign请求时,可以做一些统一的处理,比如:header设置、请求监控等。此时我们可以配置Feign拦截器来实现。

Feign拦截器的实现方式非常简单,主要分为两步:

  1. 实现feign.RequestInterceptor接口,重写其apply方法,如下:
public class FeignRequestInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate requestTemplate) {
        requestTemplate.header("token","123");
    }
}
  1. 将其配置在@FeignClient(configuration)中:
@FeignClient(value="merchant-service",configuration = FeignRequestInterceptor.class)
public interface MerchantService {


    @PostMapping("/merchant/saveOrder")
    public String saveMerchantOrder();
}

此时我们可以先调整下Merchant服务的接口,使用@RequestHeader("token")来接收token参数。

Feign超时及重试机制

微服务之间调用最大的一个问题就是超时问题(没有之一)。比如说,当Platform调用Merchant时,由于网络不通或者Merchant服务响应缓慢,那么Platform是不能一直等待下去的,这样资源会一致被占用,前端也得不到快速响应。此时一般会设置超时时间。

大家可以测试一下,当连接不上服务端时,会报connect timeout,当服务端响应时间过长,会报read timeout。默认情况下,Feign是不会重试的,即重试逻辑为Retryer.NEVER_RETRY。我们可以根据实际情况作如下配置:

@Configuration
public class FeignConfigure {
    @Bean
    Request.Options feignOptions() {
        return new Request.Options(
                /**connectTimeoutMillis**/
                1 * 1000,
                /** readTimeoutMillis **/
                1 * 5000);
    }

    @Bean
    public Retryer feignRetryer() {
       return new Retryer.Default();
    }
}

该配置类里面,我们设置了连接超时未1秒、读取超时未5秒,然后默认重试机制会重试5次。测试方式比较简单,比如我们可以把Merchant服务从Nacos上摘除下来,或者在接口中手动设置sleep,这里不再给出。

调用方在收到超时异常时,很可能服务方会继续执行(比如执行过长导致de 超时),所以重试的前提是:【一定要保证服务方的幂等性】,即重复多次不会影响业务逻辑。

4. 微服务间的数据传输

在实际开发中,有一个很现实的问题是数据传输格式的约定问题。在微服务架构中,实现一个完整的功能需要涉及到多个服务的调用,每个服务都有与自己领域相关的数据封装,微服务之间的调用需要遵循对方的数据格式要求。以前面的订单为例,Platform在调用Merchant时,应该传入商户订单对象,然后被返回Merchant服务的响应对象。听起来很简单对吧?但是Platform和Merchant是不同项目,后者约定好的对象类在前者是不存在的,前者工程师需要手动新建匹配的类才行。在服务接口非常繁多的情况下,这种手工处理会占用工程师很多时间。为了让他们过的爽一点,我们应该让这些类/对象共享才对。

所以,笔者建议针对每个服务都新建一个DTO项目,专门用于定义数据传输对象。比如我们可以新建MerchantDTO,专门定义该服务对应的输入、输出对象,每次更新升级时,可以将其打入公司的私有仓库中。为了自动化这一过程,可以使用CI/CD工具(比如jenkins)自动拉取git代码并install/deploy到私有仓库。在需要调用Merchant服务时,在pom中加入依赖就可以了。在项目规模较小时,可以暂时只做一个DTO项目,涵盖所有服务,以后再拆也是OK的。

在DTO中,我们会约定两种类型的数据:请求参数值、响应返回值。请求参数与业务域相关。比如保存商户订单,那么请求参数就是商户订单数据,比如:

@ApiModel("商户订单实体")
@Setter
@Getter
public class MerchantOrderRequest {

    @ApiModelProperty(name = "ordername",value = "订单名称")
    private String ordername;

    @ApiModelProperty(name="price",value = "价格")
    private double price;

}

通常来说,响应返回值都会有些公共的字段,比如code、message等,一般来说会设计响应对象的基类,这样便于后面做统一的code处理:

@ApiModel(value = "默认响应实体")
@Setter
@Getter
public class DefaultResponseData {


    @ApiModelProperty(name = "code",value = "返回码,默认1000是成功、5000是失败")
    private String code;

    @ApiModelProperty(name = "message",value = "返回信息")
    private String message;
    /**
     * 额外数据
     */
    @ApiModelProperty(name = "extra",value = "额外数据")
    private String extra;
}

这里用到了swagger注解,这样在接口文档中就会有明确说明,方便调试。@Setter、@Getter主要用于生产Setter/Getter代码,有助于解放大家的双手,具体安装及依赖过程可以看这篇文章:
如何使用Lombok简化你的代码?

我们改造一下之前的saveMerchantOrder方法,让其传入MerchantOrderRequest、返回DefaultResponseData。

public DefaultResponseData saveMerchantOrder(
  @RequestBody MerchantOrderRequest merchantOrderRequest, 
  @RequestHeader("token") String token){
...
    
}

重新启动Merchant服务,打开swagger,可以看到请求和响应参数的描述:
Java微服务应用开发(简版)实战之SpringCloud

User服务可以完全按照同样的处理策略,这里不再赘述。

5. 使用Hystrix进行熔断保护(降级)

在分布式/微服务环境中往往会出现各种各样的问题,比如网络异常,超时等,而这些问题可能会导致系统的级联失败,即使不断重试,也可能无法解决的,还会耗费更多的资源。比如说我们Platform在调用Merchant时,后者的数据库突然挂了,然后系统卡顿或者不停报错,用户此时可能会不断刷新,做更多的请求。这最终会让应用程序由于资源耗尽而导致雪崩。遇到这种情况,更好的做法是在调用阶段进行熔断保护并做降级处理。

熔断保护类似于电路中的保险丝,当电流异常升高时会自动切断电流,以保护电路安全。在开发中,熔断器通常有三个状态,即Closed、Open、Half-Open,如下图:

Java微服务应用开发(简版)实战之SpringCloud

默认情况下,熔断器是关闭(Closed)的,一旦在某个时间窗口T(默认10秒)内发生异常或者超时的次数在N以上,那么熔断器就会开启(Open),然后在时间窗口S之后,熔断器会进入半开状态(Half-Open),此时假如新请求成功执行,那么会进入关闭状态(Closed),否则继续开启(Open)。为了让熔断后能快速降级,我们通常需要指定相应的fallback处理逻辑。

在SpringCloud中,我们主要使用Hystrix组件来完成熔断降级,下面看看怎么实现。

首先,我们得引入依赖:

<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

在启动类上加@EnableHystrix注解,开启Hystrix。

在API层,我们只需要加@HystrixCommand注解即可。如前面所说,当接口熔断后,我们需要指定降级逻辑,即指定fallback方法:

   @GetMapping("/simpleHystrix")
    @HystrixCommand(fallbackMethod = "fallbackHandler"
    })
    public String simpleHystrix(@RequestParam("count") Integer count){
        System.out.println("执行.................");
        int i=10/count;
        return "success";
    }

    public String fallbackHandler(Integer count){
        System.out.println("count="+count);
        return "fail";
    }

这里我们定义了一个简单的接口,当count=0时,很明显会发生异常,在某段时间内出现异常的次数达到阈值,新请求就会进入fallbackHandler进行处理,不会继续调用simpleHystrix的逻辑。我们可以通过commandProperties/@HystrixProperty指定一些基本的参数,比如:

commandProperties = {
            @HystrixProperty(name=HystrixPropertiesManager.CIRCUIT_BREAKER_REQUEST_VOLUME_THRESHOLD,value = "3"),
            @HystrixProperty(name =HystrixPropertiesManager.CIRCUIT_BREAKER_SLEEP_WINDOW_IN_MILLISECONDS,value = "20000")

这里我们指定了10秒内出现3次异常,就会进入Open状态,然后再20秒之后,会进入Half-Open状态。

Feign整合Hystrix

在实际场景中,熔断器解决的大部分是微服务调用的问题,所以这里我们看看怎样让Feign整合Hystrix。

前面提到过的@FeignClient,其实直接支持配置Hystrix。它支持的方式有两种:fallback、fallbackFactory。前者比较简单,仅需要配置当前接口实现类作为降级函数,后者功能丰富一点,可以获取触发降级的原因。我们这里先用前者快速实现一下。

首先定义fallback类,该类实现服务接口及其所有方法,以MerchantService为例:

public class MerchantServiceFallBack implements MerchantService {
    
    @Override
    public DefaultResponseData saveMerchantOrder(MerchantOrderRequest merchantOrderRequest) {
        DefaultResponseData responseData=new DefaultResponseData();
        responseData.setCode("1001");
        responseData.setMessage("fallback");
        return responseData;
    }
}

然后在@FeignClient中加上:

fallback = MerchantServiceFallBack.class

最后,别忘记在配置文件中开启feign-hystrix:

feign.hystrix.enabled=true

当调用MerchantService接口服务时,一旦出现异常情况,会转入MerchantServiceFallBack的逻辑。

6. API网关之Spring Cloud Gateway

API网关主要解决的问题有:API鉴权、流量控制、请求过滤、聚合服务等。它并非微服务的必需品,具体怎么用得看实际场景。目前比较流行的网关有Zuul、Spring Cloud GateWay等。前者比较老牌了,网上资料也较多,而后者是新贵,算是SpringCloud的亲儿子,个人感觉也更好用,我们以它为例来讲解API网关的常见用法。

Spring Cloud Gateway基于Spring5、Reactor以及SpringBoot2构建,提供路由(断言、过滤器)、熔断集成、请求限流、URL重写等功能。

SpringBoot/SpringCloud系列的组件太多,经常会出现版本不对应,以下是经过测试无误的搭配:

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.4.RELEASE</version>
  </parent>
  
  <dependencies>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-dependencies</artifactId>
      <version>Finchley.RELEASE</version>
      <type>pom</type>
    </dependency>

    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-gateway</artifactId>
      <version>2.0.4.RELEASE</version>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>
  </dependencies>

最重要的一步是定义路由:

@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
    return builder.routes()
            .route(p -> p
                    .path("/api/order/gateWay")
                    .uri("http://localhost:8889"))
                .build();
    }

代码解释:当访问本服务的/api/order/gateWay时,会将请求转发到http://localhost:8889/api/order/gateWay。然后我们也可以在转发请求前进行过滤处理,比如新增header参数、请求参数等,大家可以自行测试:

filters(f -> f.addRequestHeader("token", "123"))

在实际项目中,网关所调用的目标服务都注册在注册中心里面,所以一般来说,会让网关访问注册中心地址。假如用的是Nacos,可以将uri中的http换成lb:

lb://platform-service
上一篇:SpringCloud 应用在 Kubernetes 上的最佳实践 — 部署篇(开发部署)


下一篇:中小企业如何选择阿里云服务器配置?(小白教程)