Spring Cloud Eureka 分布式開發之服務注冊中心、負載均衡、聲明式服務調用實現


介紹

本示例主要介紹 Spring Cloud 系列中的 Eureka,使你能快速上手負載均衡、聲明式服務、服務注冊中心等

Eureka Server

Eureka 是 Netflix 的子模塊,它是一個基於 REST 的服務,用於定位服務,以實現雲端中間層服務發現和故障轉移。

服務注冊和發現對於微服務架構而言,是非常重要的。有了服務發現和注冊,只需要使用服務的標識符就可以訪問到服務,而不需要修改服務調用的配置文件。該功能類似於 Dubbo 的注冊中心,比如 Zookeeper。

Eureka 采用了 CS 的設計架構。Eureka Server 作為服務注冊功能的服務端,它是服務注冊中心。而系統中其他微服務則使用 Eureka 的客戶端連接到 Eureka Server 並維持心跳連接

Eureka Server 提供服務的注冊服務。各個服務節點啟動后會在 Eureka Server 中注冊服務,Eureka Server 中的服務注冊表會存儲所有可用的服務節點信息。

Eureka Client 是一個 Java 客戶端,用於簡化 Eureka Server 的交互,客戶端同時也具備一個內置的、使用輪詢負載算法的負載均衡器。在應用啟動后,向 Eureka Server 發送心跳(默認周期 30 秒)。如果 Eureka Server 在多個心跳周期內沒有接收到某個節點的心跳,Eureka Server 會從服務注冊表中將該服務節點信息移除。

簡單理解:各個微服務將自己的信息注冊到server上,需要調用的時候從server中獲取到其他微服務信息

Ribbon

Spring Cloud Ribbon 是基於 Netflix Ribbon 實現的一套客戶端負載均衡工具,其主要功能是提供客戶端的軟件負載均衡算法,將 Netflix 的中間層服務連接在一起。

Ribbon 提供多種負載均衡策略:如輪詢、隨機、響應時間加權等。

Feign

Feign是聲明式、模板化的HTTP客戶端,可以更加快捷優雅的調用HTTP API。在部分場景下和Ribbon類似,都是進行數據的請求處理,但是在請求參數使用實體類的時候顯然更加方便,同時還支持安全性、授權控制等。
Feign是集成了Ribbon的,也就是說如果引入了Feign,那么Ribbon的功能也能使用,比如修改負載均衡策略等

代碼實現

1.創建eureka-server服務注冊中心

pom.xml pom配置

<?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.easy</groupId>
    <artifactId>eureka-server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>eureka-server</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <artifactId>cloud-feign</artifactId>
        <groupId>com.easy</groupId>
        <version>1.0.0</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

application.yml 配置文件

server:
    port: 9000

spring:
  application:
    name: eureka-server
    
eureka:
    instance:
        hostname: localhost   # eureka 實例名稱
    client:
        register-with-eureka: false # 不向注冊中心注冊自己
        fetch-registry: false       # 是否檢索服務
        service-url:
            defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/  # 注冊中心訪問地址

EurekaServerApplication.java 啟動類

package com.easy.eurekaServer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

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

2.創建hello-service-api接口

Result.java 統一返回實體

package com.easy.helloServiceApi.vo;


import lombok.Getter;

import java.io.Serializable;

@Getter
public class Result implements Serializable {

    private static final long serialVersionUID = -8143412915723961070L;

    private int code;

    private String msg;

    private Object data;

    private Result() {
    }

    private Result(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    private Result(int code, String msg, Object data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }


    public static Result success() {
        return success(null);
    }

    public static Result success(Object data) {
        return new Result(200, "success", data);
    }

    public static Result fail() {
        return fail(500, "fail");
    }

    public static Result fail(int code, String message) {
        return new Result(code, message);
    }
}

Order.java 訂單實體類

package com.easy.helloServiceApi.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 訂單類
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order {

    private String orderId;

    private String goodsId;

    private int num;

}

GoodsServiceClient.java 聲明商品服務類

package com.easy.helloServiceApi.client;

import com.easy.helloServiceApi.vo.Result;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

@FeignClient(value = "hello-server")
public interface GoodsServiceClient {

