主頁 > 後端開發 > 學習一下 SpringCloud (二)-- 服務注冊中心 Eureka、Zookeeper、Consul、Nacos

學習一下 SpringCloud (二)-- 服務注冊中心 Eureka、Zookeeper、Consul、Nacos

2020-12-27 06:14:30 後端開發

(1) 相關博文地址:

學習一下 SpringCloud (一)-- 從單體架構到微服務架構、代碼拆分(maven 聚合): https://www.cnblogs.com/l-y-h/p/14105682.html

(2)代碼地址:

https://github.com/lyh-man/SpringCloudDemo

 

一、從零開始 搭建、優化 微服務

1、專案說明

【基本說明:】
    上一篇介紹了 架構演變 以及 代碼拆分,詳見:https://www.cnblogs.com/l-y-h/p/14105682.html
    從這篇開始,將從零開始搭建微服務,逐步對 代碼進行 優化,并選擇相關技術解決 微服務相關問題,
    
【基本環境:】
    開發工具:IDEA
    編碼環境:Java8 + MySQL 8
    框架:SpringBoot 2.3.5 + SpringCloud Hoxton.SR9 + MyBatisPlus 3.3.1
注:
    微服務相關技術此處不一一列舉出來了,有些技術僅會簡單使用、原理部分并沒有全弄懂(持續學習中,有不對的地方還望不吝賜教),
    MyBatisPlus 基本使用可參考:https://www.cnblogs.com/l-y-h/p/12859477.html
    搭建 SpringBoot 專案可參考:https://www.cnblogs.com/l-y-h/p/13083375.html

 

2、基本專案創建

(1)專案簡介

【專案簡介:】
    上一篇介紹了 垂直拆分 代碼,詳見:https://www.cnblogs.com/l-y-h/p/14105682.html#_label1_2
    此處以此為基礎,逐步優化、并使用相關微服務技術去搭建,

【專案基本模塊:(從最簡單開始,后續模塊視情況添加)】
    專案分為兩個模塊:生產者模塊(producer)、消費者模塊(consumer),
    生產者模塊 用于 提供 各種服務,
    消費者模塊 用于 訪問 各種服務,
注:
    生產者提供各種服務,其需要與資料庫進行互動(controller、service、mapper 都需要),
    消費者訪問服務,只需要撰寫 controller 即可,消費者 去 遠程訪問 生產者服務,
    可以使用 RestTemplate 進行遠程服務呼叫,

【專案命名約定:】
    為了便于區分各服務模塊,各個模塊服務名 命名規則為: 模塊名 + _ + 埠號,
    比如:生產者模塊為 producer_8000、消費者模塊為 consumer_9000

 

(2)采用 maven 聚合 SpringBoot 子模塊的方式創建專案
  基本操作詳見上一篇:https://www.cnblogs.com/l-y-h/p/14105682.html#_label1_2

Step1:創建 maven 聚合工程(SpringCloudDemo),

 

 

 

Step2:創建 SpringBoot 子模塊(producer_8000),
  修改子模塊組態檔(埠號為 8000、服務名為 producer),
  修改子模塊 pom.xml 中 <parent> 標簽,指向父工程,
  修改父工程 pom.xml 中 <module> 標簽,指向子模塊,.

 

 

 

Step3:引入 producer_8000 所需依賴,
  引入 MyBatisPlus 以及 MySQL 等依賴,在 父工程 進行版本控制,

【建一個表(producer_user),SQL 如下:】
DROP DATABASE IF EXISTS producer;

CREATE DATABASE producer;

USE producer;

CREATE TABLE producer_user(
    id BIGINT(20) AUTO_INCREMENT COMMENT 'ID',
    name VARCHAR(100) COMMENT 'Name',
    PRIMARY KEY (id)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COMMENT 'user';

【在父工程(SpringCloudDemo)中進行版本控制:】
<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.lyh.springcloud</groupId>
  <artifactId>SpringCloudDemo</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>pom</packaging>

  <modules>
    <module>producer_8000</module>
    <module>consumer_9000</module>
  </modules>

  <name>SpringCloudDemo</name>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <mybatisplus.version>3.3.1.tmp</mybatisplus.version>
    <mysql.connector.version>8.0.18</mysql.connector.version>
    <httpcore.version>4.4.13</httpcore.version>
    <lombok.version>1.18.12</lombok.version>
    <java.version>1.8</java.version>
  </properties>

  <!-- springboot -->
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.5.RELEASE</version>
  </parent>

  <dependencyManagement>
    <dependencies>
      <!-- mybatis-plus -->
      <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>${mybatisplus.version}</version>
      </dependency>

      <!-- mybatis-plus 代碼生成器相關依賴 -->
      <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-generator</artifactId>
        <version>${mybatisplus.version}</version>
      </dependency>
      <!-- 添加 mybatis-plus 模板引擎 依賴 -->
      <dependency>
        <groupId>org.apache.velocity</groupId>
        <artifactId>velocity-engine-core</artifactId>
        <version>2.2</version>
      </dependency>

      <!-- mysql-connector -->
      <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>${mysql.connector.version}</version>
      </dependency>

      <!-- 狀態碼參考地址:http://hc.apache.org/httpcomponents-core-ga/httpcore/apidocs/org/apache/http/HttpStatus.html -->
      <dependency>
        <groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpcore</artifactId>
        <version>${httpcore.version}</version>
      </dependency>

      <!-- lombok -->
      <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>${lombok.version}</version>
        <scope>provided</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>
</project>

【在子工程(producer_8000)中進行依賴引入:】
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.lyh.springcloud</groupId>
        <artifactId>SpringCloudDemo</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <artifactId>producer</artifactId>
    <name>producer</name>

    <dependencies>
        <!-- spring web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- mybatis-plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>

        <!-- mysql-connector -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!-- 狀態碼參考地址:http://hc.apache.org/httpcomponents-core-ga/httpcore/apidocs/org/apache/http/HttpStatus.html -->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpcore</artifactId>
        </dependency>

        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>

        <!-- test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- mybatis-plus 代碼生成器相關依賴 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
        </dependency>
        <!-- 添加 mybatis-plus 模板引擎 依賴 -->
        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity-engine-core</artifactId>
        </dependency>
    </dependencies>
</project>

 

 

 

Step4:撰寫 producer_8000 所需基本代碼,
  配置 MySQL 資料源,

【配置 MySQL 資料源:】
server:
  port: 8000

spring:
  application:
    name: producer
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123456
    url: jdbc:mysql://120.26.184.41:3306/producer?useUnicode=true&characterEncoding=utf8

 

 

 

撰寫相關 bean、mapper、service、controller 等代碼,
此處通過 mybatis-plus 代碼生成器生成相關代碼,也可以手動創建,
代碼生成器相關操作詳見:https://www.cnblogs.com/l-y-h/p/12859477.html#_label1_2

【TestAutoGenerate:】
package com.lyh.springcloud.generateCode;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import org.junit.jupiter.api.Test;

public class TestAutoGenerate {
    @Test
    public void autoGenerate() {
        // Step1:代碼生成器
        AutoGenerator mpg = new AutoGenerator();

        // Step2:全域配置
        GlobalConfig gc = new GlobalConfig();
        // 填寫代碼生成的目錄(需要修改)
        String projectPath = "E:\\myProject\\SpringCloudDemo\\producer_8000";
        // 拼接出代碼最終輸出的目錄
        gc.setOutputDir(projectPath + "/src/main/java");
        // 配置開發者資訊(可選)(需要修改)
        gc.setAuthor("lyh");
        // 配置是否打開目錄,false 為不打開(可選)
        gc.setOpen(false);
        // 物體屬性 Swagger2 注解,添加 Swagger 依賴,開啟 Swagger2 模式(可選)
        //gc.setSwagger2(true);
        // 重新生成檔案時是否覆寫,false 表示不覆寫(可選)
        gc.setFileOverride(false);
        // 配置主鍵生成策略,此處為 ASSIGN_ID(可選)
        gc.setIdType(IdType.ASSIGN_ID);
        // 配置日期型別,此處為 ONLY_DATE(可選)
        gc.setDateType(DateType.ONLY_DATE);
        // 默認生成的 service 會有 I 前綴
        gc.setServiceName("%sService");
        mpg.setGlobalConfig(gc);

        // Step3:資料源配置(需要修改)
        DataSourceConfig dsc = new DataSourceConfig();
        // 配置資料庫 url 地址
        dsc.setUrl("jdbc:mysql://120.26.184.41:3306/producer?useUnicode=true&characterEncoding=utf8");
        // dsc.setSchemaName("testMyBatisPlus"); // 可以直接在 url 中指定資料庫名
        // 配置資料庫驅動
        dsc.setDriverName("com.mysql.cj.jdbc.Driver");
        // 配置資料庫連接用戶名
        dsc.setUsername("root");
        // 配置資料庫連接密碼
        dsc.setPassword("123456");
        mpg.setDataSource(dsc);

        // Step:4:包配置
        PackageConfig pc = new PackageConfig();
        // 配置父包名(需要修改)
        pc.setParent("com.lyh.springcloud");
        // 配置模塊名(需要修改)
        pc.setModuleName("producer");
        // 配置 entity 包名
        pc.setEntity("entity");
        // 配置 mapper 包名
        pc.setMapper("mapper");
        // 配置 service 包名
        pc.setService("service");
        // 配置 controller 包名
        pc.setController("controller");
        mpg.setPackageInfo(pc);

        // Step5:策略配置(資料庫表配置)
        StrategyConfig strategy = new StrategyConfig();
        // 指定表名(可以同時操作多個表,使用 , 隔開)(需要修改)
        strategy.setInclude("producer_user");
        // 配置資料表與物體類名之間映射的策略
        strategy.setNaming(NamingStrategy.underline_to_camel);
        // 配置資料表的欄位與物體類的屬性名之間映射的策略
        strategy.setColumnNaming(NamingStrategy.underline_to_camel);
        // 配置 lombok 模式
        strategy.setEntityLombokModel(true);
        // 配置 rest 風格的控制器(@RestController)
        strategy.setRestControllerStyle(true);
        // 配置駝峰轉連字符
        strategy.setControllerMappingHyphenStyle(true);
        // 配置表前綴,生成物體時去除表前綴
        // 此處的表名為 test_mybatis_plus_user,模塊名為 test_mybatis_plus,去除前綴后剩下為 user,
        strategy.setTablePrefix(pc.getModuleName() + "_");
        mpg.setStrategy(strategy);

        // Step6:執行代碼生成操作
        mpg.execute();
    }
}

 

 

 

Step5:統一結果處理
  為了統一回傳的資料格式,自定義一個包裝類,用于包裝并回傳資料,
  詳見:https://www.cnblogs.com/l-y-h/p/13083375.html#_label1_1

【Result】
package com.lyh.springcloud.producer.common.tools;

import lombok.Data;
import org.apache.http.HttpStatus;

import java.util.HashMap;
import java.util.Map;

/**
 * 統一結果回傳類,方法采用鏈式呼叫的寫法(即回傳類本身 return this),
 * 構造器私有,不允許進行實體化,但提供靜態方法 ok、error 回傳一個實體,
 * 靜態方法說明:
 *      ok     回傳一個 成功操作 的結果(實體物件),
 *      error  回傳一個 失敗操作 的結果(實體物件),
 *
 * 普通方法說明:
 *      success      用于自定義回應是否成功
 *      code         用于自定義回應狀態碼
 *      message      用于自定義回應訊息
 *      data         用于自定義回應資料
 *
 * 依賴資訊說明:
 *      此處使用 @Data 注解,需匯入 lombok 相關依賴檔案,
 *      使用 HttpStatus 的常量表示 回應狀態碼,需匯入 httpcore 相關依賴檔案,
 */
@Data
public class Result {
    /**
     * 回應是否成功,true 為成功,false 為失敗
     */
    private Boolean success;

    /**
     * 回應狀態碼, 200 成功,500 系統例外
     */
    private Integer code;

    /**
     * 回應訊息
     */
    private String message;

    /**
     * 回應資料
     */
    private Map<String, Object> data = https://www.cnblogs.com/l-y-h/p/new HashMap<>();

    /**
     * 默認私有構造器
     */
    private Result(){}

    /**
     * 私有自定義構造器
     * @param success 回應是否成功
     * @param code 回應狀態碼
     * @param message 回應訊息
     */
    private Result(Boolean success, Integer code, String message){
        this.success = success;
        this.code = code;
        this.message = message;
    }

    /**
     * 回傳一個默認的 成功操作 的結果,默認回應狀態碼 200
     * @return 成功操作的實體物件
     */
    public static Result ok() {
        return new Result(true, HttpStatus.SC_OK, "success");
    }

    /**
     * 回傳一個自定義 成功操作 的結果
     * @param success 回應是否成功
     * @param code 回應狀態碼
     * @param message 回應訊息
     * @return 成功操作的實體物件
     */
    public static Result ok(Boolean success, Integer code, String message) {
        return new Result(success, code, message);
    }

    /**
     * 回傳一個默認的 失敗操作 的結果,默認回應狀態碼為 500
     * @return 失敗操作的實體物件
     */
    public static Result error() {
        return new Result(false, HttpStatus.SC_INTERNAL_SERVER_ERROR, "error");
    }

    /**
     * 回傳一個自定義 失敗操作 的結果
     * @param success 回應是否成功
     * @param code 回應狀態碼
     * @param message 相應訊息
     * @return 失敗操作的實體物件
     */
    public static Result error(Boolean success, Integer code, String message) {
        return new Result(success, code, message);
    }

    /**
     * 自定義回應是否成功
     * @param success 回應是否成功
     * @return 當前實體物件
     */
    public Result success(Boolean success) {
        this.setSuccess(success);
        return this;
    }

    /**
     * 自定義回應狀態碼
     * @param code 回應狀態碼
     * @return 當前實體物件
     */
    public Result code(Integer code) {
        this.setCode(code);
        return this;
    }

    /**
     * 自定義回應訊息
     * @param message 回應訊息
     * @return 當前實體物件
     */
    public Result message(String message) {
        this.setMessage(message);
        return this;
    }

    /**
     * 自定義回應資料,一次設定一個 map 集合
     * @param map 回應資料
     * @return 當前實體物件
     */
    public Result data(Map<String, Object> map) {
        this.data.putAll(map);
        return this;
    }

    /**
     * 通用設定回應資料,一次設定一個 key - value 鍵值對
     * @param key 鍵
     * @param value 資料
     * @return 當前實體物件
     */
    public Result data(String key, Object value) {
        this.data.put(key, value);
        return this;
    }
}

 

 

 

Step6:撰寫兩個介面,也即 生產者 對外提供的功能,
  此處定義一個 查詢介面(根據 id 回傳資料),一個添加介面(向資料庫中添加資料),
  使用代碼生成器生成的 UserService 中實作了 IService 介面,其內部定義了許多方法,此處可以直接使用,而不用 通過 xml 撰寫 SQL 陳述句,
注:
  想要使用 MyBatisPlus,不要忘記使用 @Mapper 或者 @MapperScan 指定 mapper 的位置,

【controller:】
package com.lyh.springcloud.producer.controller;


import com.lyh.springcloud.producer.common.tools.Result;
import com.lyh.springcloud.producer.entity.User;
import com.lyh.springcloud.producer.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/producer/user")
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping("/get/{id}")
    public Result getUser(@PathVariable Integer id) {
        User user = userService.getById(id);
        if (user == null) {
            return Result.error(false, 404, "data not found");
        }
        return Result.ok(true, 200, "query data success").data("user", user);
    }

    @PostMapping("/create")
    public Result createUser(@RequestBody User user) {
        boolean result = userService.save(user);
        if (!result) {
            return Result.error(false, 404, "create data error");
        }
        return Result.ok(true, 200, "create data success");
    }
}

 

 

 

