1. 程式人生 > >SpringCloud(4)---Ribbon服務調用,源碼分析

SpringCloud(4)---Ribbon服務調用,源碼分析

port click struct 過去 打印 客戶端 tex align 啟動

SpringCloud(4)---Ribbon

本篇模擬訂單服務調用商品服務,同時商品服務采用集群部署。

註冊中心服務端口號7001,訂單服務端口號9001,商品集群端口號:8001、8002、8003。

技術分享圖片

各服務的配置文件這裏我這邊不在顯示了,和上篇博客配置一樣。博客地址:SpringCloud(3)---Eureka服務註冊與發現

一、商品中心服務端

1、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.jincou</groupId> <artifactId>product</artifactId> <version>0.0.1-SNAPSHOT</version> <
packaging>jar</packaging> <name>product</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <!--定義當前springcloud版本--> <spring-cloud.version>Finchley.RELEASE</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!--表明是Eureka Client客戶端--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <scope>provided</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
pom.xml

2、Product商品實體類

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product implements Serializable {

    private int id;
    //商品名稱
    private String name;
    //價格,分為單位
    private int price;
    //庫存
    private int store;
}

3、ProductService商品接口

public interface ProductService {

    //查找所有商品
    List<Product> listProduct();

    //根據商品ID查找商品
    Product findById(int id);
}

4、ProductServiceImpl商品實現類

@Service
public class ProductServiceImpl implements ProductService {

    private static final Map<Integer, Product> daoMap = new HashMap<>();

    //模擬數據庫商品數據
    static {
        Product p1 = new Product(1, "蘋果X", 9999, 10);
        Product p2 = new Product(2, "冰箱", 5342, 19);
        Product p3 = new Product(3, "洗衣機", 523, 90);
        Product p4 = new Product(4, "電話", 64345, 150);

        daoMap.put(p1.getId(), p1);
        daoMap.put(p2.getId(), p2);
        daoMap.put(p3.getId(), p3);
        daoMap.put(p4.getId(), p4);
    }

    @Override
    public List<Product> listProduct() {
        Collection<Product> collection = daoMap.values();
        List<Product> list = new ArrayList<>(collection);
        return list;
    }
    @Override
    public Product findById(int id) {
        return daoMap.get(id);
    }
}

5、ProductController

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

    //集群情況下,用於訂單服務查看到底調用的是哪個商品微服務節點
    @Value("${server.port}")
    private String port;

    @Autowired
    private ProductService productService;

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

    //根據id查找商品詳情
    @RequestMapping("find")
    public Object findById(int id){
        Product product = productService.findById(id);
        Product result = new Product();
        BeanUtils.copyProperties(product,result);
        result.setName( result.getName() + " data from port="+port );
        return result;
    }
}

6、測下該服務接口是否成功

技術分享圖片

二、訂單中心服務端

1、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.jincou</groupId>
    <artifactId>order</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>order</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
    </properties>

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

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

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

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

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

</project>
pom.xml

2、ProductOrder商品訂單實體

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ProductOrder implements Serializable {  
    //訂單ID
    private int id;
    // 商品名稱
    private String productName;
    //訂單號
    private String tradeNo;
    // 價格,分
    private int price;
    //訂單創建時間
    private Date createTime;
    //用戶id
    private int userId;
    //用戶名
    private String userName;
}

3、ProductOrderService訂單接口

/**
 * 訂單業務類
 */
public interface ProductOrderService {
     //下單接口
     ProductOrder save(int userId, int productId);
}

4、ProductOrderServiceImpl訂單實現類

@Service
public class ProductOrderServiceImpl implements ProductOrderService {

    @Autowired
    private RestTemplate restTemplate;

    @Override
    public ProductOrder save(int userId, int productId) {
        //product-service是微服務名稱(這裏指向的商品微服務名稱),api/v1/product/find?id=? 就是商品微服務對外的接口
        Map<String, Object> productMap = restTemplate.getForObject("http://product-service/api/v1/product/find?id=" + productId, Map.class);

        ProductOrder productOrder = new ProductOrder();
        productOrder.setCreateTime(new Date());
        productOrder.setUserId(userId);
        productOrder.setTradeNo(UUID.randomUUID().toString());
        //獲取商品名稱和商品價格
        productOrder.setProductName(productMap.get("name").toString());
        productOrder.setPrice(Integer.parseInt(productMap.get("price").toString()));
        
        //因為在商品微服務配置了集群,所以這裏打印看下調用了是哪個集群節點,輸出端口號。
        System.out.println(productMap.get("name").toString());
        return productOrder;
    }
}

