Spring Cloud 核心組件——注冊中心


1. 什么是微服務的注冊中心

注冊中心:服務管理,核心是有個服務注冊表,心跳機制動態維護。

為什么要用?

微服務應用和機器越來越多,調用方需要知道接口的網絡地址,如果靠配置文件的方式去控制網絡地址,對於動態新增機器,維護帶來很大問題。

主流的注冊中心:Zookeeper、Eureka、Consul、ETCD 等。

 

服務提供者 Provider:啟動的時候向注冊中心上報自己的網絡信息。

服務消費者 Consumer:啟動的時候向注冊中心上報自己的網絡信息,拉取 Provider 的相關網絡信息。

 

2. 分布式應用知識CAP理論知識

CAP定理:指的是在一個分布式系統中,Consistency(一致性)、 Availability(可用性)、Partition Tolerance(分區容錯性),三者不可同時獲得。

一致性(C):在分布式系統中的所有數據備份,在同一時刻是否同樣的值。(所有節點在同一時間的數據完全一致,越多節點,數據同步越耗時)

可用性(A):負載過大后,集群整體是否還能響應客戶端的讀寫請求。(服務一直可用,而且是正常響應時間)

分區容錯性(P):分區容忍性,就是高可用性,一個節點崩了,並不影響其它的節點。(100個節點,掛了幾個,不影響服務,越多機器越好)

CAP理論就是說在分布式存儲系統中,最多只能實現上面的兩點。而由於當前的網絡硬件肯定會出現延遲丟包等問題,所以分區容忍性是我們必須需要實現的。所以我們只能在一致性和可用性之間進行權衡。

原因:

CA 滿足的情況下,P 不能滿足的原因:數據同步(C)需要時間,也要正常的時間內響應(A),那么機器數量就要少,所以P就不滿足。

CP 滿足的情況下,A 不能滿足的原因:數據同步(C)需要時間,,機器數量也多(P),但是同步數據需要時間,所以不能再正常時間內響應,所以A就不滿足。

AP 滿足的情況下,C不能滿足的原因:機器數量也多(P),正常的時間內響應(A),那么數據就不能及時同步到其他節點,所以C不滿足。

注冊中心選擇:

Zookeeper:CP 設計,保證了一致性,集群搭建的時候,某個節點失效,則會進行選舉行的 leader,或者半數以上節點不可用,則無法提供服務,因此可用性沒法滿足。

Eureka:AP 原則,無主從節點,一個節點掛了,自動切換其他節點可以使用,去中心化。

結論:

分布式系統中P,肯定要滿足,所以只能在 C 和 A 中二選一。沒有最好的選擇,最好的選擇是根據業務場景來進行架構設計,如果要求一致性,則選擇 Zookeeper,如金融行業;如果要去可用性,則 Eureka,如電商系統。

 

Eureka 原理圖:

Spring Cloud 體系官方地址:http://projects.spring.io/spring-cloud/

參考:

https://www.jianshu.com/p/d32ae141f680
https://blog.csdn.net/zjcjava/article/details/78608892

 

3. 使用 IDEA 搭建 Eureka 服務中心 Server 端並啟動

官方文檔:http://cloud.spring.io/spring-cloud-netflix/single/spring-cloud-netflix.html#spring-cloud-eureka-server

第一步:創建項目

和創建普通的 Spring Boot 項目是一樣的,只是需要選擇下圖所示依賴

第二步:添加注解 @EnableEurekaServer

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

第三步:增加配置 application.yml(其實可以使用 application.properties,但官網上使用 yml,這里我們照抄官網)

server:
  port: 8761

eureka:
  instance:
    hostname: localhost
  client:
    registerWithEureka: false
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

第四步:訪問注冊中心頁面

使用 http://localhost:8761/ 訪問注冊中心頁面,這個地址按照配置文件中的來,這里注意下,按照配置文件說的,訪問地址為:http://localhost:8761/eureka/

但不同版本,可能不一樣,我所使用的版本是 Greenwich,它就不能添加 /eureka/ 否則會報 404 錯誤

 

Eureka 管理后台出現一串紅色字體:EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.

這個是警告,說明有服務上線率低。

關閉檢查方法:Eureka 服務端配置文件加入

server:
  enable-self-preservation: false

注意:自我保護模式禁止關閉,默認是開啟狀態 true

 

4. 創建商品服務,並將服務注冊到注冊中心

第一步:創建一個 Spring Boot 應用,增加服務注冊和發現依賴


第二步:模擬商品信息,存儲在內存中

第三步:開發商品列表接口,商品詳情接口

第四步:配置文件加入注冊中心地址

server:
  port: 8771


#指定注冊中心地址
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/

#服務的名稱
spring:
  application:
    name: product-service

我們可以用當前項目啟動多個實例,參考教程:https://blog.csdn.net/zhou520yue520/article/details/81167841

啟動后,我們可以登錄下訪問注冊中心頁面,看到如下效果:

為什么只加一個注冊中心地址,就可以注冊?

官網解釋:By having spring-cloud-starter-netflix-eureka-client on the classpath, your application automatically registers with the Eureka Server.(也就是說,這樣 Jar 包在類路徑上,就可以自動識別)

 

5.常用的服務間的調用方式

RPC:遠程過程調用,像調用本地服務(方法)一樣調用服務器的服務。支持同步、異步調用。客戶端和服務器之間建立 TCP 連接,可以一次建立一個,也可以多個調用復用一次鏈接。PRC 數據包小。

Rest:Http 請求,支持多種協議和功能。開發方便成本低。Http 數據包大。類似 HttpClient,URLConnection。

 