此處暫時使用 postman 測驗一下兩個介面的功能,也可以 整合 Swagger 進行測驗,
SpringBoot 整合 Swagger 可以參考:https://www.cnblogs.com/l-y-h/p/13083375.html#_label2_0

 

 

 

通過上面操作,producer 已經能基本調通了(細節并沒有過多處理),能夠對外提供服務了,
接下來就是對 consumer 進行操作了(創建流程與 producer 類似),

 

Step7:創建 SpringBoot 子模塊(consumer_9000),
  修改子模塊組態檔(埠號為 9000、服務名為 consumer),
  修改子模塊 pom.xml 中 <parent> 標簽,指向父工程,
  修改父工程 pom.xml 中 <module> 標簽,指向子模塊,
注意:
  consumer 也屬于 web 工程,所以得添加 web 相關依賴,

 

 

 

Step8:撰寫 consumer 基本代碼,
  由于 consumer 只用于訪問 producer 的服務,所以只需撰寫 controller 代碼即可,
  此處通過 RestTemplate 進行遠程呼叫(見下一小節),

 

3、使用 RestTemplate 進行遠程呼叫

(1)什么是 RestTemplate?

【RestTemplate:】
    RestTemplate 是 Spring 提供的用于訪問 Rest 服務的客戶端模板工具集,提供一種簡單、便捷的模板類 來訪問 restful 服務,

簡單的理解:
    RestTemplate 提供了多種 簡單便捷的 訪問遠程 Http 服務的方法, 
注:
    需要引入 Spring-web 依賴, 
    
【檔案地址:(Spring 5.2.8)】
    https://docs.spring.io/spring-framework/docs/5.2.8.RELEASE/javadoc-api/org/springframework/web/client/RestTemplate.html

 

(2)RestTemplate 常用方法

【發送 POST 請求:】
    postForObject(URI url, Object args, Class<T> class)
注:
    url 指的是 遠程呼叫 地址,即 需要訪問的介面的 請求地址,
    args 指的是 請求引數,
    class 指的是 HTTP 回應結果 被轉換的 物件型別(即 對回傳結果進行 包裝),
    
【發送 GET 請求:】
    getForObject(String url, Class<T> class)
注:
    引數同上, 

 

(3)使用 RestTemplate?
  Step1:先得宣告一下 RestTemplate(在配置類中通過 @Bean 創建并交給 Spring 容器管理)

【ApplicationConfig】
package com.lyh.springcloud.consumer.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class ApplicationConfig {

    @Bean
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}

 

 

 

Step2:撰寫相關代碼,
  由于 consumer 呼叫 Producer 服務,且為了 回傳結果的統一,所以在 consumer 中還需要引入 Result 以及 User 兩個類 以及 這兩個類所需的依賴,

【ConsumerController】
package com.lyh.springcloud.consumer.controller;

import com.lyh.springcloud.consumer.entity.User;
import com.lyh.springcloud.consumer.tools.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;

@RestController
@RequestMapping("/consumer/user")
public class ConsumerController {

    // 注意,此處 url 寫死的,僅用于演示,實際專案中不能這么干,
    public static final String PRODUCER_URL = "http://localhost:8000/producer/";

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/get/{id}")
    public Result getUser(@PathVariable Integer id) {
        return restTemplate.getForObject(PRODUCER_URL + "/user/get/" + id, Result.class);
    }

    @PostMapping("/create")
    public Result createUser(@RequestBody User user) {
        return restTemplate.postForObject(PRODUCER_URL + "/user/create", user, Result.class);
    }
}

 

 

 

Step3:使用 postman 測驗一下,

producer 埠號為 8000,consumer 埠號為 9000,

【訪問流程舉例:】
    通過 POST 請求 訪問地址 http://localhost:9000/consumer/user/create,
    經過 consumer 內部轉換,會通過 RestTemplate 訪問 http://localhost:8000/producer/user/create ,

 

 

 

4、熱部署、IDEA 開啟 Run Dashboard 視窗(提高開發效率)

(1)熱部署
  詳見:https://www.cnblogs.com/l-y-h/p/13083375.html#_label2_3

(2)IDEA 開啟 Run Dashboard 視窗
  一般專案啟動后,可以在 run 視窗中看到 專案情況,但專案啟動的越多,關閉、停止等控制就很麻煩,可以通過開啟 Run Dashboard 視窗,簡化專案 Run、Debug 等操作,

 

 

 

在專案目錄下找到 .idea 檔案夾,打開 workspace.xml 檔案,并添加如下配置,然后啟動專案即可,

<component name="RunDashboard">
  <option name="configurationTypes">
    <set>
      <option value="https://www.cnblogs.com/l-y-h/p/SpringBootApplicationConfigurationType" />
    </set>
  </option>
</component>

 

 

 

5、專案優化(提取能復用的公共代碼)

(1)相關代碼
  上面演示的相關代碼可以在 GitHub 中獲取到,

【git 地址:】
    https://github.com/lyh-man/SpringCloudDemo.git

 

(2)抽取公共代碼,使其變成一個公共模塊
  通過上面兩個模塊的撰寫,可以發現 會出現相同的 代碼,比如:Result、User,若模塊過多時,每個模塊都寫一遍這些代碼,則代碼冗余,若代碼需要修改時,還得一個一個模塊進行修改,增加了不必要的作業量,
  可以將這些相同代碼抽取出來,形成一個 公共模塊,此時只需要引入這個公共模塊即可,修改代碼時,只需要針對 公共模塊 進行修改、添加即可,

 

Step1:創建一個子模塊 common,

 

 

 

Step2:抽取 其他子模塊 公共代碼 放入 公共模塊 中(比如:Result),相關依賴也需要引入,

 

 

 

Step3:剔除 其他子模塊 中的公共代碼,并在 pom.xml 檔案中引入 公共模塊,
  若啟動報錯,先 mvn install 執行一下 common 模塊,

 

 

 

  通過上面一系列步驟,已經簡單的搭建了一個專案,現在就是考慮 微服務 的問題了,逐步引入 微服務 的各種技術 來解決問題,

 

二、引入服務注冊 與 發現

1、問題 與 解決

【問題:】
    首先要解決的就是 服務 注冊 與 發現的問題,
    現在專案中存在 兩個模塊 consumer_9000 與 producer_8000,實際作業環境中,這兩個模塊 一般都是以 集群 方式進行部署,
比如:
    consumer_9000、consumer_9001、consumer_9002 構成集群來提供 消費者服務,
    producer_8000、producer_8001、producer_8002 構成集群來提供 生產者服務,
    
    那么如何去管理這些服務?集群的各服務之間通信怎么處理?這些服務掛掉了怎么辦?哪些服務可用 與 不可用? ... 一系列問題
    
    由于 服務與服務 之間依賴關系復雜、管理難度大,所以提出 服務注冊與發現 的概念,

【常見服務注冊與發現的 技術實作:】
    Eureka     停止維護了,不推薦使用,
    ZooKeeper
    Consul
    Nacos      阿里開源的產品,功能還是很強悍的,推薦使用

【服務注冊與發現:】
    服務注冊與發現,顧名思義,就是 服務如何注冊,以及 服務如何發現,
服務注冊:
    存在一個注冊中心,當服務啟動時,會將當前服務 的元資料資訊 以別名的方式 注冊到 注冊中心中(比如:主機號、埠號等),使用心跳檢測的方式判斷當前服務是否可用、剔除,
服務發現:
    獲取服務時,會向注冊中心查詢服務別名,用來獲取真實服務資訊(主機、埠號),再通過 遠程呼叫 的方式訪問真實服務,

 

2、CAP 原則、BASE 理論

(1)什么是 CAP?

【CAP:】
    CAP 原則指的是一個分布式系統中,無法同時滿足 C、A、P 三點,最多只能滿足兩點(AP、CP、AC),

【C(Consistency 一致性)】
    指的是 資料的一致性,即執行某個操作后,保證所有節點上的資料 同步更新,
比如:
    分布式系統中,某個服務執行了更新資料的操作后,那么所有取得該資料的用戶 應該獲取的是 最新的值,即所有節點訪問 同一份最新的資料副本,

【A(Availability 可用性)】
    指的是 服務的高可用性,即一個操作能在一定的時間內回傳結果(不管結果是成功還是失敗),
比如:
    分布式系統中,某個服務掛掉了(宕機),系統整體 應保證 還能正常運行、回應請求(不會整體崩潰),

【P(Partition tolerance 磁區容錯性)】
    指的是 網路磁區 情況下,仍能正常對外提供服務,
比如:
    分布式系統中,各個節點組成的網路應該是連通的,
    若因 軟體、硬體 故障導致 某些節點之間不連通了,即 網路分為幾個區域(網路磁區),
    此時節點服務沒有掛掉(宕機),但是不能正常通信,系統整體 應保證 還能正常運行、回應請求,
    
磁區容錯:
    網路磁區出現時,資料分布在 這些不連通的區域中,即 節點之間不能相互通信、資料不能同步,
    而容錯解決的問題 就是 即使兩個節點不能通信,仍要對外提供服務,不能因為磁區而使整個系統癱瘓,

 

(2)CAP 選擇

【CAP 選擇:】
    在分布式系統中,磁區是不可避免的,
    提高磁區容錯性的方式一般為 服務部署在多個節點上(即 資料放置在多個節點上),當一個節點斷開后,可以從其他節點獲取到資料,保證系統正常運行,
    
    但是一個服務存在多個節點后,多個節點之間的資料 為了保證資料一致,就會帶來 資料一致性問題(C),
    
    要保證資料一致性,則 每次操作資料后 均得等待 所有資料同步一致后 才能正常回傳結果,
    而在 資料同步的程序中,節點之間可能出現 網路阻塞、故障等 導致回應超時(服務呼叫失敗),這又帶來了 可用性問題(A),

    若要保證 可用性,即 不管資料是否同步成功,直接回傳結果,那就有可能導致 多個節點之間資料不一致,即 資料一致性問題,
    
    當然若一定要保證 資料一致性,可以不做磁區(每個服務都是單節點),此時也不用擔心資料同步問題(可用性也解決了),但服務一旦掛了,系統就崩潰了(容錯性低),不適用于 高可用的分布式系統,

綜上所述:
    分布式系統中,服務部署節點越多,磁區容錯性越高,但資料同步操作也就更復雜、耗時(一致性難保證),
    若想保證一致性,就需要犧牲可用性,
    若想保證可用性,就需要犧牲一致性(只是犧牲強一致性,資料最侄訓是一致的),
  
【CAP 組合方式:】
    CAP 組合方式有 AP、CP、CA,
    CA 不適用于 分布式系統,
    AP 常見組件:Eureka,
    CP 常見組件:Zookeeper、Consul,
    Nacos 可以實作 AP 與 CP 的切換,

 

(3)BASE 理論

