微服務之間的呼叫(Ribbon與Feign)

推敲發表於2017-09-04

概述

在前面的文章中,我們講了使用Eureka作為服務註冊中心,在服務啟動後,各個微服務會將自己註冊到Eureka server。那麼服務之間是如何呼叫?又是如何進行負載均衡的呢?本文講講服務之間呼叫及負載均衡Ribbon。

目前,在Spring cloud 中服務之間通過restful方式呼叫有兩種方式
- restTemplate+Ribbon
- feign

從實踐上看,採用feign的方式更優雅(feign內部也使用了ribbon做負載均衡)。

本文使用如下的專案例項來分別演示這兩種寫法。
- hello服務,讀取資料庫,返回資訊
- world服務,返回資訊
- helloworld服務,呼叫了hello服務和world服務(restTemplate+Ribbon方式)
- helloworldfeign服務,呼叫了hello服務和world服務(feign方式)

專案之間關係.png

本文專案程式碼:springcloud-demo
如何理解客戶端Ribbon
zuul也有負載均衡的功能,它是針對外部請求做負載,那客戶端ribbon的負載均衡又是怎麼一回事?

客戶端ribbon的負載均衡,解決的是服務發起方(在Eureka註冊的服務)對被呼叫的服務的負載,比如我們查詢商品服務要呼叫顯示庫存和商品明細服務,通過商品服務的介面將兩個服務組合,可以減少外部應用的請求,比如手機App發起一次請求即可,可以節省網路頻寬,也更省電。

ribbon是對服務之間呼叫做負載,是服務之間的負載均衡,zuul是可以對外部請求做負載均衡。
Ribbon負載均衡.png

hello服務與world服務

hello服務
Git專案程式碼:hello
pom.xml

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>        
        <!--connect the db-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
            <version>1.5.4.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.31</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
            <version>1.5.6.RELEASE</version>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Dalston.SR2</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

啟動類HelloApplication:

package com.example.hello;

import com.example.hello.model.Info;
import com.example.hello.repository.InfoRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.Bean;


@EnableDiscoveryClient
@SpringBootApplication
public class HelloApplication {

    private static final Logger log= LoggerFactory.getLogger(HelloApplication.class);
    public static final String KEY="Database";
    public static final String VALUE="MYSQL FROM BILLJIANG";

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

    @Bean
    public CommandLineRunner initDatabase(InfoRepository repository){
        return (args) -> {
            repository.save(new Info(KEY,VALUE));
        };
    }
}

入口類HelloController:

package com.example.hello.controller;

import com.example.hello.HelloApplication;
import com.example.hello.model.HelloMessage;
import com.example.hello.model.Info;
import com.example.hello.repository.InfoRepository;
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.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;


@RestController
public class HelloController {

    @Autowired
    private DiscoveryClient discoveryClient;

    @Autowired
    private InfoRepository infoRepository;

    @GetMapping("/")
    public String home(){
        return "hello";
    }

    @GetMapping("/message")
    public HelloMessage getMessage() {
        HelloMessage helloMessage = new HelloMessage();
        helloMessage.setName(getLocalInstanceInfo());
        helloMessage.setMessage(getInfoFromDatabase());
        return helloMessage;
    }


    private String getLocalInstanceInfo() {
        ServiceInstance serviceInstance = discoveryClient.getLocalServiceInstance();
        return serviceInstance.getServiceId() + ":" + serviceInstance.getHost() + ":" + serviceInstance.getPort();
    }


    private String getInfoFromDatabase() {
        List<Info> infoList = infoRepository.findByName(HelloApplication.KEY);
        for (Info info : infoList) {
            return info.toString();
        }
        return "(no database info)";
    }
}

配置檔案application.yml:


#隨機埠
server:
  port: 0

#服務註冊中心
eureka:
  client:
    service-url:
      default-zone: http://localhost:8761/eureka
  instance:
    instance-id: ${spring.application.name}:${spring.application.instance_id:${random.value}}

#資料來源
spring:
  application:
    name: hello
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/${{MYSQL_DATABASE}:foodb}
    username: ${{MYSQL_USERNAME}:root}
    password: ${{MYSQL_PASSWORD}:billjiang}
    testWhileIdle: true
    validationQuery: SELECT 1
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: update
      naming-strategy: org.hibernate.cfg.ImprovedNamingStrategy
      properties:
        hibernate:
        dialect: org.hibernate.dialect.MySQL5Dialect
  zipkin:
    base-url: http://127.0.0.1:9411