    @RequestMapping("/goods/goodsInfo/{goodsId}")
    Result goodsInfo(@PathVariable("goodsId") String goodsId);
}

Goods.java商品實體類

package com.easy.helloServiceApi.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

/**
 * 商品類
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Goods {

    private String goodsId;

    private String name;

    private String descr;

    // 測試端口
    private int port;
}

3.創建hello-service-01服務提供者(這里創建三個一樣的服務提供者做負載均衡用)

GoodsController.java商品服務入口


package com.easy.helloService.controller;

import com.easy.helloService.service.GoodsService;
import com.easy.helloServiceApi.model.Goods;
import com.easy.helloServiceApi.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/goods")
public class GoodsController {

    @Autowired
    private GoodsService goodsService;

    @RequestMapping("/goodsInfo/{goodsId}")
    public Result goodsInfo(@PathVariable String goodsId) {

        Goods goods = this.goodsService.findGoodsById(goodsId);
        return Result.success(goods);
    }
}

GoodsService.java接口

package com.easy.helloService.service;

import com.easy.helloServiceApi.model.Goods;

public interface GoodsService {

    Goods findGoodsById(String goodsId);
}

GoodsServiceImpl.java實現接口

package com.easy.helloService.service.impl;

import com.easy.helloService.service.GoodsService;
import com.easy.helloServiceApi.model.Goods;
import org.springframework.stereotype.Service;

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

@Service
public class GoodsServiceImpl implements GoodsService {

    // 模擬數據庫
    private static Map<String, Goods> data;

    static {
        data = new HashMap<>();
        data.put("1", new Goods("1", "華為", "華為手機", 8081));  //表示調用8081端口的數據,實際上數據會放在數據庫或緩存中
        data.put("2", new Goods("2", "蘋果", "蘋果", 8081));
    }

    @Override
    public Goods findGoodsById(String goodsId) {
        return data.get(goodsId);
    }
}

HelloServiceApplication.java啟動類

package com.easy.helloService;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@EnableDiscoveryClient
@SpringBootApplication
public class HelloServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(HelloServiceApplication.class, args);
    }
}

application.yml配置文件,8081端口做01服務

server:
    port: 8081

spring:
  application:
    name: hello-server
    
eureka:
    instance:
        instance-id: goods-api-8081
        prefer-ip-address: true # 訪問路徑可以顯示 IP
    client:
        service-url:
            defaultZone: http://localhost:9000/eureka/  # 注冊中心訪問地址

4.創建hello-service-02服務提供者(貼出和01服務不一樣的地方)

application.yml配置文件,8082做02服務端口

server:
    port: 8082

spring:
  application:
    name: hello-server
    
eureka:
    instance:
        instance-id: goods-api-8082
        prefer-ip-address: true # 訪問路徑可以顯示 IP
    client:
        service-url:
            defaultZone: http://localhost:9000/eureka/  # 注冊中心訪問地址

GoodsServiceImpl.java 這里故意設置不同的數據源,用來測試負載均衡有沒生效使用

package com.easy.helloService.service.impl;

import com.easy.helloService.service.GoodsService;
import com.easy.helloServiceApi.model.Goods;
import org.springframework.stereotype.Service;

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

@Service
public class GoodsServiceImpl implements GoodsService {

    // 模擬數據庫
    private static Map<String, Goods> data;

    static {
        data = new HashMap<>();
        data.put("1", new Goods("1", "華為", "華為手機", 8082));  //表示8082端口的數據,實際上數據會放在數據庫或緩存中
        data.put("2", new Goods("2", "蘋果", "蘋果", 8082));
    }

    @Override
    public Goods findGoodsById(String goodsId) {
        return data.get(goodsId);
    }
}

5.創建hello-service-02服務提供者(貼出和01服務不一樣的地方)

application.yml配置文件,8082做02服務端口

server:
    port: 8083

spring:
  application:
    name: hello-server
    
eureka:
    instance:
        instance-id: goods-api-8083
        prefer-ip-address: true # 訪問路徑可以顯示 IP
    client:
        service-url:
            defaultZone: http://localhost:9000/eureka/  # 注冊中心訪問地址

GoodsServiceImpl.java 這里故意設置不同的數據源,用來測試負載均衡有沒生效使用

package com.easy.helloService.service.impl;

import com.easy.helloService.service.GoodsService;
import com.easy.helloServiceApi.model.Goods;
import org.springframework.stereotype.Service;

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

@Service
public class GoodsServiceImpl implements GoodsService {

    // 模擬數據庫
    private static Map<String, Goods> data;

    static {
        data = new HashMap<>();
        data.put("1", new Goods("1", "華為", "華為手機", 8083));  //表示8083端口的數據,實際上數據會放在數據庫或緩存中
        data.put("2", new Goods("2", "蘋果", "蘋果", 8083));
    }

    @Override
    public Goods findGoodsById(String goodsId) {
        return data.get(goodsId);
    }
}

6.創建feign-consumer服務消費者,引入Ribbon實現服務調用負載均衡並實現聲明式服務調用

pom.xml配置

<?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.easy</groupId>
    <artifactId>feign-consumer</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>

    <name>feign-consumer</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <artifactId>cloud-feign</artifactId>
        <groupId>com.easy</groupId>
        <version>1.0.0</version>
    </parent>

    <dependencies>

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

        <!-- eureka 客戶端 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

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

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

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.9</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.easy</groupId>
            <artifactId>hello-service-api</artifactId>
            <version>0.0.1</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

引入openfeign、ribbon、eureka-client等依賴,openfeign用來實現聲明式服務調用,ribbon用來實現負載均衡,eureka-client用來注冊、發現服務

RestConfiguration.java 配置

package com.easy.feignConsumer.config;

import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestConfiguration {

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

    /**
     * 隨機選取負載均衡策略
     * @return
     */
    @Bean
    public IRule testRule() {
        return new RandomRule();
    }
}