【BASE 理論:】
    BASE 理論基于 CAP 演變而來,權衡 A 與 C 對系統的影響(理解為對 AP 的補充),對系統要求降低,
    在無法做到 強一致性 的情況下,應該使系統基本可用、資料最終一致,
    BASE 是 BA、S、E 縮寫,

【BA(Basically Available 基本可用):】
    指的是  系統 發生不可預知的故障時,允許損失部分可用性,但是系統整體是可用的,
注:
    損失部分可用性(舉例:)
        時間上的損失:正常情況下,系統處理請求可能需要 0.5 秒,但由于系統故障,可能需要 3 秒才能處理完請求,保證請求能正常處理完成,
        非系統核心功能的損失:正常情況下,用戶可以訪問系統所有功能,但是訪問量突然變大時,可以減少非核心功能的使用 保證 核心功能的正常運行,

【S(Soft state 軟狀態):】
    指的是 允許系統中資料存在中間狀態(各節點間的資料不一致),但資料中間狀態不會影響到系統的整體可用性,
    即允許節點之間 資料同步 可以存在 延時的程序,
    
【E(Eventually consistent 最終一致性):】
    指的是 系統各節點經過一段時間 資料同步后,最終的資料都是一致的,
注:
    強一致性:某個節點執行寫操作后,則各個節點執行 讀操作 讀取的結果都是一致的、且是最新的資料,
    弱一致性:讀操作執行后,讀取的 不一定是 最新的資料,
    最終一致性:系統在一定時間內 肯定會 達到資料一致的狀態,

 

三、服務注冊與發現 -- Eureka

1、什么是 Eureka ?

  Eureka 是 NetFlix 公司開發的 實作服務注冊與發現的 技術框架,遵循 AP 原則,
  SpringCloud 已將其集成到 spring-cloud-netflix 中,實作 SpringCloud 的服務注冊與發現,
  官方已經停止維護 Eureka,雖然不推薦使用,但還是可以學習一下基本思想、以及使用,

【官方檔案:】
    https://github.com/Netflix/eureka/wiki

 

 

 

(2)Eureka Server、Eureka Client,
  Eureka 采用 C/S 架構設計,分為 Eureka Server、Eureka Client,

【Eureka Server:】
    Eureka Server 作為服務注冊的服務器(即 注冊中心),當服務啟動后,會在注冊中心注冊,
    也即通過 Eureka Server 中的服務注冊表 可以知道所有可用的 服務節點資訊,
注:
    Eureka Server 本身也是一個服務,默認會自動注冊進 注冊中心,
    若是單機版的 Eureka Server,一般取消自動注冊自身的邏輯(自己注冊自己,沒啥意義),

【Eureka Client:】
    Eureka Client 作為客戶端,簡化與 Eureka Server 的互動,擁有一個內置的、輪詢的負載均衡器(提供基本的負載均衡),
    Eureka Client 既可以作為 服務提供者,又可以是 服務的消費者,
    作為服務提供者時,服務啟動后,會在 Eureka Server 注冊中心進行注冊,
    作為服務消費者時,即 呼叫服務提供者提供的服務,會從注冊中心 獲取到 服務提供者的真實地址,將地址快取在本地,向 Eureka Server 發送心跳(默認周期 30s),
    如果 Eureka Server 在多個心跳周期內沒有接收到某個節點的心跳(默認 90s),Eureka Server 將會從注冊中心中移除 該服務節點,  

 

 

 

(3)Eureka 1.x 與 2.x 的依賴區別

【Eureka 1.x】
    Eureka Server 與 Eureka Client 參考的是同一個依賴,
如下:
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
    <version>1.4.7.RELEASE</version>
</dependency>
    
【Eureka 2.x】
    Eureka Server 與 Eureka Client 參考的是不同的依賴,
如下:
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    <version>2.2.6.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    <version>2.2.6.RELEASE</version>
</dependency>

 

(4)Eureka 常用配置引數

【前綴為 eureka.instance 的引數:】
hostname:
    即 eureka.instance.hostname
    配置當前實體的主機名,
    
appname:
    即 eureka.instance.appname
    設定服務端實體名稱,優先級高于 spring.application.name,
注:
    服務名 不要 使用下劃線 _ 作為連接符,可以使用 - 作為連接符,
    
instance-id:
    即 eureka.instance.instance-id
    設定當前實體 ID,
    
lease-expiration-duration-in-seconds:
    即 eureka.instance.lease-expiration-duration-in-seconds
    設定服務失效時間,默認 90 秒
    
lease-renewal-interval-in-seconds:
    即 eureka.instance.lease-renewal-interval-in-seconds
    設定心跳時間,默認 30 秒

ip-address:
    即 eureka.instance.ip-address
    設定當前實體 IP 地址,

prefer-ip-address:
    即 eureka.instance.prefer-ip-address
    默認為 false,設定為 true 時,則顯示在注冊中心的 是 IP 地址 而非 主機名,

【前綴為 eureka.server 的引數:】
enable-self-preservation:
    即 eureka.server.enable-self-preservation
    默認為 true,設定 false 表示關閉自我保護模式(Eureka Server 短時間內丟失客戶端時,自我保護模式 使 Server 不洗掉失去連接的客戶端)

eviction-interval-timer-in-ms:
    即 eureka.server.eviction-interval-timer-in-ms
    設定 Eureka Server 清理無效節點的時間間隔,單位:毫秒,默認為 60000 毫秒,

【前綴為 eureka.client 的引數:】
register-with-eureka:
    即 eureka.client.register-with-eureka
    默認為 true,設定 false 表示不向注冊中心注冊自己(Eureka Server 一般設定為 false),
    
fetch-registry:
    即 eureka.client.fetch-registry
    默認為 true,設定 false 表示不去注冊中心 獲取 注冊資訊(Eureka Server 一般設定為 false),
    
service-url.defaultZone:
    即 eureka.client.service-url.defaultZone
    設定 Eureka 服務器地址,型別為 HashMap,默認為:serviceUrl.put("defaultZone", "http://localhost:8761/eureka/");

 

 

 

2、Eureka 使用 -- 單機版

(1)基本說明

【基本說明:】
    Eureka 使用 分為 server 與 client,
    首先需要創建一個 Eureka Server 模塊(eureka_server_7000),作為 服務注冊中心,
    前面創建的兩個模塊 consumer_9000、producer_8000 可以作為 Eureka Client 模塊,
注:
    producer_8000 作為 服務提供者,向 Eureka Server 中注冊,
    consumer_9000 作為 服務消費者,從 Eureka Server 中發現服務,
    創建與 consumer_9000 一樣的 eureka_client_consumer_9001 作為服務消費者進行演示,
    創建與 producer_8000 一樣的 eureka_client_producer_8001 作為服務提供者進行演示, 
    
    單機版沒使用價值,主要是為了由淺入深,為后面的集群版做鋪墊,
也即:
    單機版需要創建三個子工程,
        eureka_server_7000                作為服務注冊中心
        eureka_client_producer_8001       作為服務提供者(提供服務)
        eureka_client_consumer_9001       作為服務消費者(呼叫服務)
    
    eureka_client_producer_8001 與 eureka_client_consumer_9001 都會注冊進 eureka_server_7000,
    eureka_client_consumer_9001 通過 eureka_client_producer_8001 配置的服務名,在 eureka_server_7000 注冊中心中找到 eureka_client_producer_8001 真實地址,
    然后再通過 RestTemplate 遠程呼叫該地址,從而完成 服務之間的互動,

 

 

 

(2)創建一個 Eureka Server 子模塊(eureka_server_7000)
  創建一個 Eureka Server 子模塊 eureka_server_7000,作為服務注冊中心,
Step1:引入 Eureka Servers 依賴
  在父工程中管理 springcloud 版本,
  在子模塊中引入 eureka-server 依賴,

【父工程管理 springcloud 版本:】
<properties>
  <springcloud.version>Hoxton.SR9</springcloud.version>
