五、服务降级和熔断
本节将引入熔断器Hystrix。首先来看一下什么是雪崩效应:
服务雪崩效应是一种因“服务提供者的不可用”(原因)导致“服务调用者不可用”(结果),并将不可用逐渐放大的现象
举个栗子:假设,order-server请求goods-server时,由于某些原因,goods-server返回时间变为无限长,此时order-server也将一直等待响应,当order-server堆积了大量处于等待状态的请求时,order-server服务器终将撒手人寰。
这时就需要Hystrix登场,在服务出现问题时它会及时的去切断有问题的服务器,保证系统的基本秩序。
开刀:
订单服务加入hystrix依赖:
<?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 https://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.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.mujio</groupId>
<artifactId>mall</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>mall</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- 熔断所需依赖 start -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
<version>1.4.0.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-javanica</artifactId>
</dependency>
<!-- 熔断所需依赖 end -->
<!-- 添加web服务依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MySQL数据库依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.41</version>
</dependency>
<!-- mybatis依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>
<!-- springboot项目需要的依赖,创建项目自动添加 -->
<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>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
启动类OrderServerApplication加上开启hystrix的注解@EnableHystrix:
最后需要改造一下订单服务中的GoodService:
package com.mujio.orderserver.service;
import com.mujio.orderserver.entity.Goods;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
public class GoodService {
//利用RestTemplate请求接口
@Autowired
private RestTemplate restTemplate;
//启用负载均衡后,restTemplate自动选择访问哪个服务
@HystrixCommand(fallbackMethod = "getGoodsServiceOffline")
public Goods getGoods(int id){
String serverName = "goods-server";
String url = "http://" + serverName + "/goods/" + id;
return restTemplate.getForObject(url,Goods.class);
}
//请求失败后,调用fallbackMethod指定的方法
public Goods getGoodsServiceOffline(int id){
Goods goods = new Goods();
goods.setId(0);
goods.setName("未能获取到商品信息");
goods.setPrice("");
return goods;
}
/*
// @Autowired
// private DiscoveryClient discoveryClient;
public Goods getGoods(int id) {
String serverName = "goods-server";
String url = "http://" + serverName + "/goods/" + id;
System.out.println(url);
return restTemplate.getForObject(url,Goods.class);
}
*/
/* public Goods getGoods(int id) {
String service = "goods-server";
List<ServiceInstance> instances = discoveryClient.getInstances(service);
if (instances.isEmpty()) {
return null;
}
//instances.get(0)使用获取到的第一个服务
String url = "http://" + instances.get(0).getHost() + ":" + instances.get(0).getPort() + "/goods/" + id;
return restTemplate.getForObject(url, Goods.class);
}*/
/* public Goods getGoods(int id){
String url = "http://localhost:8000/goods/" + id;
Goods goods = new Goods();
goods.setId(0);
goods.setName("未能获取到商品信息");
goods.setPrice("");
Goods g;
try {
g = restTemplate.getForObject(url, Goods.class);
} finally {
g = goods;
return g;
}
}*/
}
重启服务,访问http://localhost:9000/order/4并多次刷新,信息正常;停止goods-server01,再次访问,多次刷新:
可以看到,刚停止时goods-server01时,返回速度明显慢了一些,并且在关闭后访问会显示“未能获取到商品信息”,但是多次访问刷新之后又会发现信息又正常了。这是因为熔断器发挥了作用,在服务出现故障的时候调用了fallbackMethod,并且及时切断该服务,使得再刷新后数据恢复正常。
目前我们已经初步实现了分布式的目的,但是仔细一想,这个系统还是有问题:eureka要是崩了咋办?开启两个order-server有个锤子用?访问的时候不还是得区分端口?还有这个服务调用藏在service中会不会有点太深了?
当然哈,只是觉得这一节篇幅有点短了,不如现在强行加点:
Feign:
feign是声明式的web service客户端,Spring Cloud集成了Ribbon和Eureka,可在使用Feign提供负载均衡的http客户端。
瞧瞧这说法,“可使用Feign提供负载均衡的http客户端”,怎么提供呢?一步一步来:
加依赖(负载均衡在order-server中,当然也是给order-server的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 https://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>
<!-- springboot版本改为2.1.1.RELEASE -->
<version>2.1.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.mujio</groupId>
<artifactId>order-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>order-server</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.RC2</spring-cloud.version>
</properties>
<dependencies>
<!-- Feign所需依赖 start -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- 根据spring cloud的版本选择,早期的使用下面的依赖 -->
<!--<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>-->
<!-- Feign所需依赖 end -->
<!-- 熔断所需依赖 start -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
<version>1.4.0.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-javanica</artifactId>
</dependency>
<!-- 熔断所需依赖 end -->
<!-- 添加web服务依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MySQL数据库依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.41</version>
</dependency>
<!-- mybatis依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>
<!-- springboot项目需要的依赖,创建项目自动添加 -->
<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>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 添加eureka依赖 start -->
<!-- eureka客户端依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<exclusions>
<!-- eureka的数据转换,自动将数据结果转为xml格式,我们不需要xml格式的结果所以需要排除 -->
<exclusion>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<!-- 指定springcloud依赖版本 -->
<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>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<!-- 添加eureka依赖 end -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
需要注意的是,本文中使用的spring cloud的版本是:Greenwich.RC2,加入feign时如出现冲突,比其早的版本,请考虑使用spring-cloud-starter-feign
继续改造,首先是启动类加注解@EnableFeignClients:
package com.mujio.orderserver;
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 OrderServerApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServerApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
创建个使用feign的接口:
这里有个@FeignClient(value = “goods-server”),它会根据value值自动转换成对应的服务,所以要注意商品服务的应用名要与之一致。
它有点类似与controller根据请求路径调用对应service方法,所以我是放在了controller包中。
再次改造GoodService(刀妹??):
package com.mujio.orderserver.service;
import com.mujio.orderserver.controller.GoodsFeignClient;
import com.mujio.orderserver.entity.Goods;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class GoodService {
// 引入feign
@Autowired
private GoodsFeignClient goodsFeignClient;
//启用负载均衡后,有restTemplate自己去选择访问那个服务
@HystrixCommand(fallbackMethod = "getGoodsServiceOffline")
public Goods getGoods(int id){
return goodsFeignClient.getGoods(id);
}
public Goods getGoodsServiceOffline(int id){
Goods goods = new Goods();
goods.setId(0);
goods.setName("未能获取到商品信息");
goods.setPrice("");
return goods;
}
/*
@Autowired
private RestTemplate restTemplate;
//启用负载均衡后,restTemplate自动选择访问哪个服务
@HystrixCommand(fallbackMethod = "getGoodsServiceOffline")
public Goods getGoods(int id){
String serverName = "goods-server";
String url = "http://" + serverName + "/goods/" + id;
return restTemplate.getForObject(url,Goods.class);
}
//请求失败后,调用fallbackMethod指定的方法
public Goods getGoodsServiceOffline(int id){
Goods goods = new Goods();
goods.setId(0);
goods.setName("未能获取到商品信息");
goods.setPrice("");
return goods;
}
*/
/*
// @Autowired
// private DiscoveryClient discoveryClient;
public Goods getGoods(int id) {
String serverName = "goods-server";
String url = "http://" + serverName + "/goods/" + id;
System.out.println(url);
return restTemplate.getForObject(url,Goods.class);
}
*/
/* public Goods getGoods(int id) {
String service = "goods-server";
List<ServiceInstance> instances = discoveryClient.getInstances(service);
if (instances.isEmpty()) {
return null;
}
//instances.get(0)使用获取到的第一个服务
String url = "http://" + instances.get(0).getHost() + ":" + instances.get(0).getPort() + "/goods/" + id;
return restTemplate.getForObject(url, Goods.class);
}*/
/* public Goods getGoods(int id){
String url = "http://localhost:8000/goods/" + id;
Goods goods = new Goods();
goods.setId(0);
goods.setName("未能获取到商品信息");
goods.setPrice("");
Goods g;
try {
g = restTemplate.getForObject(url, Goods.class);
} finally {
g = goods;
return g;
}
}*/
}
搞完重启服务,测试啥的自然不用说了。下一节将引入zuul,顺便解决另外两个问题。(主要是因为本节代码已经上传了,嘿嘿)