其他類略,請參照原始碼:

world
world服務與hello服務類似,不過沒有連線資料庫
專案程式碼:world

helloworld呼叫hello服務和world服務

採用restTemplate+Ribbon呼叫服務。
專案程式碼:helloworld

啟動類HelloworldApplication:
配置restTemplate的Bean

package com.example.helloworld;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
@EnableDiscoveryClient
public class HelloworldApplication {

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

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

入口類HelloworldController:

package com.example.helloworld.controller;

import com.example.helloworld.model.HelloMessage;
import com.example.helloworld.model.HelloworldMessage;
import com.example.helloworld.model.WorldMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

/**
 * @author billjiang 475572229@qq.com
 * @create 17-8-22
 */
@RestController
public class HelloworldController {

    private static final Logger log = LoggerFactory.getLogger(HelloworldController.class);

    private static final String HELLO_SERVICE_NAME = "hello";

    private static final String WORLD_SERVICE_NAME = "world";

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/")
    public String home() {
        return "hello world";
    }

    @GetMapping("/message")
    public HelloworldMessage getMessage() {
        HelloMessage hello = getMessageFromHelloService();
        WorldMessage world = getMessageFromWorldService();
        HelloworldMessage helloworld = new HelloworldMessage();
        helloworld.setHello(hello);
        helloworld.setWord(world);
        log.debug("Result helloworld message:{}", helloworld);
        return helloworld;
    }

    private HelloMessage getMessageFromHelloService() {
        HelloMessage hello = restTemplate.getForObject("http://hello/message", HelloMessage.class);
        log.debug("From hello service : {}.", hello);
        return hello;
    }

    private WorldMessage getMessageFromWorldService() {
        WorldMessage world = restTemplate.getForObject("http://world/message", WorldMessage.class);
        log.debug("From world service : {}.", world);
        return world;
    }


}

測試:

helloworldfeign呼叫hello服務和world服務

feign方式呼叫服務,專案程式碼參考:helloworldfeign
pom.xml引入feignx

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

配置啟動類註解:

package com.example.helloworldfeign;

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

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class HelloworldFeignApplication {

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

被呼叫介面HelloService

package com.example.helloworldfeign.service;

import com.example.helloworldfeign.model.HelloMessage;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

/**
 * @author billjiang 475572229@qq.com
 * @create 17-8-23
 */
@FeignClient(value="hello")
public interface HelloService {

    @GetMapping("/message")
    HelloMessage hello();


}

被呼叫介面WorldService

package com.example.helloworldfeign.service;

import com.example.helloworldfeign.model.WorldMessage;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

/**
 * @author billjiang 475572229@qq.com
 * @create 17-8-23
 */
@FeignClient(value="world")
public interface WorldService {

    @GetMapping("/message")
    WorldMessage world();

}

入口類HelloworldController:

package com.example.helloworldfeign.controller;

import com.example.helloworldfeign.model.HelloMessage;
import com.example.helloworldfeign.model.HelloworldMessage;
import com.example.helloworldfeign.model.WorldMessage;
import com.example.helloworldfeign.service.HelloService;
import com.example.helloworldfeign.service.WorldService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author billjiang 475572229@qq.com
 * @create 17-8-23
 */
@RestController
public class HelloworldController {

    private static final Logger log = LoggerFactory.getLogger(HelloworldController.class);

    private static final String HELLO_SERVICE_NAME = "hello";

    private static final String WORLD_SERVICE_NAME = "world";

    @Autowired
    private HelloService helloService;

    @Autowired
    private WorldService worldService;

    @GetMapping("/")
    public String home() {
        return "hello world";
    }

    @GetMapping("/message")
    public HelloworldMessage getMessage() {
        HelloMessage hello = helloService.hello();
        WorldMessage world = worldService.world();
        HelloworldMessage helloworld = new HelloworldMessage();
        helloworld.setHello(hello);
        helloworld.setWord(world);
        log.debug("Result helloworld message:{}", helloworld);
        return helloworld;
    }

}

為了更好的地檢視效果,hello服務和world服務可以啟動多個例項,並把呼叫例項的ip和埠輸出來。

相關文章