6.訂單服務調用商品服務獲取商品信息

第一步:創建 order_service 項目

注意:調用方需要引入 Ribbon 依賴

第二步:使用 Ribbon(類似 HTTPClient,URLConnection)

啟動類增加注解

@Bean
@LoadBalanced public RestTemplate restTemplate() {
    return new RestTemplate();
}

第三步:開發偽下單接口

第四步:根據名稱進行調用商品,獲取商品詳情

注意:紅字標識的就是上面商品服務的 spring.application.name

@Service
public class ProductOrderServiceImpl implements ProductOrderService {


    @Autowired
    private RestTemplate restTemplate;

    @Override
    public ProductOrder save(int userId, int productId) {

        Object obj = restTemplate.getForObject("http://product-service/api/v1/product/find?id="+productId, Object.class);

        System.out.println(obj);

        ProductOrder productOrder = new ProductOrder();
        productOrder.setCreateTime(new Date());
        productOrder.setUserId(userId);
        productOrder.setTradeNo(UUID.randomUUID().toString());

        return productOrder;
    }
}

當我們用請求多次訪問時,可以從控制台看到端口號的不同,說明這是從不同端口的應用返回的數據

商品服務的 Controller 如下:

@RestController
@RequestMapping("/api/v1/product")
public class ProductController {

    @Value("${server.port}")
    private String port;

    @Autowired
    private ProductService productService;

    /**
     * 獲取所有商品列表
     * @return
     */
    @RequestMapping("list")
    public Object list(){
        return productService.listProduct();
    }

    /**
     * 根據id查找商品詳情
     * @param id
     * @return
     */
    @RequestMapping("find")
    public Object findById(int id){

        Product product = productService.findById(id);

        Product result = new Product();
        BeanUtils.copyProperties(product,result);
        result.setName( result.getName() + " data from port="+port );
        return result;
    }
}

我們還可以通過另一種調用方式進行調用

//調用方式二
ServiceInstance instance = loadBalancer.choose("product-service");
String url = String.format("http://%s:%s/api/v1/product/find?id="+productId, instance.getHost(),instance.getPort());
RestTemplate restTemplate = new RestTemplate();
Object obj = restTemplate.getForObject(url, Object.class);
//Map<String,Object> productMap = restTemplate.getForObject(url, Map.class);

調用原理:

1)首先從注冊中心獲取 Provider 的列表

2)通過一定的策略選擇其中一個節點

3)再返回給 restTemplate 調用

我們也可以自定義負載均衡策略,官網說明:https://cloud.spring.io/spring-cloud-static/spring-cloud-netflix/2.2.0.M3/reference/html/#customizing-the-ribbon-client-by-setting-properties

server:
  port: 8781


#指定注冊中心地址
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/

#服務的名稱
spring:
  application:
    name: order-service

#自定義負載均衡策略 product-service: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

策略選擇:

1)如果每個機器配置一樣,則建議不修改策略 (推薦)

2)如果部分機器配置強,則可以改為 WeightedResponseTimeRule

 

7.使用 Feign 改造訂單服務 

Feign: 偽 RPC 客戶端(本質還是用http)

官方文檔: https://cloud.spring.io/spring-cloud-openfeign/

第一步:加入依賴

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

第二步:啟動類增加 @EnableFeignClients

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

第三步:增加一個接口並使用注解 @FeignClient(name="product-service")

package com.jwen.order_service.service;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

/**
 * 商品服務客戶端
 */
@FeignClient(name = "product-service")
public interface ProductClient {
    @GetMapping("/api/v1/product/find")
    String findById(@RequestParam(value = "id") int id);
}

注意點:

1)服務名和 Http 方法必須對應

2)使用 RequestBody,應該使用 @PostMapping

3)多個參數的時候,通過 @RequestParam(value = "id") int id 方式調用

第四步:更改調用方式編碼

package com.jwen.order_service.service.impl;

import com.fasterxml.jackson.databind.JsonNode;
import com.jwen.order_service.domain.ProductOrder;
import com.jwen.order_service.service.ProductClient;
import com.jwen.order_service.service.ProductOrderService;
import com.jwen.order_service.utils.JsonUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.UUID;

@Service
public class ProductOrderServiceImpl implements ProductOrderService {
    @Autowired
    private ProductClient productClient;

    @Override
    public ProductOrder save(int userId, int productId) {
        String response = productClient.findById(productId);
        JsonNode jsonNode = JsonUtils.str2JsonNode(response);

        ProductOrder productOrder = new ProductOrder();
        productOrder.setCreateTime(new Date());
        productOrder.setUserId(userId);
        productOrder.setTradeNo(UUID.randomUUID().toString());
        productOrder.setProductName(jsonNode.get("name").toString());
        productOrder.setPrice(Integer.parseInt(jsonNode.get("price").toString()));
        return productOrder;
    }
}
JsonUtils 工具類的代碼
package com.jwen.order_service.utils;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;

/**
 * json工具類
 */
public class JsonUtils {
    private static final ObjectMapper objectMappper = new ObjectMapper();
    /**
     * json字符串轉JsonNode對象的方法
     */
    public static JsonNode str2JsonNode(String str){
        try {
            return  objectMappper.readTree(str);
        } catch (IOException e) {
            return null;
        }
    }
}

Ribbon 和 Feign 兩個之間,應該選擇 Feign。Feign 默認集成了 Ribbon。寫起來更加思路清晰和方便。采用注解方式進行配置,配置熔斷等方式方便

超時配置

#修改調用超時時間
feign:
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 5000

模擬接口響應慢,線程睡眠新的方式

try {
    TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
    e.printStackTrace();
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM