SpringCloud简介

Spring Cloud是一系列框架的有序集合。它利用SpringBoot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务注册中心、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用SpringBoot的开发风格做到一键启动和部署。SpringCloud将目前各家公司开发的比较成熟、经得起考验的服务框架组合起来,通过SpringBoot风格进行再封装屏蔽掉复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。微服务是可以独立部署、水平扩展、独立访问(或者有独立的数据库)的服务单元,springcloud就是这些微服务的大管家,采用了微服务这种架构之后,项目的数量会非常多,springcloud需要管理好这些微服务。

本文主要将通过手动搭建一个Spring Cloud分布式项目学习SpringCloud中的各个组件。

本文采用Idea搭建SpringCloud项目,文中使用的工程和服务都是整个项目的模块。结构类如下:

image.png

Maven主工程

基于对maven依赖管理的考虑,新建一个maven主工程,主要用来约定整个工程项目中的所有服务使用到的依赖版本。新建服务spring-cloud-parent,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>

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

    <groupId>com.lin</groupId>
    <artifactId>spring-cloud-parent</artifactId>
    <version>1.0-SNAPSHOT</version>

    <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>Hoxton.SR6</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </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中的都使用如下配置

<parent>
  <groupId>com.lin</groupId>
  <artifactId>spring-cloud-parent</artifactId>
  <version>1.0-SNAPSHOT</version>
</parent>

服务注册与发现——Eureka

SpringCloud中的所有服务之间的交互都是基于注册中心,这里就用Eureka注册中心组件搭建服务。

注册中心

首先新建一个eureka-server服务,需要额外添加依赖spring-cloud-starter-netflix-eureka-server

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

启动一个服务注册中心只需要在Springboot启动类上添加一个注解@EnableEurekaServer

@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {

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

}

另外,我们还需要配置eureka使用的端口,registerWithEureka和fetchRegistry来标明这是一个eureka-server服务,配置好serviceUrl.defaultZone, application.yml文件内容如下:

server:
  port: 8761

eureka:
  instance:
    hostname: localhost
  client:
    registerWithEureka: false
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

现在eureka-server服务已经配置完成,接下来就只要启动注册中心服务就好了。使用浏览器访问http://localhost:8761/ 可以看到如下界面

image.png

服务提供者和服务消费者

其实相对eureka-server服务来说,其他的eureka-client服务本身既可以是服务提供者,也可以是服务消费者,所以我们把spring-cloud-starter-netflix-eureka-client和spring-boot-starter-web添加到parent主工程中,新建一个service-hi服务,

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

添加完依赖之后在SpringBoot启动类中添加 @EnableDiscoveryClient :

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

接着需要配置spring.application.name和需要注册到eureka的地址eureka.client.serviceUrl.defaultZone, application.yml内容如下:

server:
  port: 8762

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
spring:
  application:
    name: service-a

添加一个Controller接口提供给其他服务,HelloController类:

@RestController
public class HelloController {
    @Value("${server.port}")
    String port;

    @RequestMapping("/hi")
    public String home(@RequestParam(value = "name", defaultValue = "forezp") String name) {
        return "hi " + name + " ,i am from port:" + port;
    }
}

启动服务后使用浏览器访问 http://localhost:8761/ 就可以看到service-hi服务已经启动

image.png
再使用浏览器访问http://localhost:8762/hi?name=lin将会返回:

hi lin ,i am from port:8762

服务间通讯与负载均衡

在微服务架构中,业务都会被拆分成一个独立的服务,服务与服务直接的通讯是基于http restful的,Spring Cloud有两种服务调用方式,一种是Ribbon+RestTemplate,另一个是feign。

Ribbon+RestTemplate

在使用Ribbon之前,先启动eureka-server服务,再利用idea启动两个service-hi服务,端口分别为8762和8763,我们利用两个端口对应的service-hi服务模拟一个小的集群。基于上面小节所介绍的消费者服务搭建基础上新建一个service-ribbon服务,在pom.xml中添加ribbon依赖:

<dependencies>
  <dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
  </dependency>
</dependencies>

服务配置文件指定的服务的注册中心地址为http://localhost:8761/eureka/,服务名为service-ribbon,端口为8764,配置文件application.yml如下:

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
server:
  port: 8764