</properties>

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-dependencies</artifactId>
      <version>${springcloud.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

【子工程引入 eureka-server 依賴】
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

 

 

 

Step2:配置 Eureka Server
  撰寫 Eureka Server 組態檔,

【application.yml】
server:
  port: 7000

eureka:
  instance:
    hostname: localhost
    appname: Eureka-Server # 設定服務端實體名稱,優先級高于 spring.application.name
    instance-id: eureka-server-instance1 # 設定實體 ID
  client:
    register-with-eureka: false # 默認為 true,設定 false 表示不向注冊中心注冊自己
    fetch-registry: false # 默認為 true,設定 false 表示不去注冊中心 獲取 注冊資訊
    # 設定 Eureka 服務器地址,型別為 HashMap,默認為:serviceUrl.put("defaultZone", "http://localhost:8761/eureka/");
    service-url:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka

 

 

 

Step3:啟動 Eureka Server 服務
  在啟動類上,添加 @EnableEurekaServer 注解,用于開啟 EurekaServer,

 

 

 

 

 

 

(3)創建子工程 eureka_client_producer_8001
  創建一個與 producer_8000 相同的子工程 eureka_client_producer_8001 ,
  作為 Eureka Client,并注冊到 注冊中心中,

Step1:創建子工程 eureka_client_producer_8001,并引入 eureka-client 依賴,
  與 producer_8000 流程相同,直接 copy 然后修改亦可(此處不再重復截圖),

【eureka_client_producer_8001 引入 eureka-client 依賴:】
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

 

 

 

Step2:配置 Eureka Client,

【application.yml】
server:
  port: 8001

spring:
  application:
    name: eureka-client-producer-8001
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123456
    url: jdbc:mysql://120.26.184.41:3306/producer?useUnicode=true&characterEncoding=utf8

eureka:
  instance:
    appname: eureka-client-producer-8001 # 優先級比 spring.application.name 高
    instance-id: ${eureka.instance.appname} # 設定當前實體 ID
  client:
    register-with-eureka: true # 默認為 true,注冊到 注冊中心
    fetch-registry: true # 默認為 true,從注冊中心 獲取 注冊資訊
    service-url:
      # 指向 注冊中心 地址,也即 eureka_server_7000 的地址,
      defaultZone: http://localhost:7000/eureka

 

 

 

Step3:啟動 Eureka Client 服務
  在啟動類上,添加 @EnableEurekaClient 注解,用于開啟 EurekaClient(不添加也能正常注冊到 注冊中心),

 

 

 

 

 

 

(4)創建子工程 eureka_client_consumer_9001
  創建一個與 consumer_9000 相同的子工程 eureka_client_consumer_9001,
  作為 Eureka Client,并注冊到 注冊中心中,

Step1:創建 eureka_client_consumer_9001
  與創建 eureka_client_producer_8001 類似,此處不重復截圖,
  可以直接 copy 一份 consumer_9000 代碼進行修改,
  引入 Eureka Client 依賴,
  配置 Eureka Client,然后在啟動類上添加 @EnableEurekaClient 注解,

【eureka_client_consumer_9001 引入 eureka-client 依賴:】
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

【application.yml:】
server:
  port: 9001
spring:
  application:
    name: eureka-client-consumer-9001

eureka:
  instance:
    appname: eureka-client-consumer-9001 # 優先級比 spring.application.name 高
    instance-id: ${eureka.instance.appname} # 設定當前實體 ID
  client:
    register-with-eureka: true # 默認為 true,注冊到 注冊中心
    fetch-registry: true # 默認為 true,從注冊中心 獲取 注冊資訊
    service-url:
      # 指向 注冊中心 地址,也即 eureka_server_7000 的地址,
      defaultZone: http://localhost:7000/eureka

 

 

 

Step2:更換 RestTemplate 訪問的 URL,
  配置了 Eureka 后,consumer 呼叫 producer 不能直接寫死了,應該在 Eureka Server 注冊中心通過 服務名 找到 真實對應的 地址后 再去 遠程訪問,
  此處需要更換 RestTemplate 的訪問地址(為 Eureka Client 注冊時的 服務名),
  在配置 RestTemplate 時需要添加上 @LoadBalanced 注解,

 

 

 

Step3:簡單測驗一下,

【訪問流程:】
    訪問 http://localhost:9001/consumer/user/get/2
    內部通過 EUREKA-CLIENT-PRODUCER-8001 服務名找到對應的 地址 localhost:8001,
    然后轉為遠程呼叫 http://localhost:8001/producer/user/get/2
即
    consumer 根據 服務注冊中心 找到 producer 的地址,
    然后通過 遠程呼叫 該地址,達到 訪問 producer 服務的目的,

 

 

 

 

 

 

3、Eureka 填坑(通過服務名訪問 服務遇到的坑)

(1)說明

【場景:】
    沒有配置 Eureka 時,consumer 通過 RestTemplate 呼叫 producer 服務,
    此時呼叫地址是寫死的,比如:http://localhost:8001/
    
    配置了 Eureka 后,consumer、producer 已經注冊到 Eureka Server 中,
    此時 consumer 應該從 Eureka 中通過 服務名 獲取到 producer 的真實地址,然后再通過 RestTemplate 去呼叫,
    此時呼叫地址寫的是 被呼叫的服務名,比如:http://EUREKA_CLIENT_PRODUCER_8001/
注:
    此處 替換地址后 遇到的三個坑(通過注冊中心 服務名 訪問真實服務遇到的坑),

 

(2)錯誤一:(未添加 @LoadBalanced 注解)

【錯誤資訊:】
    java.net.UnknownHostException: EUREKA_CLIENT_PRODUCER_8001
    
【解決:】
    使用 @Bean 配置 RestTemplate 時,同時添加上 @LoadBalanced 注解即可,

 

 

 

(3)錯誤二:(服務名使用了 下劃線 _ 作為連接符 )

【錯誤資訊:】
    java.lang.IllegalStateException: Request URI does not contain a valid hostname:http://EUREKA_CLIENT_PRODUCER_8001/
    
【解決:】
    配置服務名時,將下劃線 _ 改為 - 作為連接符,

 

 

 

(4)錯誤三:(決議主機號、域名失敗)

【錯誤資訊:】
    java.net.UnknownHostException: eureka.client.producer.8002

【解決:】
    打開 hosts 檔案,并配置域名映射(在后面構建集群版 Eureka 時可能遇到),

 

 

 

4、Eureka 偽集群版

(1)基本說明

【為什么使用集群:】
    遠程服務呼叫 最重要的一個問題 就是 高可用,如果只有一個服務,那么當服務掛掉了,整個系統將會崩潰,
    所以需要部署多個服務(集群),除非所有服務都掛掉了,整個系統才會崩潰,
    同樣的,注冊中心也需要部署多個(集群),
    采用集群方式部署、并實作負載均衡以及故障容錯 從而提高 可用性,
    
【集群搭建基本說明:】
    前面單機版創建了 eureka_server_7000、eureka_client_producer_8001、eureka_client_consumer_9001 三個工程,
    此處為了區分,并演示集群的操作,
        創建與 eureka_server_7000 一樣的 eureka_server_7001、eureka_server_7002、eureka_server_7003 作為 注冊中心 集群,
        創建與 eureka_client_producer_8001 一樣的 eureka_client_producer_8002、eureka_client_producer_8003、eureka_client_producer_8004 作為 服務提供者 集群,
        創建與 eureka_client_consumer_9001  一樣的 eureka_client_consumer_9002 作為 服務消費者(可以不做集群),
注:
    創建流程基本一致,但是組態檔有些許差別,

 

 

 

(2)創建 Eureka Server 集群,
Step1:創建與 eureka_server_7000 相同的 eureka_server_7001,
  修改 pom.xml 引入 eureka-server 依賴,
  配置 eureka-server,
  在啟動類上添加 @EnableEurekaServer 注解,

【引入依賴:】
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

【配置 Eureka Server:】
server:
  port: 7001

eureka:
  instance:
    hostname: eureka.server.7001.com # 定義主機名
    appname: Eureka-Server # 設定服務端實體名稱,優先級高于 spring.application.name
    instance-id: eureka-server-instance2 # 設定實體 ID
  client:
    register-with-eureka: false # 默認為 true,設定 false 表示不向注冊中心注冊自己
    fetch-registry: false # 默認為 true,設定 false 表示不去注冊中心 獲取 注冊資訊
    # 指向集群中 其他的 注冊中心
    service-url:
      defaultZone: http://eureka.server.7002.com:7002/eureka,http://eureka.server.7003.com:7003/eureka

 

 

 

Step2:同理創建 eureka_server_7002、eureka_server_7003

【eureka_server_7002 的 application.yml:】
server:
  port: 7002

eureka:
  instance:
    hostname: eureka.server.7002.com # 定義主機名
    appname: Eureka-Server # 設定服務端實體名稱,優先級高于 spring.application.name
    instance-id: eureka-server-instance3 # 設定實體 ID
  client:
    register-with-eureka: false # 默認為 true,設定 false 表示不向注冊中心注冊自己
    fetch-registry: false # 默認為 true,設定 false 表示不去注冊中心 獲取 注冊資訊
    # 指向集群中 其他的 注冊中心
    service-url:
      defaultZone: http://eureka.server.7001.com:7001/eureka,http://eureka.server.7003.com:7003/eureka

【eureka_server_7003 的 application.yml:】
server:
  port: 7003

eureka:
  instance:
    hostname: eureka.server.7003.com # 定義主機名
    appname: Eureka-Server # 設定服務端實體名稱,優先級高于 spring.application.name
    instance-id: eureka-server-instance4 # 設定實體 ID
  client:
    register-with-eureka: false # 默認為 true,設定 false 表示不向注冊中心注冊自己
    fetch-registry: false # 默認為 true,設定 false 表示不去注冊中心 獲取 注冊資訊
    # 指向集群中 其他的 注冊中心
    service-url:
      defaultZone: http://eureka.server.7001.com:7001/eureka,http://eureka.server.7002.com:7002/eureka

 

 

 

Step3:修改 hosts 檔案,進行域名映射,
  若服務啟動后,各個服務無法正常顯示在 Eureka 頁面中,可以配置域名映射試試,
  若未配置映射,則 Eureka Client 注冊時可能會出現問題,

【hosts 檔案位置:】
    windows 的 hosts 檔案位置:C:\Windows\System32\drivers\etc\hosts
    linux 的 hosts 檔案位置:/etc/hosts
    
【添加埠映射:】
127.0.0.1       eureka.server.7001.com
127.0.0.1       eureka.server.7002.com
127.0.0.1       eureka.server.7003.com

 

 

 

 

 

 

(3)創建 eureka_client_producer 集群,
Step1:創建與 eureka_client_producer_8001 相同的 eureka_client_producer_8002,
  引入 eureka_client 依賴,
  修改 application.yml 組態檔,
  在啟動類上添加 @EnableEurekaClient 注解,

【引入依賴:】
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

【application.yml】
server:
  port: 8002

spring:
  application:
    name: eureka-client-producer
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123456
    url: jdbc:mysql://120.26.184.41:3306/producer?useUnicode=true&characterEncoding=utf8

eureka:
  instance:
    appname: eureka-client-producer # 優先級比 spring.application.name 高
    instance-id: eureka-client-producer.instance1 # 設定當前實體 ID
    hostname: eureka.client.producer.8002 # 設定主機名
  client:
    register-with-eureka: true # 默認為 true,注冊到 注冊中心
    fetch-registry: true # 默認為 true,從注冊中心 獲取 注冊資訊
    service-url:
      # 指向 注冊中心 地址,注冊到 集群所有的 注冊中心,
      defaultZone: http://eureka.server.7001.com:7001/eureka,http://eureka.server.7002.com:7002/eureka,http://eureka.server.7003.com:7003/eureka 

 

 

 

Step2:同理創建 eureka_client_producer_8003、eureka_client_producer_8004

【eureka_client_producer_8003 的 application.yml:】
server:
  port: 8003

spring:
  application:
    name: eureka-client-producer
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123456
    url: jdbc:mysql://120.26.184.41:3306/producer?useUnicode=true&characterEncoding=utf8

eureka:
  instance:
    appname: eureka-client-producer # 優先級比 spring.application.name 高
    instance-id: eureka-client-producer.instance2 # 設定當前實體 ID
    hostname: eureka.client.producer.8003 # 設定主機名
  client:
    register-with-eureka: true # 默認為 true,注冊到 注冊中心
    fetch-registry: true # 默認為 true,從注冊中心 獲取 注冊資訊
    service-url:
      # 指向 注冊中心 地址,注冊到 集群所有的 注冊中心,
      defaultZone: http://eureka.server.7001.com:7001/eureka,http://eureka.server.7002.com:7002/eureka,http://eureka.server.7003.com:7003/eureka
      
【eureka_client_producer_8004 的 application.yml:】
server:
  port: 8004

spring:
  application:
    name: eureka-client-producer
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123456
    url: jdbc:mysql://120.26.184.41:3306/producer?useUnicode=true&characterEncoding=utf8

eureka:
  instance:
    appname: eureka-client-producer # 優先級比 spring.application.name 高
    instance-id: eureka-client-producer.instance3 # 設定當前實體 ID
    hostname: eureka.client.producer.8004 # 設定主機名
  client:
    register-with-eureka: true # 默認為 true,注冊到 注冊中心
    fetch-registry: true # 默認為 true,從注冊中心 獲取 注冊資訊
    service-url:
      # 指向 注冊中心 地址,注冊到 集群所有的 注冊中心,
      defaultZone: http://eureka.server.7001.com:7001/eureka,http://eureka.server.7002.com:7002/eureka,http://eureka.server.7003.com:7003/eureka

 

 

 

Step3:為了防止服務訪問失敗,修改 hosts 檔案,添加域名映射,

【域名映射:】
127.0.0.1       eureka.client.producer.8002
127.0.0.1       eureka.client.producer.8003
127.0.0.1       eureka.client.producer.8004

 

 

 

(4)創建 eureka_client_consumer_9002
Step1:創建與 eureka_client_consumer_9001 相同的 eureka_client_consumer_9002,
  引入 eureka_client 依賴,
  修改 application.yml 組態檔,
  在啟動類上添加 @EnableEurekaClient 注解,

【引入依賴:】
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

【application.yml】
server:
  port: 9002
spring:
  application:
    name: eureka-client-consumer

eureka:
  instance:
    appname: eureka-client-consumer # 優先級比 spring.application.name 高
    instance-id: eureka-client-consumer-instance1  # 設定當前實體 ID
    hostname: eureka.client.consumer.9002 # 設定主機名
  client:
    register-with-eureka: true # 默認為 true,注冊到 注冊中心
    fetch-registry: true # 默認為 true,從注冊中心 獲取 注冊資訊
    service-url:
      # 指向 注冊中心 地址,注冊到 集群所有的 注冊中心,
      defaultZone: http://eureka.server.7001.com:7001/eureka,http://eureka.server.7002.com:7002/eureka,http://eureka.server.7003.com:7003/eureka

 

 

 

Step2:修改 RestTemplate 發送的 URL 地址,

 

 

 

Step3:
  為了區分究竟呼叫的是 哪一個 producer 服務,在 producer 服務介面回傳時,回傳埠號以及主機名,對 三個 producer 服務進行如下修改,

package com.lyh.springcloud.eureka_client_producer_8002.controller;


import com.lyh.springcloud.common.tools.Result;
import com.lyh.springcloud.eureka_client_producer_8002.entity.User;
import com.lyh.springcloud.eureka_client_producer_8002.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/producer/user")
public class UserController {
    @Autowired
    private UserService userService;

    @Value("${eureka.instance.hostname}")
    private String hostname;

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

    @GetMapping("/get/{id}")
    public Result getUser(@PathVariable Integer id) {
        User user = userService.getById(id);
        if (user == null) {
            return Result.error(false, 404, "data not found").data("ip", (hostname + ":" + port));
        }
        return Result.ok(true, 200, "query data success").data("user", user).data("ip", (hostname + ":" + port));
    }

    @PostMapping("/create")
    public Result createUser(@RequestBody User user) {
        boolean result = userService.save(user);
        if (!result) {
            return Result.error(false, 404, "create data error").data("ip", (hostname + ":" + port));
        }
        return Result.ok(true, 200, "create data success").data("ip", (hostname + ":" + port));
    }
}

 

 

 

(5)啟動專案 并訪問,

【訪問流程:】
    訪問 http://localhost:9002/consumer/user/get/2 時,
    根據服務名 EUREKA-CLIENT-PRODUCER 會得到三個 producer 服務,
    會根據負載均衡,輪詢三個服務中的某個進行遠程呼叫,
注:
    若訪問出錯為 java.net.UnknownHostException: eureka.client.producer.8002 時,
    可以修改 hosts 檔案,進行 域名映射,
    比如:
        127.0.0.1       eureka.client.producer.8002

 

 

 

 

 

 

 

 

 

5、配置 actuator、服務發現、自我保護機制

(1)配置 actuator
  用于監控 springboot 應用,比如:查看狀態、健康檢查等,

【引入 actuator 依賴:】
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>    

 

 

 

未配置 actuator 時,出現如下圖所示錯誤,

 

 

 

配置 actuator 后,再次訪問如下圖所示,

 

 

 

(2)服務發現
  對于注冊進 注冊中心 的服務,可以通過服務發現來獲取 服務串列的資訊,
  以 eureka_client_producer_8002 為例,在其中撰寫一個 介面,用于回傳 服務資訊,
  在啟動類上添加 @EnableDiscoveryClient 注解(不添加好像也可以獲取服務資訊),

【撰寫一個介面:】
package com.lyh.springcloud.eureka_client_producer_8002.controller;

import com.lyh.springcloud.common.tools.Result;
import com.lyh.springcloud.eureka_client_producer_8002.entity.User;
import com.lyh.springcloud.eureka_client_producer_8002.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/producer/user")
public class UserController {
    @Autowired
    private UserService userService;

    @Value("${eureka.instance.hostname}")
    private String hostname;

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

    @Autowired
    private DiscoveryClient discoveryClient;

    @GetMapping("/discovery")
    public Result discovery() {
        // 獲取服務名串列
        List<String> servicesList = discoveryClient.getServices();

        // 根據服務名 獲取 每個服務名下的 各個服務的資訊
        Map<String, List<ServiceInstance>> map = new HashMap<>();
        servicesList.stream().forEach(service -> {
            map.put(service, discoveryClient.getInstances(service));
        });

        return Result.ok(true, 200, "discovery services success").data("services", map);
    }

    @GetMapping("/get/{id}")
    public Result getUser(@PathVariable Integer id) {
        User user = userService.getById(id);
        if (user == null) {
            return Result.error(false, 404, "data not found").data("ip", (hostname + ":" + port));
        }
        return Result.ok(true, 200, "query data success").data("user", user).data("ip", (hostname + ":" + port));
    }

    @PostMapping("/create")
    public Result createUser(@RequestBody User user) {
        boolean result = userService.save(user);
        if (!result) {
            return Result.error(false, 404, "create data error").data("ip", (hostname + ":" + port));
        }
        return Result.ok(true, 200, "create data success").data("ip", (hostname + ":" + port));
    }
}

 

 

 

啟動服務 eureka_server_7001、eureka_client_producer_8002、eureka_client_producer_8003、eureka_client_consumer_9002,并通過 postman 測驗一下,

【測驗 url:】
    http://localhost:8002/producer/user/discovery
    
【測驗結果:】
{
    "success": true,
    "code": 200,
    "message": "create data success",
    "data": {
        "services": {
            "eureka-client-producer": [
                {
                    "scheme": "http",
                    "host": "eureka.client.producer.8003",
                    "port": 8003,
                    "metadata": {
                        "management.port": "8003"
                    },
                    "secure": false,
                    "uri": "http://eureka.client.producer.8003:8003",
                    "serviceId": "EUREKA-CLIENT-PRODUCER",
                    "instanceId": "eureka-client-producer.instance2",
                    "instanceInfo": {
                        "instanceId": "eureka-client-producer.instance2",
                        "app": "EUREKA-CLIENT-PRODUCER",
                        "appGroupName": null,
                        "ipAddr": "192.168.217.1",
                        "sid": "na",
                        "homePageUrl": "http://eureka.client.producer.8003:8003/",
                        "statusPageUrl": "http://eureka.client.producer.8003:8003/actuator/info",
                        "healthCheckUrl": "http://eureka.client.producer.8003:8003/actuator/health",
                        "secureHealthCheckUrl": null,
                        "vipAddress": "eureka-client-producer",
                        "secureVipAddress": "eureka-client-producer",
                        "countryId": 1,
                        "dataCenterInfo": {
                            "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo",
                            "name": "MyOwn"
                        },
                        "hostName": "eureka.client.producer.8003",
                        "status": "UP",
                        "overriddenStatus": "UNKNOWN",
                        "leaseInfo": {
                            "renewalIntervalInSecs": 30,
                            "durationInSecs": 90,
                            "registrationTimestamp": 1608171423023,
                            "lastRenewalTimestamp": 1608172078443,
                            "evictionTimestamp": 0,
                            "serviceUpTimestamp": 1608171423023
                        },
                        "isCoordinatingDiscoveryServer": false,
                        "metadata": {
                            "management.port": "8003"
                        },
                        "lastUpdatedTimestamp": 1608171423023,
                        "lastDirtyTimestamp": 1608171418248,
                        "actionType": "ADDED",
                        "asgName": null
                    }
                },
                {
                    "scheme": "http",
                    "host": "eureka.client.producer.8002",
                    "port": 8002,
                    "metadata": {
                        "management.port": "8002"
                    },
                    "secure": false,
                    "uri": "http://eureka.client.producer.8002:8002",
                    "serviceId": "EUREKA-CLIENT-PRODUCER",
                    "instanceId": "eureka-client-producer.instance1",
                    "instanceInfo": {
                        "instanceId": "eureka-client-producer.instance1",
                        "app": "EUREKA-CLIENT-PRODUCER",
                        "appGroupName": null,
                        "ipAddr": "192.168.217.1",
                        "sid": "na",
                        "homePageUrl": "http://eureka.client.producer.8002:8002/",
                        "statusPageUrl": "http://eureka.client.producer.8002:8002/actuator/info",
                        "healthCheckUrl": "http://eureka.client.producer.8002:8002/actuator/health",
                        "secureHealthCheckUrl": null,
                        "vipAddress": "eureka-client-producer",
                        "secureVipAddress": "eureka-client-producer",
                        "countryId": 1,
                        "dataCenterInfo": {
                            "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo",
                            "name": "MyOwn"
                        },
                        "hostName": "eureka.client.producer.8002",
                        "status": "UP",
                        "overriddenStatus": "UNKNOWN",
                        "leaseInfo": {
                            "renewalIntervalInSecs": 30,
                            "durationInSecs": 90,
                            "registrationTimestamp": 1608172121773,
                            "lastRenewalTimestamp": 1608172121773,
                            "evictionTimestamp": 0,
                            "serviceUpTimestamp": 1608170460627
                        },
                        "isCoordinatingDiscoveryServer": false,
                        "metadata": {
                            "management.port": "8002"
                        },
                        "lastUpdatedTimestamp": 1608172121773,
                        "lastDirtyTimestamp": 1608172117716,
                        "actionType": "ADDED",
                        "asgName": null
                    }
                }
            ],
            "eureka-client-consumer": [
                {
                    "scheme": "http",
                    "host": "eureka.client.consumer.9002",
                    "port": 9002,
                    "metadata": {
                        "management.port": "9002"
                    },
                    "secure": false,
                    "uri": "http://eureka.client.consumer.9002:9002",
                    "serviceId": "EUREKA-CLIENT-CONSUMER",
                    "instanceId": "eureka-client-consumer-instance1",
                    "instanceInfo": {
                        "instanceId": "eureka-client-consumer-instance1",
                        "app": "EUREKA-CLIENT-CONSUMER",
                        "appGroupName": null,
                        "ipAddr": "192.168.217.1",
                        "sid": "na",
                        "homePageUrl": "http://eureka.client.consumer.9002:9002/",
                        "statusPageUrl": "http://eureka.client.consumer.9002:9002/actuator/info",
                        "healthCheckUrl": "http://eureka.client.consumer.9002:9002/actuator/health",
                        "secureHealthCheckUrl": null,
                        "vipAddress": "eureka-client-consumer",
                        "secureVipAddress": "eureka-client-consumer",
                        "countryId": 1,
                        "dataCenterInfo": {
                            "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo",
                            "name": "MyOwn"
                        },
                        "hostName": "eureka.client.consumer.9002",
                        "status": "UP",
                        "overriddenStatus": "UNKNOWN",
                        "leaseInfo": {
                            "renewalIntervalInSecs": 30,
                            "durationInSecs": 90,
                            "registrationTimestamp": 1608171433871,
                            "lastRenewalTimestamp": 1608172060016,
                            "evictionTimestamp": 0,
                            "serviceUpTimestamp": 1608171433872
                        },
                        "isCoordinatingDiscoveryServer": false,
                        "metadata": {
                            "management.port": "9002"
                        },
                        "lastUpdatedTimestamp": 1608171433872,
                        "lastDirtyTimestamp": 1608171429809,
                        "actionType": "ADDED",
                        "asgName": null
                    }
                }
            ]
        }
    }
}

 

 

 

 

 

 

(3)自我保護機制

【自我保護機制:】
    自我保護機制主要用于 Eureka Client 與 Eureka Server 之間存在 網路磁區(中斷了連接)時 對服務注冊表資訊的保護,
    當自我保護機制開啟時,Eureka Server 不再洗掉 服務注冊表中的資料,即不會注銷、剔除 任何服務(即使 Eureka Client 宕機了),
注:
    Eureka Server 出現如下提示時,即表示進入了保護模式,
    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.
    
【為什么產生 自我保護機制:】
    自我保護機制屬于 CAP 原則里的 AP(即在 網路磁區時,保證服務的可用性),
    一般情況下,Eureka Server 在一定時間內沒有收到 某個服務的心跳(默認 30 秒發一次心跳),Eureka Server 將會注銷該實體(默認 90 秒收不到心跳就剔除),
    但是存在特殊情況:發生網路磁區故障(比如:延時、擁堵、卡頓)等情況時,服務 與 Eureka Server 之間無法正常通信,此時若直接 剔除服務,那就可能造成很大的影響(此時的服務 本身并沒有問題,注銷服務 是不合理的),
    
    為了解決上面的特殊情況,引入了 自我保護 的概念,當 Eureka Server 短時間內丟失過多服務時,將會開啟自我保護模式,    
    自我保護模式一旦開啟,將不會注銷任何服務實體(寧愿保留錯誤的服務資訊,也不洗掉正常的服務),
    
    而自我保護模式一開,客戶端訪問時就容易訪問到 已經不存在的服務資訊,將會出現服務呼叫失敗的情況,所以客戶端必須進行容錯處理(比如:請求重試、斷路器等),
    
【自我保護機制觸發條件:】
    經過一分鐘,Renews(last min) < Renews threshold * 0.85,就會觸發自我保護機制,
注:
    Renews(last min) 表示 Eureka 最后一分鐘接收的心跳數,
    Renews threshold 表示 Eureka 最后一分鐘應該接收的心跳數,

 

 

 

關閉自我保護模式:

【舉例:】
    以 eureka_server_7001、eureka_client_producer_8002 為例,
    eureka_server_7001 為 Eureka Server,
    eureka_client_producer_8002 為 Eureka Client,
    當 eureka_server_7001 在一定時間內沒有接收到 eureka_client_producer_8002 的心跳,將會從服務串列中 剔除 eureka_client_producer_8002  服務,
    
在 Eureka Server 端配置 關閉自我保護模式,
eureka:
  server:
    enable-self-preservation: false # 關閉自我保護模式
    eviction-interval-timer-in-ms: 2000 # 清理無效服務的間隔

在 Eureka Client 端配置 心跳發送時間間隔、以及超時等待時間,
eureka:
  instance:
    lease-renewal-interval-in-seconds: 1 # 客戶端向 注冊中心 發送心跳的時間間隔,默認 30 秒
    lease-expiration-duration-in-seconds: 5 # 注冊中心 等待心跳最長時間,超時剔除服務,默認 90 秒

【在 eureka_server_7001 中 配置關閉自我保護模式:】
server:
  port: 7001

eureka:
  server:
    enable-self-preservation: false # 關閉自我保護模式
    eviction-interval-timer-in-ms: 2000 # 清理無效服務的間隔
  instance:
    hostname: eureka.server.7001.com # 定義主機名
    appname: Eureka-Server # 設定服務端實體名稱,優先級高于 spring.application.name
    instance-id: eureka-server-instance2 # 設定實體 ID
  client:
    register-with-eureka: false # 默認為 true,設定 false 表示不向注冊中心注冊自己
    fetch-registry: false # 默認為 true,設定 false 表示不去注冊中心 獲取 注冊資訊
    # 指向集群中 其他的 注冊中心
    service-url:
      defaultZone: http://eureka.server.7002.com:7002/eureka,http://eureka.server.7003.com:7003/eureka
      
【在 eureka_client_producer_8002 中配置 心跳發送時間間隔、以及超時等待時間:】
server:
  port: 8002

spring:
  application:
    name: eureka-client-producer
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123456
    url: jdbc:mysql://120.26.184.41:3306/producer?useUnicode=true&characterEncoding=utf8

eureka:
  instance:
    appname: eureka-client-producer # 優先級比 spring.application.name 高
    instance-id: eureka-client-producer.instance1 # 設定當前實體 ID
    hostname: eureka.client.producer.8002 # 設定主機名
    lease-renewal-interval-in-seconds: 1 # 客戶端向 注冊中心 發送心跳的時間間隔,默認 30 秒
    lease-expiration-duration-in-seconds: 5 # 注冊中心 等待心跳最長時間,超時剔除服務,默認 90 秒
  client:
    register-with-eureka: true # 默認為 true,注冊到 注冊中心
    fetch-registry: true # 默認為 true,從注冊中心 獲取 注冊資訊
    service-url:
      # 指向 注冊中心 地址,注冊到 集群所有的 注冊中心,
      defaultZone: http://eureka.server.7001.com:7001/eureka,http://eureka.server.7002.com:7002/eureka,http://eureka.server.7003.com:7003/eureka

 

 

 

 

 

 

6、Eureka 保證 AP、基本作業流程

(1)Eureka 保證 AP
  前面搭建集群版 Eureka 時,存在多個 Eureka Server 節點,這些節點不分 主、從 節點(所有節點平等),節點之間 相互注冊,每個節點通過 service-url 指向其他的 Eureka Server,
  當一個 Eureka Server 節點宕機后,會自動切換到其他可用的 Eureka Server 節點,也就意味著 只要有一臺 Eureka Server 正常作業,那么系統就不會崩潰(提高了可用性),
  但是 Eureka Server 各節點間采用異步方式進行資料同步,不保證節點間資料強一致性,也即各個 Server 保存的服務串列資訊可能不一致,但是資料最終是一致的,也即 保證 AP,

(2)基本作業流程
  通過前面一系列操作,應該大致理解了 Eureka 作業流程,此處總結一下,

【基本作業流程:】
Step1:
    Eureka Server 啟動后,會等待 Eureka Client 注冊,并將其保存在 服務串列中,
    如果配置了 Eureka Server 集群,那么集群各節點之間會同步服務串列資訊,
    
Step2:
    Eureka Client 啟動后,會根據配置去 Eureka Server 注冊中心進行 服務注冊,
    
Step3:
    Eureka Client 默認每隔 30 秒向 Eureka Server 發送一次心跳請求,保持與注冊中心的連接(保證 Client 是正常的),
    
Step4:
    Eureka Server 默認 90 秒沒有收到 Eureka Client 心跳請求,則視其為 失效服務,會從 注冊中心將 Client 服務剔除,
    
Step5:
    單位時間內,若 Eureka Server 統計到大量 Eureka Client 心跳丟失,則認定出現了 網路例外,
    將會開啟 自動保護模式,此時不會剔除 Client 服務(即使服務宕機 也會將其保留),
    
Step6:
    Eureka Client 心跳恢復正常后,Eureka Server 將會自動退出 自動保護模式,
    
Step7:
    Eureka Client 默認 30 秒從注冊中心 獲取 服務串列資訊,并將其資訊快取在本地,
    
Step8:
    Eureka Client 進行服務呼叫時,先從本地快取查詢 服務,獲取不到時服務時 會去 注冊中心 獲取最新的 服務串列資訊 并保存在本地,
    
Step9:
    Eureka Client 獲取到 目標服務資訊后,通過 遠程呼叫、負載均衡(默認輪詢)的方式 發起服務呼叫,
    
Step10:
    Eureka Client 正常關閉時,會向 Eureka Server 發送取消請求,Eureka Server 將其從服務串列中剔除,

 

四、服務注冊與發現 -- Zookeeper

1、什么是 Zookeeper?

(1)什么是 Zookeeper ?

【Zookeeper:】
    Zookeeper 是一個開源、分布式的服務管理框架,屬于 Apache Hadoop 的一個子專案,
    為分布式應用提供協調服務的(比如:狀態同步、集群管理、分布式應用配置管理 等),

【官網地址:】
    https://zookeeper.apache.org/
    https://github.com/apache/zookeeper

 

(2)本質

【本質:】
    Zookeeper 可以看成一個基于 觀察者模式 設計的分布式服務管理框架,
    在服務器端可以 存盤、管理 資料,并接受 客戶端(觀察者)的注冊,一旦資料變化,將通知這些 觀察者 作出相應的動作,
    
    簡單的理解:Zookeeper 就是 檔案系統 加上 監聽通知機制 來作業,

【檔案系統:】
    Zookeeper 維護一個類似 Linux 檔案目錄 的資料結構,
    每個子目錄稱為一個 znode 節點(可以通過唯一路徑進行標識),znode 可用于存盤資料(不宜存放大資料,一般存盤上限 1 M),相同層級的 znode 不能重名,
    
【監聽通知機制:】
    客戶端注冊后,會監聽 znode 節點 是否變化,一旦節點變化(資料改變、節點洗掉、增加子節點 等),Zookeeper 將會通知客戶端,
    
【znode 型別:】
持久節點(persistent):
    客戶端與 zookeeper 斷開連接后,節點仍然存在,
    
持久有序節點(persistent sequential):
    客戶端與 Zookeeper 建立連接后,會給節點按照順序進行編號(比如:/znode 變為 /znode0000000001),
    客戶端與 zookeeper 斷開連接后,節點仍然存在,

臨時節點(ephemeral):
    客戶端與 zookeeper 斷開連接后,節點會被洗掉,
    
臨時有序節點(ephemeral sequential):
    客戶端與 Zookeeper 建立連接后,會給節點按照順序進行編號,
    客戶端與 zookeeper 斷開連接后,節點會被洗掉,
    
注:
    編號由父節點維護,是一個單調遞增的計數器,可用于全域事件的排序(便于推斷分布式系統中事件的執行先后順序),

 

(3)Zookeeper 功能舉例 -- 分布式應用配置管理

在實際作業中,一個服務經常以集群的方式進行部署,如果此時需要修改服務的配置,
若不對配置進行管理,那么將需要 逐個服務 進行修改,非常麻煩、易出錯,
通過 Zookeeper 可以對配置進行管理,將配置放在 Zookeeper 某個目錄節點中,然后讓這些服務 去監聽 該節點,
此時修改節點中的資料(配置資訊),那么每個服務都會監聽到資料的變化,獲取到最新的配置資訊,

 

 

 

(4)zookeeper 選舉機制
  選舉發生在 zookeeper 集群中,單機版不存在選舉,
注:
  此處僅簡單介紹一下,篇幅有限,后續再補充,詳情可自行查閱相關檔案,
  常見概念有 選舉機制、ZAB 協議、兩階段提交、寫操作流程等,

【zookeeper 集群特點:】
    Zookeeper 集群由一個 Leader 以及 多個 Follower 組成,
    集群中只要有半數以上的節點正常作業,那么集群將能正常提供服務,zookeeper 集群一般為奇數節點,
注:
    對于寫請求,請求會同時發送給其他 zookeeper 服務器,達成一致后,請求才會回傳成功,
    所以提高集群的機器數量,雖然提高了讀效率,但是降低了寫效率,

【選舉發生場合:】
    場合一:zookeeper 服務器集群初始化啟動時會選取 Leader,
    場合二:集群中 Leader 故障(宕機)時從剩余節點中選取新的 Leader,

【zxid、myid】
    myid 全域唯一的數字(一般 1-255),每個數字表示 zookeeper 服務器集群中的一個 服務器,
    zxid 指的是 zookeeper transaction id,zookeeper 每一次狀態改變(增加、洗掉節點,修改節點資料等),都將對應一個遞增的 transaction id,即 zxid,
注:
    zxid 越大,表示當前資料越新,
        
【選舉基本原則:】
    每個節點先投自己一票,
    然后與其他節點進行比較,如果有其他節點 A 被更多人選擇,那么跟隨大部隊(將票投給節點 A),
    若節點 A 被超過一半節點選擇,那么將結束選舉程序,并將節點 A 視為 Leader,

節點比較規則:
    兩個節點 zxid 進行比較,zxid 大的節點作為 leader,
    若 zxid 相同時,根據 myid 進行比較,myid 大的節點作為 leader, 

 

2、使用 Docker 安裝、使用 Zookeeper

(1)安裝
  此處使用 Docker-compose 進行鏡像下載 以及 啟動容器,
注:
  Docker 以及 Docker-compose 使用可參考:
  https://www.cnblogs.com/l-y-h/p/12622730.html
  https://www.cnblogs.com/l-y-h/p/12622730.html#_label8_2

【docker-compose.yml】
# 指定 compose 檔案版本,與 docker 兼容,高版本的 docker 一般使用 3.x,
version: '3.7'

services:
  # 設定服務名
  zookeeper_service1:
    # 配置所使用的鏡像
    image: zookeeper
    # 容器總是重啟
    restart: always
    # 容器名稱
    container_name: zookeeper_service1
    #與宿主機的埠映射
    ports:
      - 2181:2181
    #容器目錄映射
    volumes:
      - /usr/mydata/zookeeper/zookeeper_service1/data:/data
      - /usr/mydata/zookeeper/zookeeper_service1/datalog:/datalog

 

 

 

(2)常用命令
  可以直接進入 容器內部 進行相關操作,
  進入 zookeeper_service1 的 bin 目錄,并執行 zkCli.sh 命令,可以開啟客戶端,

【進入 zookeeper_service1 容器的命令:】
    docker exec -it zookeeper_service1 /bin/bash
    
【退出 zookeeper_service1 容器的命令:】
    exit

 

 

 

客戶端常用命令:
  不同版本的命令可能稍微不同,但大體還是一致的,

【查詢當前節點下的全部子節點:】
    ls 節點名稱
注:
    節點名稱 就是 節點路徑,比如:/、/zookeeper、/test/zookeeper 等
比如:
    ls /         查詢根目錄下全部子節點

【查詢當前節點下的資料:】
    get 節點名稱
比如:
    get /zookeeper     獲取 /zookeeper 節點的資料
    
【查詢節點資訊:】
    stat 節點名稱
比如:
    stat /zookeeper   獲取 /zookeeper 節點的資訊
    
【創建節點:】
    create [-s] [-e] 節點名稱 節點資料
注:
    -s 為 sequential,即當前節點型別為 有序節點,
    -e 為 ephemeral,即當前節點型別為 臨時節點,
    若不存在 -s 、-e 引數,則默認為 持久節點, 
比如:
    create -e /ephemeralZnode ephemeralData     創建一個臨時節點,名為 ephemeralZnode,
    create -s -e /ephemeralSequentialZnode ephemeralSequentialData     創建一個臨時有序的節點,系統會自動編號,比如: ephemeralSequentialZnode0000000003
    
【修改節點資料:】
    set 節點名稱 節點新資料
比如:
    set /ephemeralZnode newEphemeralData   修改 /ephemeralZnode 資料為 newEphemeralData   
    
【洗掉節點資料:】
    delete 節點名稱        
或者 
    deleteall 節點名稱
注:
    delete 洗掉的是沒有子節點的節點,
    deleteall 洗掉的是當前節點 以及 其全部子節點
    
【離開客戶端:】
    quit

 

 

 

 

 

 

 

 

 

3、SpringCloud 整合 Zookeeper 單機版

(1)說明

在之前學習 Eureka 時,SpringCloud 整合了 Eureka 服務端 以及 客戶端的實作,
而此處 Zookeeper 已經提供了服務端的實作,所以只需要使用 SpringCloud 整合 Zookeeper 客戶端即可,

與 Eureka 類似,客戶端也可分為 服務提供者、服務消費者,

創建與 eureka_client_producer_8001 類似的 zookeeper_client_producer_8005 作為 服務提供者,
創建與 eureka_client_consumer_9001 類似的 zookeeper_client_consumer_9003 作為 服務消費者,

 

 

 

(2)創建 zookeeper_client_producer_8005 子模塊,
  修改子模塊 與 父模塊 pom.xml 檔案(與前面創建模塊類似,此處省略),
  引入 zookeeper_discovery 依賴,
  修改 application.yml 組態檔,
  在啟動類上添加 @EnableDiscoveryClient 注解(不添加好像也可以正常注冊),

【pom.xml:】
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
</dependency>

【application.yml】
server:
  port: 8005

spring:
  application:
    name: zookeeper-client-producer
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123456
    url: jdbc:mysql://120.26.184.41:3306/producer?useUnicode=true&characterEncoding=utf8
  cloud:
    # zookeeper 配置
    zookeeper:  
      # 配置連接 zookeeper 服務器的地址
      connect-string: 120.26.184.41:2181

 

 

 

  啟動服務,在 zookeeper 服務器端,執行 zkCli.sh 進入客戶端,可以查看到 注冊節點 的資訊,
注:
  默認是臨時節點,服務一旦宕機,zookeeper 將會將該節點移除,

 

 

 

 

(3)同理創建 zookeeper_client_consumer_9003 模塊
  修改子模塊 與 父模塊 pom.xml 檔案(與前面創建模塊類似,此處省略),
  引入 zookeeper_discovery 依賴,
  修改 application.yml 組態檔,
  在啟動類上添加 @EnableDiscoveryClient 注解(不添加好像也可以正常注冊),
  引入 RestTemplate 時,需要添加 @LoadBalanced,并修改訪問地址為 服務名,

【依賴:】
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
</dependency>

【application.yml】
server:
  port: 9003
spring:
  application:
    name: zookeeper-client-consumer
  cloud:
    zookeeper:
      connect-string: 120.26.184.41:2181

 

 

 

 

 

 

啟動服務,可以在注冊中心看到已經注冊的服務,

 

 

 

 

 

 

4、Zookeeper 偽集群版(docker-compose 啟動)

(1)集群角色說明
  Zookeeper Server 集群不同于 Eureka,其節點之間存在主從之分,其一個 Server 斷開后,將會在剩余節點中 重新選舉出一個 Leader,

【Server 節點角色:】
    Leader       主節點
    Follower     從節點(參與選舉主節點)
    Observer     從節點(不參與選舉主節點)

進入 zookeeper 容器的 bin 目錄后,可以通過 zkServer.sh status 查看當前服務的角色,

 

 

 

(2)專案說明:

【專案說明:】
    與 Eureka 類似,需要對 Server 以及 producer 作出集群處理,
    此處使用 docker-compose 進行 Server 集群(偽集群)處理,
    創建 zookeeper_client_producer_8006、zookeeper_client_producer_8007、zookeeper_client_producer_8008 作為 producer 集群,
    創建 zookeeper_client_consumer_9004 作為服務消費者,進行服務呼叫,

 

(3) 通過 docker-compose 啟動 Zookeeper 集群

【docker-compose.yml】
# 指定 compose 檔案版本,與 docker 兼容,高版本的 docker 一般使用 3.x,
version: '3.7'

services:
  # 設定服務名
  zookeeper_service2:
    # 配置所使用的鏡像
    image: zookeeper
    # 容器總是重啟
    restart: always
    # 容器名稱
    container_name: zookeeper_service2
    #與宿主機的埠映射
    ports:
      - 2182:2181
    #容器目錄映射
    volumes:
      - /usr/mydata/zookeeper/zookeeper_service2/data:/data
      - /usr/mydata/zookeeper/zookeeper_service2/datalog:/datalog
    # 設定環境變數
    environment:
      # Server 唯一標識(1 - 255)
      ZOO_MY_ID: 2
      # 指定服務資訊,格式: server.A=B:C:D;E
      # 其中: A 表示服務器標識,B 是服務器 ip(服務名),C 是服務器與集群中 Leader 進行互動的埠,D 是用來選取新 Leader 進行互動的埠, E 為埠號
      ZOO_SERVERS: server.2=zookeeper_service2:2888:3888;2181 server.3=zookeeper_service3:2888:3888;2181 server.4=zookeeper_service4:2888:3888;2181

  # 設定服務名
  zookeeper_service3:
    # 配置所使用的鏡像
    image: zookeeper
    # 容器總是重啟
    restart: always
    # 容器名稱
    container_name: zookeeper_service3
    #與宿主機的埠映射
    ports:
      - 2183:2181
    #容器目錄映射
    volumes:
      - /usr/mydata/zookeeper/zookeeper_service3/data:/data
      - /usr/mydata/zookeeper/zookeeper_service3/datalog:/datalog
    # 設定環境變數
    environment:
      # Server 唯一標識(自然數)
      ZOO_MY_ID: 3
      # 指定服務資訊,格式: server.A=B:C:D;E
      # 其中: A 表示服務器標識,B 是服務器 ip(服務名),C 是服務器與集群中 Leader 進行互動的埠,D 是用來選取新 Leader 進行互動的埠, E 為埠號
      ZOO_SERVERS: server.2=zookeeper_service2:2888:3888;2181 server.3=zookeeper_service3:2888:3888;2181 server.4=zookeeper_service4:2888:3888;2181

  # 設定服務名
  zookeeper_service4:
    # 配置所使用的鏡像
    image: zookeeper
    # 容器總是重啟
    restart: always
    # 容器名稱
    container_name: zookeeper_service4
    #與宿主機的埠映射
    ports:
      - 2184:2181
    #容器目錄映射
    volumes:
      - /usr/mydata/zookeeper/zookeeper_service4/data:/data
      - /usr/mydata/zookeeper/zookeeper_service4/datalog:/datalog
    # 設定環境變數
    environment:
      # Server 唯一標識(自然數)
      ZOO_MY_ID: 4
      # 指定服務資訊,格式: server.A=B:C:D;E
      # 其中: A 表示服務器標識,B 是服務器 ip(服務名),C 是服務器與集群中 Leader 進行互動的埠,D 是用來選取新 Leader 進行互動的埠, E 為埠號
      ZOO_SERVERS: server.2=zookeeper_service2:2888:3888;2181 server.3=zookeeper_service3:2888:3888;2181 server.4=zookeeper_service4:2888:3888;2181

 

 

 

 

 

 

(4)創建 producer 集群,
  與單機版創建 zookeeper_client_producer_8005 同樣的流程創建 zookeeper_client_producer_8006、zookeeper_client_producer_8007、zookeeper_client_producer_8008,
  此處省略創建程序,
  唯一區別在于,配置注冊中心地址時,配置集群上所有的 server 地址(以逗號隔開),

【zookeeper_client_producer_8006 的 application.yml】
server:
  port: 8006
spring:
  application:
    name: zookeeper-client-producer
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123456
    url: jdbc:mysql://120.26.184.41:3306/producer?useUnicode=true&characterEncoding=utf8
  cloud:
    # zookeeper 配置
    zookeeper:
      # 配置連接 zookeeper 服務器的地址
      connect-string: 120.26.184.41:2181, 120.26.184.41:2182, 120.26.184.41:2183
      
【zookeeper_client_producer_8007 的 application.yml】
server:
  port: 8007
spring:
  application:
    name: zookeeper-client-producer
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123456
    url: jdbc:mysql://120.26.184.41:3306/producer?useUnicode=true&characterEncoding=utf8
  cloud:
    # zookeeper 配置
    zookeeper:
      # 配置連接 zookeeper 服務器的地址
      connect-string: 120.26.184.41:2181, 120.26.184.41:2182, 120.26.184.41:2183
      
【zookeeper_client_producer_8008 的 application.yml】
server:
  port: 8008
spring:
  application:
    name: zookeeper-client-producer
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123456
    url: jdbc:mysql://120.26.184.41:3306/producer?useUnicode=true&characterEncoding=utf8
  cloud:
    # zookeeper 配置
    zookeeper:
      # 配置連接 zookeeper 服務器的地址
      connect-string: 120.26.184.41:2181, 120.26.184.41:2182, 120.26.184.41:2183

啟動三個服務,可以在 服務器查看到 三個服務的 節點,

 

 

 

(5)創建 consumer
  與創建 zookeeper_client_consumer_9003 同樣流程創建 zookeeper_client_consumer_9004,
  此處創建流程省略,
  唯一區別在于,配置注冊中心地址時,配置集群上所有的 server 地址(以逗號隔開),

【application.yml】
server:
  port: 9004
spring:
  application:
    name: zookeeper-client-consumer
  cloud:
    zookeeper:
      connect-string: 120.26.184.41:2181, 120.26.184.41:2182, 120.26.184.41:2183

 

 

 

 

 

 

(6)實作 Discovery
  Discovery 代碼與 Eureka 代碼一致(同樣通過 DiscoveryClient 進行操作),

【ConsumerController:】
package com.lyh.springcloud.zookeeper_client_consumer_9004.controller;

import com.lyh.springcloud.common.tools.Result;
import com.lyh.springcloud.zookeeper_client_consumer_9004.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/consumer/user")
public class ConsumerController {

    // 注意,此處 url 寫死的,僅用于演示,實際專案中不能這么干,
//    public static final String PRODUCER_URL = "http://localhost:8001/producer/";
    // 通過服務名 找到  zookeeper 注冊中心真實訪問的 地址
    public static final String PRODUCER_URL = "http://zookeeper-client-producer";

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private DiscoveryClient discoveryClient;

    @GetMapping("/discovery")
    public Result discovery() {
        // 獲取服務名串列
        List<String> servicesList = discoveryClient.getServices();

        // 根據服務名 獲取 每個服務名下的 各個服務的資訊
        Map<String, List<ServiceInstance>> map = new HashMap<>();
        servicesList.stream().forEach(service -> {
            map.put(service, discoveryClient.getInstances(service));
        });

        return Result.ok(true, 200, "discovery services success").data("services", map);
    }

    @GetMapping("/get/{id}")
    public Result getUser(@PathVariable Integer id) {
        return restTemplate.getForObject(PRODUCER_URL + "/producer/user/get/" + id, Result.class);
    }

    @PostMapping("/create")
    public Result createUser(@RequestBody User user) {
        return restTemplate.postForObject(PRODUCER_URL + "/producer/user/create", user, Result.class);
    }
}

 

 

 

 

 

 

5、Zookeeper 真集群版(docker-compose 啟動)

(1)說明

【說明:】
    真集群版 與 偽集群版 基本操作還是一致的,除了 docker-compose.yml 有些許區別,
    此處僅搭建出 server 真集群環境(以及解決搭建環境遇到的坑),其余操作均與偽集群類似,不在重復講述,
    
【環境:】
    zookeeper 集群節點數量一般為 奇數,
    此處使用的是 阿里云兩臺服務器(財力有限,湊合一下),
云服務器:
    120.26.184.41
    182.92.120.184

 

(2)前提條件(開放埠)

【服務器 通過 埠 進行 資料互動:】
    由于 zookeeper server 部署在不同的服務器上,而服務器 選舉 以及 資料同步 需要進行互動,
    之前搭建偽集群時也提到了三個埠 2181、2888、3888,想要服務器之間正常互動,就需要開放這三個埠、并對其進行映射,
注:
    2181 是對客戶端提供服務的埠,
    2888 是服務器與集群中 Leader 進行資料互動的埠,
    3888 是用來選取新 Leader 進行互動的埠,
    
若需要開放埠,進入 阿里云 官網,找到相應的阿里云服務器,并添加 安全組 規則(在入方向中開放指定的埠),

 

 

 

(3)搭建環境遇到的坑 以及 解決

【問題一:】
    docker-compose up -d 啟動容器后,
    通過 docker-compose logs -f 查看日志發現,無法通過 3888 或者 2888 進行通信,

在 182.92.120.184 查看日志報錯資訊:
    Cannot open channel to 3 at election address /120.26.184.41:3888120.26.184.41 查看日志報錯資訊:
    Cannot open channel to 3 at election address /182.92.120.184:3888

注:
    極大的可能是因為 埠未開放 或者 埠未做映射,
    
【問題二:】
在 182.92.120.184 查看日志報錯資訊:
    /182.92.120.184:3888:QuorumCnxManager$Listener$ListenerHandler@1093] - Exception while listening
    java.net.BindException: Cannot assign requested address (Bind failed)