GoodsService 服務類接口

package com.easy.feignConsumer.service;

import com.easy.helloServiceApi.model.Goods;
import com.easy.helloServiceApi.vo.Result;

public interface GoodsService {
    Result placeGoods(Goods goods);
}

GoodsServiceImpl.java 實現類

package com.easy.feignConsumer.service.impl;

import com.easy.feignConsumer.service.GoodsService;
import com.easy.helloServiceApi.client.GoodsServiceClient;
import com.easy.helloServiceApi.model.Goods;
import com.easy.helloServiceApi.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class GoodsServiceImpl implements GoodsService {

    @Autowired
    private GoodsServiceClient goodsServiceClient;

    @Override
    public Result placeGoods(Goods order) {

        Result result = this.goodsServiceClient.goodsInfo(order.getGoodsId());

        if (result != null && result.getCode() == 200) {
            log.info("=====獲取本地商品====");
            log.info("接口返回數據為==>{}", ToStringBuilder.reflectionToString(result.getData()));
        }
        return result;
    }
}

GoodsController.java 控制器

package com.easy.feignConsumer.controller;

import com.easy.feignConsumer.service.GoodsService;
import com.easy.helloServiceApi.model.Goods;
import com.easy.helloServiceApi.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/goods")
public class GoodsController {

    @Autowired
    private GoodsService orderService;

    @RequestMapping("/place")
    public Result placeGoods(Goods goods) {
        Result result = this.orderService.placeGoods(goods);
        return result;
    }
}

FeignConsumerApplication.java 消息者啟動類

package com.easy.feignConsumer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableFeignClients(basePackages = {"com.easy"})
@EnableEurekaClient
@SpringBootApplication
public class FeignConsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(FeignConsumerApplication.class, args);
    }
}

application.yml 配置文件

server:
  port: 8100

spring:
  application:
    name: feign-consumer

eureka:
  instance:
    instance-id: order-api-8100
    prefer-ip-address: true # 訪問路徑可以顯示 IP
  client:
    service-url:
      defaultZone: http://localhost:9000/eureka/  # 注冊中心訪問地址

使用示例

運行創建的5個服務

1個服務注冊中心,3個服務提供者,1個服務消費者

進入服務注冊中心查看服務

地址欄輸入:http://localhost:9000/,我們看到5個服務注冊成功並且都是運行狀態了(UP狀態),效果如下:

  • Application列下有兩個服務(FEIGN-CONSUMER、HELLO-SERVER)
  • Availability Zones列下表示可用服務分別的數量(這里分別顯示1和3)
  • Status 列顯示服務狀態,UP表示服務在運行狀態,后面分別跟着服務的內部地址:goods-api-8100(服務消費者),goods-api-8081(服務提供者01), goods-api-8082(服務提供者02), goods-api-8083(服務提供者03)

調用接口測試

地址欄輸入:http://localhost:8100/goods/place?goodsId=1,返回數據結果為:

{
code: 200,
msg: "success",
data: {
goodsId: "1",
name: "華為",
descr: "華為手機",
port: 8081
}
}
  • 多刷新幾次頁面,我們發現port會在8081 8082 8083隨機變化,表示我們的隨機負載均衡器生效了
  • 隨意關掉2個或1個服務提供者,刷新頁面接口功能無影響,能正常返回數據,實現了高可用

聲明式服務和非聲明式服務對比

非聲明式服務調用代碼

    @Test
    public void testFeignConsumer() {
        Goods goods = new Goods();
        goods.setGoodsId("1");
        Result result = this.restTemplate.getForObject("http://HELLO-SERVER/goods/goodsInfo/" + goods.getGoodsId(), Result.class);
        log.info("成功調用了服務,返回結果==>{}", ToStringBuilder.reflectionToString(result));
    }

消費端每個請求方法中都需要拼接請求服務的 URL 地址,存在硬編碼問題並且這樣並不符合面向對象編程的思想

聲明式服務調用

@FeignClient(value = "hello-server")
public interface GoodsServiceClient {

    @RequestMapping("/goods/goodsInfo/{goodsId}")
    Result goodsInfo(@PathVariable("goodsId") String goodsId);
}
    @Autowired
    private GoodsServiceClient goodsServiceClient;

    @Override
    public Result placeGoods(Goods order) {
        Result result = this.goodsServiceClient.goodsInfo(order.getGoodsId());
        return result;
    }

通過編寫簡單的接口和插入注解,就可以定義好HTTP請求的參數、格式、地址等信息,實現遠程接口調用,這樣將使我們的代碼更易擴展和利用,復合面向對象編程實現。

資料


免責聲明!

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



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