5、OrderController類

@RestController
@RequestMapping("api/v1/order")
public class OrderController {
    
    @Autowired
    private ProductOrderService productOrderService;
    
    @RequestMapping("save")
    public Object save(@RequestParam("user_id")int userId, @RequestParam("product_id") int productId){

    return productOrderService.save(userId, productId);
    }
}

6、SpringBoot啟動類

@SpringBootApplication
public class OrderApplication {

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

    //當添加@LoadBalanced註解,就代表啟動Ribbon,進行負載均衡
    @LoadBalanced
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

7、接口測試

技術分享圖片

多調幾次接口,看後臺打印

技術分享圖片

發現訂單服務去掉商品服務的時候,不是固定節點,而且集群的每個節點都有可能。所以通過Ribbon實現了負載均衡。

三、Ribbon源碼分析

1、@LoadBalanced註解作用

在springcloud中,引入Ribbon來作為客戶端時,負載均衡使用的是被@LoadBalanced修飾的RestTemplate對象

技術分享圖片

RestTemplate 是Spring自己封裝的http請求的客戶端,也就是說它只能發送一個正常的Http請求,這跟我們要求的負載均衡是有出入的,還有就是這個請求的鏈接上的域名

是我們微服的一個服務名,而不是一個真正的域名,那它是怎麽實現負載均衡功能的呢?

我們來看看RestTemplate的父類InterceptingHttpAccessor。

技術分享圖片

從源碼我們可以知道InterceptingHttpAccessor中有一個攔截器列表List<ClientHttpRequestInterceptor>,如果這個列表為空,則走正常請求流程,如果不為空則走

攔截器,所以只要給RestTemplate添加攔截器,而這個攔截器中的邏輯就是Ribbon的負載均衡的邏輯。通過@LoadBalanced註解為RestTemplate配置添加攔截器

具體的攔截器的生成在LoadBalancerAutoConfiguration這個配置類中,所有的RestTemplate的請求都會轉到Ribbon的負載均衡器上

(當然這個時候如果你用RestTemplate發起一個正常的Http請求時走不通,因為它找不到對應的服務。)這樣就實現了Ribbon的請求的觸發。

2.攔截器都做了什麽?

上面提到過,發起http後請求後,請求會到達到達攔截器中,在攔截其中實現負載均衡,先看看代碼:

技術分享圖片

我們可以看到在intercept()方法中實現攔截的具體邏輯,首先會根據傳進來的請求鏈接,獲取微服的名字serviceName,然後調用LoadBalancerClient的

execute(String serviceId, LoadBalancerRequest<T> request)方法,這個方法直接返回了請求結果,所以正真的路由邏輯在LoadBalancerClient的實現類中,

而這個實現類就是RibbonLoadBalancerClient,看看execute()的源碼:

技術分享圖片

首先是獲得均衡器ILoadBalancer這個類上面講到過這是Netflix Ribbon中的均衡器,這是一個抽象類,具體的實現類是ZoneAwareLoadBalancer上面也講到過,

每一個微服名對應一個均衡器,均衡器中維護者微服名下所有的服務清單。getLoadBalancer()方法通過serviceId獲得對應的均衡器,getServer()方法通過對應的均衡器

在對應的路由的算法下計算得到需要路由到Server,Server中有該服務的具體域名等相關信息。得到了具體的Server後執行正常的Http請求,整個請求的負載均衡邏輯就完成了。

在微服中Ribbon和 Hystrix通常是一起使用的,其實直接使用Ribbon和Hystrix實現服務間的調用並不是很方便,通常在Spring Cloud中我們使用Feign完成服務間的調用,

Feign是對Ribbon和Hystrix做了進一步的封裝方便大家使用,對Ribbon的學習能幫你更好的完成Spring Cloud中服務間的調用。

我只是偶爾安靜下來,對過去的種種思忖一番。那些曾經的舊時光裏即便有過天真愚鈍,也不值得譴責。畢竟,往後的日子,還很長。不斷鼓勵自己,

天一亮,又是嶄新的起點,又是未知的征程(上校6)

SpringCloud(4)---Ribbon服務調用,源碼分析