注:
    極大的可能是配置 自身服務器 ip 時,使用了公網 ip.

 

 

 

 

 

 

【如何解決(標準步驟、親測有效):】
對于某個服務器配置步驟如下:
Step1:
    進入阿里云官網,找到對應的服務器,并配置安全組規則,開放 2182、2888、3888 埠,

Step2:
    指定埠映射,將開放的埠與 2181、2888、3888 進行映射,
注:
    服務器埠開放后,如果不進行映射,埠效果等同于未開放,

Step3:
    指定服務器 ip 時(配置),
    對于本身服務器地址,可以使用 服務名 或者 0.0.0.0 進行替代,不要使用 公網 ip(會出錯),
    對于其他服務器,直接使用公網 ip 即可,

 

 

 

(4)在 120.26.184.41 服務器上通過 docker-compose.yml 啟動 zookeeper,
  此處使用 服務名(容器名)替代 自身 ip 地址,

【docker-compose.yml】
# 指定 compose 檔案版本,與 docker 兼容,高版本的 docker 一般使用 3.x,
version: '3.7'

services:
  # 設定服務名
  zookeeper_service3:
    # 配置所使用的鏡像
    image: zookeeper
    # 容器總是重啟
    restart: always
    # 容器名稱
    container_name: zookeeper_service3
    #與宿主機的埠映射
    ports:
      - 2182:2181
      - 2888:2888
      - 3888:3888
    #容器目錄映射
    volumes:
      - /usr/mydata/zookeeper/zookeeper_service3/data:/data
      - /usr/mydata/zookeeper/zookeeper_service3/datalog:/datalog
    # 設定環境變數
    environment:
      # Server 唯一標識(自然數)
      ZOO_MY_ID: 3
      # 指定服務資訊,格式: server.A=B:C:D;E
      # 其中: A 表示服務器標識,B 是服務器 ip(服務名),C 是服務器與集群中 Leader 進行互動的埠,D 是用來選取新 Leader 進行互動的埠, E 為埠號
      ZOO_SERVERS: server.2=182.92.120.184:2888:3888;2182 server.3=zookeeper_service3:2888:3888;2182
      #ZOO_SERVERS: server.2=182.92.120.184:2888:3888;2182 server.3=0.0.0.0:2888:3888;2182

 

 

 