spring:
  application:
    name: service-ribbon

使用RestTemplate实现服务间远程调用需要将RestTemplate添加到spring ioc容器中,可以直接在启动类上注入RestTemplate bean或者在Configuration类中注入RestTemplate bean;并通过@LoadBalanced注解表明RestTemplate开启负载均衡的功能。

@SpringBootApplication
@EnableDiscoveryClient
public class ServiceRibbonApplication {

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

    /**
     * 将RestTemplate添加到spring ioc容器里面
     * @return
     */
    @Bean
    @LoadBalanced
    RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

编写一个测试类HelloService,通过使用ioc容器中的RestTemplate调用service-hi服务中的"/hi"接口,这里可以直接永程序名替代具体的url地址,在ribbon中会先根据服务名来选择具体的服务实例,再根据服务实例在请求会用具体的url代替服务名,代码如下:

@Service
public class HelloService {
    @Autowired
    RestTemplate restTemplate;
    public String hiService(String name) {
        return restTemplate.getForObject("http://SERVICE-HI/hi?name="+name,String.class);
    }
}

编写一个HelloController类,在HelloController中调用HelloService:

@RestController
public class HelloController {
    @Autowired
    HelloService helloService;
    @GetMapping(value = "/hi")
    public String hi(@RequestParam String name) {
        return helloService.hiService( name );
    }
}

此时使用浏览器多次访问 http://localhost:8764/hi?name=lin,浏览器交替显示:

hi lin ,i am from port:8762 hi lin ,i am from port:8763

此时的架构
  1. 一个服务注册中心,eureka server,端口为8761
  2. service-hi工程跑了两个实例,端口分别为8762,8763,分别向服务注册中心注册
  3. service-ribbon端口为8764,向服务注册中心注册
  4. 当service-ribbon通过RestTemplate调用service-hi的hi接口时,因为用ribbon进行了负载均衡,会以轮询的方式调用service-hi:8762和8763两个端口的hi接口;

Feign调用

在使用Feign之前,需要先了解feign是集成了ribbon的,feign的负载均衡策略默认为ribbon的轮询策略,也就是说在通过feign调用接口可以实现和ribbon一样的效果,接着在项目中新建service-feign服务,添加spring-cloud-starter-openfeign依赖:

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

想要使用feign必须启动类上添加注解@EnableFeignClients

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

}

service-feign服务同样需要配置eureka.client.serviceUrl.defaultZone为eureka的地址

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
server:
  port: 8765
spring:
  application:
    name: service-feign

编写一个HelloService调用service-hi服务中的接口(之前小节所编写的接口)

@FeignClient(value = "service-hi")
public interface HelloService {
    @GetMapping("/hi")
    String sayHiFromClientOne(@RequestParam(value = "name") String name);
}

另外在service-feign服务中添加一个Controller接口给外部调用

@RestController("/hi")
public class HelloController {
    @Autowired
    private HelloService helloService;

    @GetMapping(value = "/hi")
    public String sayHi(@RequestParam String name) {
        return helloService.sayHiFromClientOne( name );
    }
}

使用浏览器访问接口 http://localhost:8765/hi?name=luo,交替返回如下内容:

hi luo ,i am from port:8762
hi luo ,i am from port:8763

Hystrix 断路器

在微服务架构中,根据业务来拆分成一个个的服务,服务与服务之间可以相互调用(RPC),在Spring Cloud可以用RestTemplate+Ribbon和Feign来调用。为了保证其高可用,单个服务通常会集群部署。由于网络原因或者自身的原因,服务并不能保证100%可用,如果单个服务出现问题,调用这个服务就会出现线程阻塞,此时若有大量的请求涌入,Servlet容器的线程资源会被消耗完毕,导致服务瘫痪。服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成灾难性的严重后果,这就是服务故障的“雪崩”效应。为了解决这个问题,业界提出了断路器模型。

ribbon 使用短路器

hi lin ,i am from port:8762

hi,lin,sorry,error!

feign使用断路器

路由网关 Zuul

Zuul的主要功能是路由转发和过滤器。路由功能是微服务的一部分,比如/api/user转发到到user服务,/api/shop转发到到shop服务。zuul默认和Ribbon结合实现了负载均衡的功能。