熔断降级:我们在微服务系统中,所有请求先经网关,由网关再去找指定的服务,这个时候肯定是会出现服务异常和超时的情况,我们就针对这种情况进行熔断降级操作。
限流:限制每秒的最大访问次数和允许在一秒钟内完成的最大请求数
1.搭建基础的springboot工程,导入依赖,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>
<version>2.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for gateway</description>
<dependencies>
<!--nacos 注入依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
<!--Spring Cloud Gateway默认集成了Redis限流-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>2.2.3.RELEASE</version>
</dependency>
<!--限流-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
<!--熔断 降级-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>2.2.3.RELEASE</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
我们这边没有再去nacos读取配置文件,所以只引用了nacos的discovery包,微服务的话最好还是把配置文件配到nacos,这样修改配置文件的时候就不需要再重新打包部署,直接重启服务就行了。
2.开启nacos的注解
@SpringBootApplication
@EnableDiscoveryClient
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
3.新建一个HystrixController,主要作用就是当服务异常或超时的时候,走这边我们配置的接口,第五步配置文件中会用到
package com.example.demo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* 默认降级处理
*/
@RestController
public class HystrixController {
@RequestMapping("/hystrix")
public Map<String, String> defaultfallback() {
System.out.println("降级操作...");
Map<String, String> map = new HashMap<>();
map.put("code", "500");
map.put("message", "服务异常");
return map;
}
}
4. 创建bootstrap.yml
nacos地址和namespace都沿用的上一篇文章里介绍的,这里就不再介绍了
spring:
application:
name: cloud-gateway
cloud:
nacos:
discovery:
server-addr: 192.168.1.54:8848 # nacos地址
namespace: MaoJiaFeng
logging:
level:
com:
alibaba:
nacos:
client: error
5.配置application.yml
这里是需要配置redis参数的,注意这边配置文件中主要就是看filters,一个是熔断降级,一个就是限流,限流的配置文件需要先注释掉才能启动成功,因为我们还没写限流的配置文件,把代码22-27行注释掉即可
server:
port: 9527
spring:
redis:
host: 192.168.1.52
port: 6379
password: KCl9HfqbVnhQ5c3n
database: 15
cloud:
gateway:
routes:
- id: springboot-api # 名字可以随便写
uri: lb://springboot-api # lb 负载均衡 这边必须是微服务名称
predicates: # 断言 这是一个 Java 8 的 Predicate,可以使用它来匹配来自 HTTP 请求的任何内容,例如 headers 或参数。断言的输入类型是一个 ServerWebExchange
- Path=/test/** # 以/test开头的所有请求 都会转发到 lb://springboot-api
filters:
- name: Hystrix
args:
name: default
fallbackUri: 'forward:/hystrix' # /hystrix 路径是我们在HystrixController自己写的
- name: RequestRateLimiter
args:
name: default
key-resolver: "#{@remoteAddrKeyResolver}" #SPEL表达式去的对应的bean
redis-rate-limiter.replenishRate: 1 # 每秒最大访问次数
redis-rate-limiter.burstCapacity: 5 #令牌桶的容量,允许在一秒钟内完成的最大请求数
# hystrix 信号量隔离,3秒后自动超时
hystrix:
command:
default:
execution:
isolation:
strategy: SEMAPHORE
thread:
timeoutInMilliseconds: 3000
shareSecurityContext: true
6.注意我们上一步的配置文件中hystrix的延时是3S,所以gateway在向服务发送请求后,3S内得不到响应就会走我们自己配置的HystrixController中的接口,现在我们启动网关,注意端口是:9527
7.上面我们是把网关启动了,现在要启动服务,服务的话我就用上篇文章的代码了,大家也可以新建一个springboot基础模板注册到nacos就行,新增一个IndexController即可
package com.example.demo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/test")
public class IndexController {
@RequestMapping("/hello")
public String hello(){
return "hello,gateway";
}
/**
* 让线程睡眠5秒 触发hystrix
* @return
*/
@RequestMapping("/timeout")
public String timeout(){
try{
Thread.sleep(5000);
}catch (Exception e){
e.printStackTrace();
}
return "请求超时";
}
}
这边主要有两个接口,一个是直接返回结果,另一个延时了5S才返回结果,现在我们也启动起来,注意是通过网关端口号访问
请求 /hello的接口,返回正常
现在我们访问 /timeout的接口,注意这边的几个要点
1.我们的服务端口是8088,但是我们访问的却是9527,这是微服务在起的作用
2.上面的服务中我们返回的是”服务超时“,实际网页里返回的是**{“code”:“500”,“message”:“服务异常”}**,这就是网关在起作用了,因为这个接口的请求5S才能得到响应,而我们配置的网关3S得不到响应就会自动返回HystrixController中接口的信息。这一步就是我们所说的熔断降级
8.现在我们验证限流
1.新建一个RateLimiterConfig类
package com.example.demo.config;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;
/**
* 路由限流配置
* @author MaoJiaFeng
* @date 2020/11/14
*/
@Configuration
public class RateLimiterConfig {
/**
* IP限流代码
* @return
*/
@Bean()
public KeyResolver remoteAddrKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
}
}
2.理论上代码到这就可以了,我们可以查看一下服务是否正常启动,但是我们需要一个高并发的请求来测试我们的限流,注意postman是串行的,并不能做到并发测试,所以我们还是通过自己写代码实现,这个我就直接写在gateway项目里了,之后会一起上传到码云
先写一个能发送http的util
package com.example.demo.util;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
public class HttpClientOp {
public static String doGet(String httpurl) {
HttpURLConnection connection = null;
InputStream is = null;
BufferedReader br = null;
String result = null;// 返回结果字符串
try {
// 创建远程url连接对象
URL url = new URL(httpurl);
// 通过远程url连接对象打开一个连接,强转成httpURLConnection类
connection = (HttpURLConnection) url.openConnection();
// 设置连接方式:get
connection.setRequestMethod("GET");
// 设置连接主机服务器的超时时间:15000毫秒
connection.setConnectTimeout(15000);
// 设置读取远程返回的数据时间:60000毫秒
connection.setReadTimeout(60000);
// 发送请求
connection.connect();
// 通过connection连接,获取输入流
if (connection.getResponseCode() == 200) {
is = connection.getInputStream();
/* 封装输入流is,并指定字符集 */
br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
// 存放数据
StringBuffer sbf = new StringBuffer();
String temp = null;
while ((temp = br.readLine()) != null) {
sbf.append(temp);
sbf.append("\r\n");
}
result = sbf.toString();
}
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭资源
if (null != br) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != is) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
connection.disconnect();// 关闭远程连接
}
return result;
}
}
下面就是写我们测试并发的代码了
package com.example.demo.test;
import com.example.demo.util.HttpClientOp;
import java.util.concurrent.CountDownLatch;
public class LatchTest {
public static void main(String[] args) throws InterruptedException {
Runnable taskTemp = new Runnable() {
// 注意,此处是非线程安全的
private int iCounter;
@Override
public void run() {
for (int i = 0; i < 10; i++) {
// 发起请求
System.out.println(HttpClientOp.doGet("http://localhost:9527/test/hello"));
iCounter++;
System.out.println(System.nanoTime() + " [" + Thread.currentThread().getName() + "] iCounter = " + iCounter);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
LatchTest latchTest = new LatchTest();
latchTest.startTaskAllInOnce(5, taskTemp);
}
public long startTaskAllInOnce(int threadNums, final Runnable task) throws InterruptedException {
final CountDownLatch startGate = new CountDownLatch(1);
final CountDownLatch endGate = new CountDownLatch(threadNums);
for (int i = 0; i < threadNums; i++) {
Thread t = new Thread() {
public void run() {
try {
// 使线程在此等待,当开始门打开时,一起涌入门中
startGate.await();
try {
task.run();
} finally {
// 将结束门减1,减到0时,就可以开启结束门了
endGate.countDown();
}
} catch (InterruptedException ie) {
ie.printStackTrace();
}
}
};
t.start();
}
long startTime = System.nanoTime();
System.out.println(startTime + " [" + Thread.currentThread() + "] All thread is ready, concurrent going...");
// 因开启门只需一个开关,所以立马就开启开始门
startGate.countDown();
// 等等结束门开启
endGate.await();
long endTime = System.nanoTime();
System.out.println(endTime + " [" + Thread.currentThread() + "] All thread is completed.");
return endTime - startTime;
}
}
启动我们的main()方法,注意我们配置的是每秒最大访问是1,令牌桶最大容量是5,通过控制台我们可以看到只有前五个请求成功了,下面的请求全部被网关拒绝了