(5)在 182.92.120.184 服務器上通過 docker-compose.yml 啟動 zookeeper,
  此處使用 0.0.0.0 替代 自身 ip 地址,

【docker-compose.yml】
# 指定 compose 檔案版本,與 docker 兼容,高版本的 docker 一般使用 3.x,
version: '3.7'

services:
  # 設定服務名
  zookeeper_service2:
    # 配置所使用的鏡像
    image: zookeeper
    # 容器總是重啟
    restart: always
    # 容器名稱
    container_name: zookeeper_service2
    #與宿主機的埠映射
    ports:
      - 2182:2181
      - 2888:2888
      - 3888:3888
    #容器目錄映射
    volumes:
      - /usr/mydata/zookeeper/zookeeper_service2/data:/data
      - /usr/mydata/zookeeper/zookeeper_service2/datalog:/datalog
    # 設定環境變數
    environment:
      # Server 唯一標識(1 - 255)
      ZOO_MY_ID: 2
      # 指定服務資訊,格式: server.A=B:C:D;E
      # 其中: A 表示服務器標識,B 是服務器 ip(服務名),C 是服務器與集群中 Leader 進行互動的埠,D 是用來選取新 Leader 進行互動的埠, E 為埠號
      ZOO_SERVERS: server.2=0.0.0.0:2888:3888;2182 server.3=120.26.184.41:2888:3888;2182
      #ZOO_SERVERS: server.2=zookeeper_service2:2888:3888;2182 server.3=120.26.184.41:2888:3888;2182

 

 

 

