49. SpringCloud

相关概念

Spring Cloud是分布式系统的整体解决方案,而不是一项具体的技术。

SpringBoot 和 Spring 的关系

SpringBoot底层就是Spring,简化使用Spring的方式而已,多加了好多的自动配置;

Spring Cloud 和 SpringBoot 的关系

Spring Cloud是分布式系统的整体解决方案,底层用的SpringBoot来构建项目,Cloud新增很多的分布式的starter,包括这些starter的自动配置;

资料信息

http://spring.io/projects
https://projects.spring.io/spring-cloud/#quick-start
https://springcloud.cc/

SpringCloud 案例开发

本次 SpringCloud 开发案例如下:需要创建注册中心,用于服务和电影服务。两个服务之间采用 SpringCloud 自带的 RESETful 进行调用。可以查询用户,购买电影票查询最新电影等操作。

注册中心

概念

注册中心的作用是管理所有服务(服务发现和注册)。注册中心中的 Region 和 Zone 就相当于大区和机房,一个 Region(大区)可以有很多的 Zone(机房)。在Spring Cloud 中,服务消费者会优先查找在同一个 Zone 的服务,之后在去查找其他的服务。如果该项配置使用的好,那么项目请求的响应时间将大大缩短!

创建注册中心

创建一个 Spring Starter Project

引入 eureka-server

新增 application.yml,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
spring:
application:
name: cloud-eureka-registry-center

server:
port: 8761

eureka:
instance:
hostname: localhost
client:
register-with-eureka: false #自己就是注册中心,不用注册自己
fetch-registry: false #要不要去注册中心获取其他服务的地址
service-url:
# 这里是引用的上面的配置,eureka.instance.hostname 就是 localhost。server.port 就是 8761
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

主程序使用注解 @EnableEurekaServer 开启 Eureka 注册中心功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.itguigu.springcloud;

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

