从本文开始,笔者将总结 spring cloud 相关内容的教程
版本选择
为了适应 java8,笔者选择了下面的版本,后续会出 java17的以SpringBoot3.0.X为主的教程
SpringBoot 版本 2.6.5
SpringCloud 版本 2021.0.1
SpringCloudAlibaba 版本 2021.0.1.0
SpringCloudAlibaba github 版本说明截图
SpringCloud 官网:https://spring.io/projects/spring-cloud
Spring Cloud Alibaba 官网:https://sca.aliyun.com/zh-cn/
目录
1、环境准备
本文讲解Spring Cloud Gateway 使用 Redis 限流,注册中心使用 Nacos
Macos 官网:https://nacos.io/zh-cn/index.html
Nacos 安装这里不做过多介绍,不了解的朋友可以参考
Nacos 单机安装:https://blog.csdn.net/wsjzzcbq/article/details/123916233
Nacos 集群安装:https://blog.csdn.net/wsjzzcbq/article/details/123956116
笔者使用 docker 开启 nacos 和 redis
Spring Cloud Alibaba 2021.0.1.0 版本对应的nacos版本是 1.4.2
笔者使用的 Naocs 版本是 2.0.0-bugfix,redis 版本是 7.0.6
2、项目创建
新建 maven 聚合项目 cloud-learn
最外层父工程 cloud-learn 的 pom.xml
html
<?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>
<groupId>com.wsjzzcbq</groupId>
<artifactId>cloud-learn</artifactId>
<version>1.0-SNAPSHOT</version>
<modules>
<module>gateway-learn</module>
<module>consumer-learn</module>
</modules>
<packaging>pom</packaging>
<repositories>
<repository>
<id>naxus-aliyun</id>
<name>naxus-aliyun</name>
<url>https://maven.aliyun.com/repository/public</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.5</version>
<relativePath/>
</parent>
<properties>
<spring-cloud.version>2021.0.1</spring-cloud.version>
<spring-cloud-alibaba.version>2021.0.1.0</spring-cloud-alibaba.version>
<alibaba-nacos-discovery.veriosn>2021.1</alibaba-nacos-discovery.veriosn>
<alibaba-nacos-config.version>2021.1</alibaba-nacos-config.version>
<spring-cloud-starter-bootstrap.version>3.1.1</spring-cloud-starter-bootstrap.version>
</properties>
<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>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>${alibaba-nacos-discovery.veriosn}</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>${alibaba-nacos-config.version}</version>
</dependency>
<!--spring-cloud-dependencies 2020.0.0 版本不在默认加载bootstrap文件,如果需要加载bootstrap文件需要手动添加依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
<version>${spring-cloud-starter-bootstrap.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.40</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
然后创建2个子工程 consumer-learn 和 gateway-learn
gateway-learn 中配置路由转发到 consumer-learn
consumer-learn 工程 pom.xml
html
<?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">
<parent>
<artifactId>cloud-learn</artifactId>
<groupId>com.wsjzzcbq</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>consumer-learn</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--feign负载均衡-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
consumer-learn 工程 启动类
java
package com.wsjzzcbq;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* ConsumerApplication
*
* @author wsjz
* @date 2023/09/17
*/
@EnableFeignClients
@SpringBootApplication
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
}
consumer-learn 工程 配置文件
java
spring.application.name=consumer-learn
server.port=8081
spring.cloud.nacos.discovery.username=nacos
spring.cloud.nacos.discovery.password=nacos
spring.cloud.nacos.discovery.server-addr=192.168.31.152:8848
spring.cloud.nacos.discovery.namespace=public
logging.level.com.alibaba.cloudlearnconsumer.feign.ProducerService=DEBUG
consumer-learn 工程 controller
java
package com.wsjzzcbq.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* ConsumerController
*
* @author wsjz
* @date 2023/09/17
*/
@RestController
public class ConsumerController {
@RequestMapping("/name")
public String user() {
return "宝剑锋从磨砺出,梅花香自苦寒来";
}
}
gateway-learn
gateway-learn 工程 pom.xml
html
<?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">
<parent>
<artifactId>cloud-learn</artifactId>
<groupId>com.wsjzzcbq</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>gateway-learn</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- gateway负载均衡需要下面依赖,不添加会报错503-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!--redis使用较高版本,5以上版本,不要使用windows版redis,低版本redis不支持lua中的命令-->
<!--https://blog.csdn.net/wxxiangge/article/details/95024214/-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
gateway-learn 工程 启动类
java
package com.wsjzzcbq;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* GatewayApplication
*
* @author wsjz
* @date 2023/09/17
*/
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
限流需要实现 KeyResolver 接口的 resolve 方法
在 resolve 方法中返回限流的维度,如请求路径、ip地址、请求参数等
笔者这里限流维度是请求路径
java
package com.wsjzzcbq.limit;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* ApiKeyResolver
*
* @author wsjz
* @date 2023/09/17
*/
@Component
public class ApiKeyResolver implements KeyResolver {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
System.out.println("API限流: " + exchange.getRequest().getPath().value());
return Mono.just(exchange.getRequest().getPath().value());
}
}
gateway-learn 工程 配置文件
bash
server:
port: 9000
spring:
application:
name: gateway-learn
redis:
host: 192.168.31.152
password: 123456
timeout: 5000
database: 0
cloud:
nacos:
discovery:
server-addr: http://192.168.31.152:8848
gateway:
routes:
- id: consumer-learn
uri: lb://consumer-learn
predicates:
- Path=/cloudlearn/consumer/**
filters:
- name: RequestRateLimiter
args:
key-resolver: "#{@apiKeyResolver}"
redis-rate-limiter.replenishRate: 1 #生成令牌速率:个/秒
redis-rate-limiter.burstCapacity: 2 #令牌桶容量
redis-rate-limiter.requestedTokens: 1 #每次消费的Token数量,默认是 1
- StripPrefix=2
配置说明:
RequestRateLimiter 是 gateway 提供的限流过滤器
#{@apiKeyResolver} 是笔者实现的 ApiKeyResolver
redis-rate-limiter.replenishRate 生成令牌的速率每秒几个
redis-rate-limiter.burstCapacity 令牌桶容量
redis-rate-limiter.requestedTokens 每次消费的令牌数量
当请求到网关以 /cloudlearn/consumer/ 为开头前缀时,会路由到 consumer-learn 服务上
创建完成的项目结构
3、测试限流
分别启动 consumer-learn 和 gateway-learn
登录 Nacos 控制台查看
启动成功后,可在Naocs 控制台查看注册服务信息
浏览器访问测试限流:http://localhost:9000/cloudlearn/consumer/name
运行效果
可以看到当1秒钟内请求超过2次时会被限流
4、改进限流返回
上面代码实现了限流,但限流触发后返回的是429,不利于前端处理,这里我们可以在默认的限流过滤器基础上进行改进,自定义限流时的返回
新建 NewRequestRateLimiterGatewayFilterFactory 类
继承默认的 RequestRateLimiterGatewayFilterFactory
java
package com.wsjzzcbq.filter;
import com.alibaba.fastjson2.JSONObject;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.cloud.gateway.filter.ratelimit.RateLimiter;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.support.HttpStatusHolder;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Map;
/**
* NewRequestRateLimiterGatewayFilterFactory
*
* @author wsjz
* @date 2023/09/17
*/
@Component
public class NewRequestRateLimiterGatewayFilterFactory extends RequestRateLimiterGatewayFilterFactory {
private final RateLimiter defaultRateLimiter;
private final KeyResolver defaultKeyResolver;
private boolean denyEmptyKey = true;
private String emptyKeyStatusCode = HttpStatus.FORBIDDEN.name();
public NewRequestRateLimiterGatewayFilterFactory(RateLimiter defaultRateLimiter, KeyResolver defaultKeyResolver) {
super(defaultRateLimiter, defaultKeyResolver);
this.defaultRateLimiter = defaultRateLimiter;
this.defaultKeyResolver = defaultKeyResolver;
}
@Override
public GatewayFilter apply(Config config) {
System.out.println("过滤限流");
KeyResolver resolver = (KeyResolver)this.getOrDefault(config.getKeyResolver(), this.defaultKeyResolver);
RateLimiter<Object> limiter = (RateLimiter)this.getOrDefault(config.getRateLimiter(), this.defaultRateLimiter);
boolean denyEmpty = (Boolean)this.getOrDefault(config.getDenyEmptyKey(), this.denyEmptyKey);
HttpStatusHolder emptyKeyStatus = HttpStatusHolder.parse((String)this.getOrDefault(config.getEmptyKeyStatus(), this.emptyKeyStatusCode));
return (exchange, chain) -> {
return resolver.resolve(exchange).defaultIfEmpty("____EMPTY_KEY__").flatMap((key) -> {
if ("____EMPTY_KEY__".equals(key)) {
if (denyEmpty) {
ServerWebExchangeUtils.setResponseStatus(exchange, emptyKeyStatus);
return exchange.getResponse().setComplete();
} else {
return chain.filter(exchange);
}
} else {
String routeId = config.getRouteId();
if (routeId == null) {
Route route = (Route)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
routeId = route.getId();
}
return limiter.isAllowed(routeId, key).flatMap((response) -> {
Iterator var4 = response.getHeaders().entrySet().iterator();
while(var4.hasNext()) {
Map.Entry<String, String> header = (Map.Entry)var4.next();
exchange.getResponse().getHeaders().add((String)header.getKey(), (String)header.getValue());
}
if (response.isAllowed()) {
return chain.filter(exchange);
} else {
ServerHttpResponse httpResponse = exchange.getResponse();
httpResponse.getHeaders().set("Content-Type", "application/json");
JSONObject json = new JSONObject();
json.put("code", 0);
json.put("msg", "当前请求人数较多,请稍后再访问");
DataBuffer dataBuffer = httpResponse.bufferFactory().wrap(json.toJSONString().getBytes(StandardCharsets.UTF_8));
return httpResponse.writeWith(Mono.just(dataBuffer));
}
});
}
});
};
}
private <T> T getOrDefault(T configValue, T defaultValue) {
return configValue != null ? configValue : defaultValue;
}
}
修改配置文件
配置我们自定义的限流过滤器
bash
server:
port: 9000
spring:
application:
name: gateway-learn
redis:
host: 192.168.31.152
password: 123456
timeout: 5000
database: 0
cloud:
nacos:
discovery:
server-addr: http://192.168.31.152:8848
gateway:
routes:
- id: consumer-learn
uri: lb://consumer-learn
predicates:
- Path=/cloudlearn/consumer/**
filters:
- name: NewRequestRateLimiter
args:
key-resolver: "#{@apiKeyResolver}"
redis-rate-limiter.replenishRate: 1 #生成令牌速率:个/秒
redis-rate-limiter.burstCapacity: 2 #令牌桶容量
redis-rate-limiter.requestedTokens: 1 #每次消费的Token数量
- StripPrefix=2
重新启动 gateway-learn
请求测试
5、项目代码
码云地址:https://gitee.com/wsjzzcbq/csdn-blog/tree/master/cloud-learn
至此完