(6)分別在兩個服務器上通過 docker-compose up -d 啟動,
  先在 182.92.120.184 上啟動,然后在 120.26.184.41 上啟動,
  正常搭建后,server 角色 如下所示,
  同理,若需要增加節點,按照上面三步操作即可,

 

 

 

 

 

 

五、服務注冊與發現 -- Consul

1、什么是 Consul ?

(1)什么是 Consul ?

【Consul:】
    Consul 是一套開源的分布式服務發現和配置管理系統,由 HashiCorp 公司使用 Go 語言開發,
    提供了微服務系統中 服務發現、配置中心 等功能,
    
【官網地址:】
    https://www.consul.io/docs/intro
    https://learn.hashicorp.com/consul
    https://github.com/hashicorp/consul

注:
    官網解釋還是挺詳細的,請自行查閱,
    此處僅簡單使用一下 Consul,具體原理沒有仔細研究,后續有時間再補充,

 

(2)consul 角色
  consul 與 zookeeper 類似,提供了可執行程式作為 服務端,
  但其可以細分為兩種角色(client、server),

【角色:】
client: 
    客戶端, 無狀態,
    consul agent --client
    將 HTTP 和 DNS 介面請求轉發給局域網內的服務端集群,
    
server: 
    服務端, 保存配置資訊,
    可作為高可用集群, 在局域網內與本地客戶端 client 通訊, 通過廣域網與其他資料中心通訊,
    每個資料中心的 server 數量推薦為 3 個或是 5 個(奇數個),

 