// 声明当前项目为注册中心
@EnableEurekaServer
@SpringBootApplication
public class CloudEurekaRegistryCenterApplication {

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

访问 http://localhost:8761/ 就能查看注册中心的 Dashboard 页面信息

创建电影服务

新建 Spring Starter Project

选择 Eureka Client Starter 和 Spring Web Starter

创建 Controller,dao,service,service.impl,bean 包,MovieController 中的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.itguigu.springcloud.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import com.itguigu.springcloud.bean.Movie;
import com.itguigu.springcloud.service.MovieService;

// 这里使用 @RestController 来替代 @Controller 和 @ResponseBody
@RestController

// @Controller
public class MovieController {

@Autowired
MovieService moviceService;

// @ResponseBody
@GetMapping("/getMovieById/{id}")
public Movie getMovieById(@PathVariable("id") Integer id) {
return moviceService.getMovieById(id);
}
}

其余 dao,service,service.impl,bean 中的内容不在这里记录,直接启动项目(注意这里虽然没有配置 Eureka注册和发现,但是要确保你的 Eureka 已经启动了)

直接测试 http://localhost:8080/getMovieById/1 看看是否有数据返回,有数据返回则接口就可以了,接下来配置 Eureka,新建 application.yml 文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
application:
name: cloud-provider-movie

server:
port: 8000

# 指定注册到哪个注册中心
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true #注册自己服务使用ip的方式

在主程序中添加注解 @EnableDiscoveryClient 让其注册到注册中心当中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.itguigu.springcloud;

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

// 将该服务注册到注册中心(即启用服务注册和发现功能)
@EnableDiscoveryClient
@SpringBootApplication
public class CloudProviderMovieApplication {

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

注意⚠️:在启动类上面添加 @EnableDiscoveryClient@EnableEurekaClient 这二个注解作用,都可以让该服务注册到注册中心上去。不同点:@EnableEurekaClient 只支持Eureka注册中心,@EnableDiscoveryClient支持 Eureka、Zookeeper、Consul 这三个注册中心。

在此查看 Eureka 的 dashboard,就会看到服务已经注册进来了:

创建用户服务

新建 Spring Starter Project

选择 Eureka Client Starter 和 Spring Web Starter

创建 Controller,dao,service,service.impl,bean 包,UserController 中的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.itguigu.springcloud.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import com.itguigu.springcloud.bean.User;
import com.itguigu.springcloud.service.UserService;

@RestController
public class UserController {

@Autowired
UserService userService;

@GetMapping("/getUserById/{id}")
public User getUserById(@PathVariable("id") Integer id) {
return userService.getUserById(id);
}
}

直接测试 http://localhost:8080/getUserById/1 看看是否有数据返回,有数据返回则接口就可以了,接下来配置 Eureka,新建 application.yml 文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
application:
name: cloud-provider-user

server:
port: 9000

# 指定注册到哪个注册中心
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true #注册自己服务使用ip的方式

在主程序中添加注解 @EnableDiscoveryClient 让其注册到注册中心当中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.itguigu.springcloud;

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


@EnableDiscoveryClient
@SpringBootApplication
public class CloudProviderUserApplication {

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

查看 Eureka Dashboard,就会看到 Movie 服务和 User 服务都注册到里面了

远程调用

远程调用可以使用 RestTemplate 和 Feign。两种方式都需要掌握,Feign 可能会用得更多些。

RestTemplate

引入 Ribbon

在 dependencies 中使用 Alt + / 然后选择 Insert dependeny 这样就能选择导入被父工程管理的包。

容器注入 RestTemplate

给容器中注入一个RestTemplate并使用Ribbon进行负载均衡调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.itguigu.springcloud;

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;


@EnableDiscoveryClient
@SpringBootApplication
public class CloudProviderUserApplication {

/**
* 配置 RestTemplate bean 对象。因为这里主程序是一个配置类,所以就写在这里(并不是非得写到这里,只要是配置类就行)
* @return
*/
@Bean
@LoadBalanced // 负载均衡
public RestTemplate restTemplate() {
return new RestTemplate();
}

public static void main(String[] args) {
SpringApplication.run(CloudProviderUserApplication.class, args);
}
}
远程调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.itguigu.springcloud.controller;

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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import com.itguigu.springcloud.bean.Movie;
import com.itguigu.springcloud.bean.User;
import com.itguigu.springcloud.service.UserService;

@RestController
public class UserController {

@Autowired
UserService userService;

@Autowired
RestTemplate restTemplate;

/**
* 获取用户信息
* @param id
* @return
*/
@GetMapping("/getUserById/{id}")
public User getUserById(@PathVariable("id") Integer id) {
return userService.getUserById(id);
}

/**
* 购买电影票
* @param userId
* @param movieId
* @return
*/
@GetMapping("/buyMovie/{userId}/{movieId}")
public Map<String, Object> buyMovie(
@PathVariable("userId") Integer userId,
@PathVariable("movieId") Integer movieId
){
HashMap<String, Object> result = new HashMap<>();
// 获取用户信息,查询自己的服务即可
User user = userService.getUserById(userId);

// 查询电影信息,需要远程调用
Movie movie = restTemplate.getForObject("http://CLOUD-PROVIDER-MOVIE/getMovieById/"+movieId, Movie.class);

result.put("user", user);
result.put("movie", movie);
return result;
}
}

访问 http://localhost:9000/buyMovie/1/2 返回数据如下:

1
{"movie":{"id":2,"name":"电影名称2"},"user":{"id":1,"name":"User1"}}
集成 Ribbon

多运行几个 Movie 服务(多拷贝几份配置,并将配置端口设置为不一样的值)

配置多个 Movie 服务的端口。并在 Controller 中打印服务的端口信息,用来显示当前运行的端口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.itguigu.springcloud.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import com.itguigu.springcloud.bean.Movie;
import com.itguigu.springcloud.service.MovieService;

// 这里使用 @RestController 来替代 @Controller 和 @ResponseBody
@RestController

// @Controller
public class MovieController {

@Autowired
MovieService moviceService;

// 获取当前配置的端口信息,用来判断那个 Movie 服务被调用
@Value("${server.port}")
int port;

// @ResponseBody
@GetMapping("/getMovieById/{id}")
public Movie getMovieById(@PathVariable("id") Integer id) {
// 打印端口信息
System.out.println("port: " + port);
return moviceService.getMovieById(id);
}
}

启动项目

查看注册中心,就会发现 Movie 的三个服务都注册到注册中心了

再次调用购买电影票的接口 http://localhost:9000/buyMovie/1/2 6 次就会看到打印的端口信息。每个服务都打印了端口两次,也就说被调用了两次。(默认是轮训策略,每个 Movie 服务被调用一次,六次就是每个被调用两次。这说明 RestTemplate 默认就是和 Ribbon 集成的)

注意⚠️:这里的 @LoadBalanced 是配置在 User 服务上的,也就是说这里 Ribbon 是在发挥正向代理的作用,请求发给 User 服务,然后进入到 Ribbon 中进行负载均衡,将请求发往不同的 Movie 服务。Nginx 的那种代理是反向代理。

Feign

创建项目

复制一份 User 服务,名字叫做 cloud-provider-user-feign,并将以前的 User 服务名称修改为 cloud-provider-user-restTemplate,分别修改两个服务中的 pom 坐标和 application.yml 中的名称。

引入 Feign

在 cloud-provider-user-feign 的 pom 文件中引入 feign 的依赖

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
启用 Feign

在主程序中加上注解 @EnableFeignClients 启用 Feign 客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.itguigu.springcloud;

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.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class CloudProviderUserApplication {
public static void main(String[] args) {
SpringApplication.run(CloudProviderUserApplication.class, args);
}
}
创建 Feign 远程调用接口

在 service 包下新建 MovieServiceFeign 接口,并在接口上加上 @FeignClient 注解。并添加和要远程调用的 Movie 中的方法(确保名称和参数都一摸一样,只是不需要实现而已)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.itguigu.springcloud.service;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

import com.itguigu.springcloud.bean.Movie;

// 声明这是一个 Feign 的客户端(远程调用端)并指定远程服务地址(名称)
@FeignClient(value="CLOUD-PROVIDER-MOVIE")
public interface MovieServiceFeign {
// 要远程调用的方法。这里的方法和 Movie Controller 中的一摸一样。
@GetMapping("/getMovieById/{id}")
public Movie getMovieById(@PathVariable("id") Integer id);
}
实现调用

在 User Controller 中修改购买电影票接口,让其去调用 Feign 中配置的接口。只需要将以前 RestTemplate 中的远程调用方法更换为使用 Feign 去调用就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package com.itguigu.springcloud.controller;

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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import com.itguigu.springcloud.bean.Movie;
import com.itguigu.springcloud.bean.User;
import com.itguigu.springcloud.service.MovieServiceFeign;
import com.itguigu.springcloud.service.UserService;

@RestController
public class UserController {

@Autowired
UserService userService;

@Autowired
MovieServiceFeign moviceServiceFeign;

/**
* 获取用户信息
* @param id
* @return
*/
@GetMapping("/getUserById/{id}")
public User getUserById(@PathVariable("id") Integer id) {
return userService.getUserById(id);
}

/**
* 购买电影票
* @param userId
* @param movieId
* @return
*/
@GetMapping("/buyMovie/{userId}/{movieId}")
public Map<String, Object> buyMovie(
@PathVariable("userId") Integer userId,
@PathVariable("movieId") Integer movieId
){
HashMap<String, Object> result = new HashMap<>();
// 获取用户信息,查询自己的服务即可
User user = userService.getUserById(userId);

// 查询电影信息,需要远程调用
Movie movie = moviceServiceFeign.getMovieById(movieId);

result.put("user", user);
result.put("movie", movie);
return result;
}
}
集成 Ribbon

Feign 默认就是和 Ribbon 集成的。调用六次会发现,每个 Movie 服务都被平均的调用了两次。

熔断

当停止其中一个 Movie 服务接口的时候,继续调用 http://localhost:9001/buyMovie/1/2 时可能会出现卡顿或者中断的情况,原因就是请求发送到停止的 Movie 服务上去了。过一会儿就恢复了,原因是注册中心发现此 Movie 服务心跳没了,所以将该服务从注册中心中进行了剔除。

如果三个电影服务都挂了,那就导致了 User 服务完成不能调用 Movie 服务的情况。

熔断的作用是防止其中一个服务不可用而导致整个服务链路的雪崩,会在发生熔断的时候返回假数据。Ribbon 和 Hystrix 可以组合使用。Feign 和 Hystrix 也能组合使用。

熔断的状态转换如下:默认熔断是关闭的,当服务请求失败且达到一定的阀值的时候就会被打开,随着服务的慢慢恢复,会由半开状态慢慢的又转换为关闭状态。

Hystrix + Ribbon

添加 pom 依赖

1
2
3
4
5
<!-- 引入hystrix进行服务熔断 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

在主程序上开启断路保护功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.itguigu.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
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;

@EnableCircuitBreaker // 开启断路保护功能
@EnableDiscoveryClient
@SpringBootApplication
public class CloudProviderUserApplication {

/**
* 配置 RestTemplate bean 对象。因为这里主程序是一个配置类,所以就写在这里(并不是非得写到这里,只要是配置类就行)
* @return
*/
@Bean
@LoadBalanced // 负载均衡
public RestTemplate restTemplate() {
return new RestTemplate();
}

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

编写断路处理方法【注意⚠️:这个方法的定义要和远程调用方法的定义一摸一样,也就是除了方法名,参数,返回值什么的都需要一样,请求映射 @GetMapping 不用加】,当发生服务不能远程调用的时候,就使用断路方法的逻辑进行处理。需要在远程调用的方法上加上 @HystrixCommand 注解,并指定发生断路的时候,要调用的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
package com.itguigu.springcloud.controller;

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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import com.itguigu.springcloud.bean.Movie;
import com.itguigu.springcloud.bean.User;
import com.itguigu.springcloud.service.UserService;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;

@RestController
public class UserController {

@Autowired
UserService userService;

@Autowired
RestTemplate restTemplate;

/**
* 获取用户信息
* @param id
* @return
*/
@GetMapping("/getUserById/{id}")
public User getUserById(@PathVariable("id") Integer id) {
return userService.getUserById(id);
}

/**
* 购买电影票
* @param userId
* @param movieId
* @return
*/
@HystrixCommand(fallbackMethod = "buyMovieHystrix") // 当发生断路的时候,调用该方法进行处理
@GetMapping("/buyMovie/{userId}/{movieId}")
public Map<String, Object> buyMovie(
@PathVariable("userId") Integer userId,
@PathVariable("movieId") Integer movieId
){
HashMap<String, Object> result = new HashMap<>();
// 获取用户信息,查询自己的服务即可
User user = userService.getUserById(userId);

// 查询电影信息,需要远程调用
Movie movie = restTemplate.getForObject("http://CLOUD-PROVIDER-MOVIE/getMovieById/"+movieId, Movie.class);

result.put("user", user);
result.put("movie", movie);
return result;
}

/**
* 熔断处理逻辑(请求映射 @GetMapping 不用加)
* @param userId
* @param movieId
* @return
*/
public Map<String, Object> buyMovieHystrix(
@PathVariable("userId") Integer userId,
@PathVariable("movieId") Integer movieId
){
Map<String, Object> map = new HashMap<>();
User user = new User(-1, "无此用户");
Movie movie = new Movie(-1, "无此电影");

map.put("user", user);
map.put("movie", movie);

return map;
}
}

再次将三个 Movie 服务进行重启,然后中断其中某一个或者两个 Movie 服务,或者全部中断,访问 http://localhost:9002/buyMovie/1/2 就会看到会出现以下返回:

1
{"movie":{"id":-1,"name":"无此电影"},"user":{"id":-1,"name":"无此用户"}}

过一会儿之后,就会发现没有这种返回了,原因是被注册中心已经将该服务给去掉了。

关闭 Ribbon 重试机制,在远程服务调用不通的情况下,Ribbon 会进行重试。只需要配置文件中关闭重试即可

1
2
3
4
5
6
7
8
9
10
11
12
ribbon:
# http建立socket超时时间,毫秒
ConnectTimeout: 2000
# http读取响应socket超时时间
ReadTimeout: 10000
# 同一台实例最大重试次数,不包括首次调用
MaxAutoRetries: 0
# 重试负载均衡其他的实例最大重试次数,不包括首次server
MaxAutoRetriesNextServer: 0
# 是否所有操作都重试,POST请求注意多次提交错误。
# 默认false,设定为false的话,只有get请求会重试
OkToRetryOnAllOperations: false

Hystrix + Feign

添加 pom 依赖

1
2
3
4
5
<!-- 引入hystrix进行服务熔断 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

application.yml 中开启 Feign 对 Hystrix 支持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
spring:
application:
name: cloud-provider-user-feign

server:
port: 9001

# 指定注册到哪个注册中心
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true #注册自己服务使用ip的方式

feign:
hystrix:
enabled: true #默认false

主程序入口开启断路保护功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.itguigu.springcloud;

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

@EnableCircuitBreaker // 开启断路保护功能
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class CloudProviderUserApplication {
public static void main(String[] args) {
SpringApplication.run(CloudProviderUserApplication.class, args);
}
}

新建一个 exception 包,里面定义一个异常处理类 MovieServiceFeignException 并实现之前的远程方法调用接口 MovieServiceFeign 。重写里面的远程调用方法,返回一个虚假的 Movie 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.itguigu.springcloud.exception;

import org.springframework.stereotype.Component;

import com.itguigu.springcloud.bean.Movie;
import com.itguigu.springcloud.service.MovieServiceFeign;

@Component
public class MovieServiceFeignException implements MovieServiceFeign {
@Override
public Movie getMovieById(Integer id) {
Movie movie = new Movie(-1, "无此电影");
return movie;
}
}

在远程调用接口 MovieServiceFeign 中的 @FeignClient 装饰器上加上 fallback 回调的属性,值为刚才新增的异常处理类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.itguigu.springcloud.service;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

import com.itguigu.springcloud.bean.Movie;
import com.itguigu.springcloud.exception.MovieServiceFeignException;

// 声明这是一个 Feign 的客户端(远程调用端)并指定远程服务地址(名称)。指定断路异常处理类
@FeignClient(value="CLOUD-PROVIDER-MOVIE", fallback = MovieServiceFeignException.class)
public interface MovieServiceFeign {
// 要远程调用的方法。这里的方法和 Movie Controller 中的一摸一样。
@GetMapping("/getMovieById/{id}")
public Movie getMovieById(@PathVariable("id") Integer id);
}

做同样测试,再次将三个 Movie 服务进行重启,然后中断其中某一个或者两个 Movie 服务,或者全部中断,访问 http://localhost:9001/buyMovie/1/2【注意,这里 9001 是演示 Hystrix + Feign 的地址,之前的 9002 是演示 Hystrix + Ribbon 的地址 】 就会看到会出现以下返回:

1
{"movie":{"id":-1,"name":"无此电影"},"user":{"id":-1,"name":"无此用户"}}

过一会儿之后,就会发现没有这种返回了,原因是被注册中心已经将该服务给去掉了。

注意⚠️:这里的熔断是加在用户端的,也就是说当电影服务出现问题的时候,默认执行的是在用户端设置的断路处理逻辑,即返回的是虚假的数据。所以哪怕是电影服务全部挂掉之后,用户服务调用失败也会有返回值,因为这个断路处理逻辑是在用户端加上的。

Hystrix Dashboard

Hystrix 提供了近实时的监控,Hystrix 会实时、累加地记录所有关于 HystrixCommand 的执行信息,包括每秒执行多少请求,多少成功,多少失败等。Netflix 通过 hystrix-metrics-event-stream 项目实现了对以上指标的监控。

spring-boot-starter-actuator 为 SpringBoot 提供了监控,以上面的使用 Feign 的项目为例子来做演示。在 pom 文件中加上以下依赖:

1
2
3
4
5
<!-- 引入 actuator 监控 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

直接启动 cloud-provider-user-feign 项目,访问 http://localhost:9001/actuator/health 就能获得当前项目的运行状态:

1
{"status":"UP"}

还能查看其他更多信息:

此时如果我们想用 Hystrix 的 Dashboard 的话,还需修改配置文件,暴露出数据监控流。在 application.yml 中加入以下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
spring:
application:
name: cloud-provider-user-feign

server:
port: 9001

# 指定注册到哪个注册中心
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true #注册自己服务使用ip的方式

feign:
hystrix:
enabled: true #默认false

# 暴露 Hystrix 监控数据流
management:
endpoints:
web:
exposure:
include: hystrix.stream # 访问/actuator/hystrix.stream 能看到不断更新的监控流

访问 http://localhost:9001/actuator/hystrix.stream 地址能看到页面输出一些 ping 信息,但是不能看数据的详细情况。此时,我们需要引入 HystrixDashboard,在 pom 文件中加入以下依赖:

1
2
3
4
5
<!-- 引入 hystrix-dashboard -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>

在主程序上加入 @EnableHystrixDashboard 注解,开启可视化监控功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.itguigu.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@EnableHystrixDashboard // 开启可视化监控
@EnableCircuitBreaker // 开启断路保护功能
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class CloudProviderUserApplication {
public static void main(String[] args) {
SpringApplication.run(CloudProviderUserApplication.class, args);
}
}

重启项目后访问 dashboard 监控地址 http://localhost:9001/hystrix,并填入之前的监控流地址 http://localhost:9001/actuator/hystrix.stream 进入后就可能看到监控页面。

编写一个shell 脚本,让它一直请求购买电影的服务接口 http://localhost:9001/buyMovie/1/2

1
2
3
while true;         
do curl http://localhost:9001/buyMovie/1/2;
done;

可以看到服务是正常提供访问的,且熔断是关闭的。然后慢慢的一个一个的关闭 Movie 服务,当 Movie 服务全部关闭后,熔断被打开。再次恢复一个 Movie 服务,就会看到熔断又被关闭了,且服务在慢慢恢复。

服务正常

服务熔断

项目地址