Spring Cloud Gateway 使用 Redis 限流使用教程

从本文开始,笔者将总结 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、环境准备

2、项目创建

3、测试限流

4、改进限流返回

5、项目代码


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

至此完

相关推荐
喵叔哟1 分钟前
重构代码之移动字段
java·数据库·重构
喵叔哟1 分钟前
重构代码之取消临时字段
java·前端·重构
fa_lsyk3 分钟前
maven环境搭建
java·maven
Daniel 大东23 分钟前
idea 解决缓存损坏问题
java·缓存·intellij-idea
喵叔哟25 分钟前
【.NET 8 实战--孢子记账--从单体到微服务】--简易权限--访问权限中间件
微服务·中间件·.net
huaqianzkh26 分钟前
了解Hadoop:大数据处理的核心框架
大数据·hadoop·分布式
wind瑞29 分钟前
IntelliJ IDEA插件开发-代码补全插件入门开发
java·ide·intellij-idea
HappyAcmen29 分钟前
IDEA部署AI代写插件
java·人工智能·intellij-idea
马剑威(威哥爱编程)35 分钟前
读写锁分离设计模式详解
java·设计模式·java-ee
鸽鸽程序猿35 分钟前
【算法】【优选算法】前缀和(上)
java·算法·前缀和