(3)常用埠說明:

【常用埠說明:】
8300:通常用于 server 節點,處理集群內部的請求(資料讀寫、復制),
8301:通常用于單個資料中心的所有節點間相互通信(局域網 LAN 內資訊同步),
8302:通常用于單個或多個資料中心之間節點的相互通信(廣域網 WAN 內資訊同步),
8500:通常用于提供 UI 服務、獲取服務串列、注冊服務、注銷服務 等 HTTP 介面,
8600:通常作為 DNS 服務器,提供服務發現功能(通過節點名查詢節點資訊),

 

2、官網下載、安裝 consul -- windows

(1)從官網下載,
  官網提供了各種版本的可執行程式,下載相應版本即可,
  此處下載 windows 版本的 consul 為例,并使用,

【官網下載地址:】
    https://www.consul.io/downloads

 

 

 

(2)安裝 consul
  下載之后,可以得到一個可執行檔案(consul.exe),雙擊即可運行(會閃一下彈窗),
  判斷是否安裝成功,可以進入命令列模式,輸入 consul -version,
  若正常輸出版本號,則安裝成功,

 

 

 

 

 

 

(3)啟動 consul
  命令列啟動,默認通過 8500 埠可以訪問 ui 界面,

【命令列輸入:】
    consul agent -dev -client=0.0.0.0 -bootstrap-expect=1 -ui -node=consul_server1
引數說明:
    -client   指定客戶端可以訪問的 ip,默認為 127.0.0.1(不對外提供服務),設定成 0.0.0.0 表示不對客戶端 ip 進行限制(對外提供服務),
    -dev      以開發模式啟動,
    -ui       可以使用 web 界面訪問,
    -bootstrap-expect  表示集群中 server 節點個數,一般為奇數,
    -node     表示節點在 web ui 界面中顯示的名稱,

 

 

 

3、SpringCloud 整合 consul -- 單機版

(1)說明:

【說明:】
    SpringCloud 整合 consul,與 SpringCloud 整合 Zookeeper 是非常類似的,
    通過上面步驟已經成功啟動了 Server 節點,現在只需要將服務注冊進 Server 即可,

與 Zookeeper 類似,客戶端也可分為 服務提供者、服務消費者,

創建與 zookeeper_client_producer_8005 類似的 consul_client_producer_8009 作為 服務提供者,
創建與 zookeeper_client_consumer_9003 類似的 consul_client_consumer_9005 作為 服務消費者,

 

(2)創建 consul_client_producer_8009 子模塊,
  修改子模塊 與 父模塊 pom.xml 檔案(與前面創建模塊類似,此處省略),
  引入 consul_discovery 依賴 以及 actuator 依賴,
  修改 application.yml 組態檔,
  在啟動類上添加 @EnableDiscoveryClient 注解(不添加好像也可以正常注冊),

【依賴:】
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

【application.yml】
server:
  port: 8009

spring:
  application:
    name: consul_client_producer
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123456
    url: jdbc:mysql://120.26.184.41:3306/producer?useUnicode=true&characterEncoding=utf8
  cloud:
    # 配置 consul 資訊
    consul:
      # 配置注冊中心地址
      host: localhost
      # 配置訪問埠號
      port: 8500
      # 配置服務提供者資訊(非必須配置)
      discovery:
        # 是否需要注冊
        register: true
        # 配置實體 ID
        instance-id: ${spring.application.name}-${server.port}
        # 配置服務名
        service-name: ${spring.application.name}
        # 配置服務埠
        port: ${server.port}
        # 使用 ip 地址而非主機名
        prefer-ip-address: true
        # 設定健康檢查路徑
        health-check-url: http://${spring.cloud.client.ip-address}:${server.port}/actuator/health
        # 健康檢查失敗后,自動剔除服務(單位 s 表示秒,m 表示分鐘)
        health-check-critical-timeout: 10s

 

 

 

(3)創建 consul_client_consumer_9005 模塊,
  修改子模塊 與 父模塊 pom.xml 檔案(與前面創建模塊類似,此處省略),
  引入 consul_discovery 依賴 以及 actuator 依賴,
  修改 application.yml 組態檔,
  在啟動類上添加 @EnableDiscoveryClient 注解(不添加好像也可以正常注冊),
  引入 RestTemplate 時,需要添加 @LoadBalanced,并修改訪問地址為 服務名,

【依賴:】
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

【application.yml】
server:
  port: 9005

spring:
  application:
    name: consul-client-consumer
  cloud:
    # 配置 consul 資訊
    consul:
      # 配置注冊中心地址
      host: localhost
      # 配置訪問埠號
      port: 8500
      # 配置服務提供者資訊(非必須配置)
      discovery:
        # 是否需要注冊
        register: true
        # 配置實體 ID
        instance-id: ${spring.application.name}-${server.port}
        # 配置服務名
        service-name: ${spring.application.name}
        # 配置服務埠
        port: ${server.port}
        # 使用 ip 地址而非主機名
        prefer-ip-address: true
        # 設定健康檢查路徑
        health-check-url: http://${spring.cloud.client.ip-address}:${server.port}/actuator/health
        # 健康檢查失敗后,自動剔除服務(單位 s 表示秒,m 表示分鐘)
        health-check-critical-timeout: 10s

 

 

 

(4)分別啟動兩個服務
  可以在 consul UI 界面看到服務資訊,

 

 

 

 

 

 

4、健康檢查出錯問題

(1)健康檢查出錯分析
分析一:
  一般是 actuator 依賴未添加(能解決大部分問題),

分析二:
  如果添加 actuator 依賴后仍出錯,可能是服務器的問題,
  一般是通過健康檢查的 URL 無法訪問服務,
比如:
  若使用 云服務器安裝并部署 consul,而在本地啟動 服務時,此時 健康檢查 可能會出錯,服務器呼叫健康檢查請求 被拒絕(因為此時 健康檢查 URL 非公網 IP 地址,無法訪問到服務),將服務同樣部署在 服務器上,將服務器公網 IP 作為健康檢查 URL 地址(并開放相關埠),此時通過公網 IP 可以訪問到服務,從而健康檢查成功,
  詳見后面 docker-compose 啟動 consul 單機版,

(2)添加 actuator 依賴
  大多數情況下,添加上 actuator 依賴即可解決問題,

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

 

 

 

5、docker-compose 啟動 consul 單機版

(1)docker-compose.yml 檔案如下:

【docker-compose.yml:】
# 指定 compose 檔案版本,與 docker 兼容,高版本的 docker 一般使用 3.x,
version: '3.7'

services:
  # 設定服務名
  consul_server1:
    # 配置所使用的鏡像
    image: consul
    # 容器總是重啟
    restart: always
    # 容器名稱
    container_name: consul_server1
    #與宿主機的埠映射
    ports:
      - 8500:8500
      - 8300:8300
      - 8301:8301
      - 8302:8302
      - 8600:8600
    #容器目錄映射
    volumes:
      - /usr/mydata/consul/consul_server1/data:/consul/data
      - /usr/mydata/consul/consul_server1/config:/consul/config
    # 覆寫容器默認啟動命令
    command: agent -server -bind=0.0.0.0 -client=0.0.0.0 -bootstrap-expect=1 -ui -node=consul_server1
    
【command 引數解釋:】
    -server   以服務端角色啟動,
    -client   指定客戶端可以訪問的 ip,默認為 127.0.0.1(不對外提供服務),設定成 0.0.0.0 表示不對客戶端 ip 進行限制(對外提供服務),
    -dev      以開發模式啟動,
    -bind     表示系結到指定 ip,
    -ui       可以使用 web 界面訪問,
    -bootstrap-expect  表示集群中 server 節點個數,一般為奇數,
    -node     表示節點在 web ui 界面中顯示的名稱,
    -retry-join 表示加入集群中去,加入失敗后可以重新加入

 

 

 

(2)查看集群中成員
  通過 consul members 可以查看集群中成員狀態,
比如:
  docker exec -t consul_server1 consul members
注:
  consul_server1 是容器名稱,

 

 

 

(3)訪問 UI 界面,可以查看到節點情況,
  啟動正常情況,訪問界面如下:

 

 

 

啟動失敗情況,訪問頁面出現 500 錯誤,如下:

 

 

 

(4)演示健康檢查出錯
  由于此處在 云服務器上啟動 consul 服務器,而服務還是在本地啟動,
  修改服務的 組態檔,將其注冊中心地址改為云服務器公網 IP 地址: 120.26.184.41,
  以 consul_client_consumer_9005 為例,修改如下圖所示:

server:
  port: 9005

spring:
  application:
    name: consul-client-consumer
  cloud:
    # 配置 consul 資訊
    consul:
      # 配置注冊中心地址
      host: 120.26.184.41
      # 配置訪問埠號
      port: 8500
      # 配置服務提供者資訊(非必須配置)
      discovery:
        # 是否需要注冊
        register: true
        # 配置實體 ID
        instance-id: ${spring.application.name}-${server.port}
        # 配置服務名
        service-name: ${spring.application.name}
        # 配置服務埠
        port: ${server.port}
        # 使用 ip 地址而非主機名
        prefer-ip-address: true
        # 設定健康檢查路徑
        health-check-url: http://${spring.cloud.client.ip-address}:${server.port}/actuator/health
        # 健康檢查失敗后,自動剔除服務(單位 s 表示秒,m 表示分鐘)
        health-check-critical-timeout: 10s
management:
  endpoint:
    health:
      #顯示健康具體資訊,默認不會顯示詳細資訊
      show-details: always

 

 

 

  重新啟動服務后,發現健康檢查出錯,
注:
  服務本地啟動是沒問題的,但是服務器 通過 健康檢查 URL 訪問不到服務,

 

 

 

修改服務組態檔,將健康檢查 URL 設定成 服務器公網 IP 地址,并將服務打包部署到 服務器上(需要配置安全組,開放埠),此時健康檢查通過公網 IP 可以訪問到 服務,

 

 

 

 

 

 

6、docker-compose 啟動 consul 偽集群

(1)說明
  篇幅有限,此處僅演示 docker-compose 啟動偽集群,不創建服務模塊進行演示,

(2)偽集群
  docker-compose.yml 檔案如下,
  啟動三個容器,consul_server1、consul_server2、consul_server3 均以 server 模式啟動,

【docker-compose.yml】
# 指定 compose 檔案版本,與 docker 兼容,高版本的 docker 一般使用 3.x,
version: '3.7'

services:
  # 設定服務名
  consul_server1:
    # 配置所使用的鏡像
    image: consul
    # 容器總是重啟
    restart: always
    # 容器名稱
    container_name: consul_server1
    #與宿主機的埠映射
    ports:
      - 8500:8500
    #容器目錄映射
    volumes:
      - /usr/mydata/consul/consul_server1/data:/consul/data
      - /usr/mydata/consul/consul_server1/config:/consul/config
    # 覆寫容器默認啟動命令
    command: agent -server -bind=0.0.0.0 -client=0.0.0.0 -ui -bootstrap-expect=3 -ui -node=consul_server1

  # 設定服務名
  consul_server2:
    # 配置所使用的鏡像
    image: consul
    # 容器總是重啟
    restart: always
    # 容器名稱
    container_name: consul_server2
    #與宿主機的埠映射
    ports:
      - 8501:8500
    #容器目錄映射
    volumes:
      - /usr/mydata/consul/consul_server2/data:/consul/data
      - /usr/mydata/consul/consul_server2/config:/consul/config
    # 覆寫容器默認啟動命令
    command: agent -server -bind=0.0.0.0 -client=0.0.0.0 -ui -bootstrap-expect=3 -ui -node=consul_server2 -join=consul_server1

  # 設定服務名
  consul_server3:
    # 配置所使用的鏡像
    image: consul
    # 容器總是重啟
    restart: always
    # 容器名稱
    container_name: consul_server3
    #與宿主機的埠映射
    ports:
      - 8502:8500
    #容器目錄映射
    volumes:
      - /usr/mydata/consul/consul_server3/data:/consul/data
      - /usr/mydata/consul/consul_server3/config:/consul/config
    # 覆寫容器默認啟動命令
    command: agent -server -bind=0.0.0.0 -client=0.0.0.0 -ui -bootstrap-expect=3 -ui -node=consul_server3 -join=consul_server1
    
【command 引數解釋:】
    -server   以服務端角色啟動,
    -client   指定客戶端可以訪問的 ip,默認為 127.0.0.1(不對外提供服務),設定成 0.0.0.0 表示不對客戶端 ip 進行限制(對外提供服務),
    -dev      以開發模式啟動,
    -bind     表示系結到指定 ip,
    -ui       可以使用 web 界面訪問,
    -bootstrap-expect  表示集群中 server 節點個數,一般為奇數,
    -node     表示節點在 web ui 界面中顯示的名稱,
    -join       表示加入集群中去,
    -retry-join 表示加入集群中去,加入失敗后可以重新加入

 

 

 

 

 

 

consul 就簡單介紹到這了,沒有在實際作業中使用過,用到的時候再去研究一下,
有什么不對的地方,希望不吝賜教,

 

六、服務注冊與發現 -- Nacos

1、未完待續,,,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/240755.html

標籤:Java

上一篇:JVM HotSpot 可達性分析演算法實作細節

下一篇:Java基礎之:集合——Map

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more