天天看點

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時,會将請求轉發到

:8889/api/order/gateWay。然後我們也可以在轉發請求前進行過濾處理,比如新增header參數、請求參數等,大家可以自行測試:

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

在實際項目中,網關所調用的目标服務都注冊在注冊中心裡面,是以一般來說,會讓網關通路注冊中心位址。假如用的是Nacos,可以将uri中的http換成lb:

lb://platform-service