介紹
本示例主要介紹 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請求的參數、格式、地址等信息,實現遠程接口調用,這樣將使我們的代碼更易擴展和利用,復合面向對象編程實現。