Skip to content

🚀 第9章 分布式网关——基于Spring Cloud Gateway

随着 Saas 付费软件的流行,企业在达到一定的规模后都会对外开放自身的 API,以实现技术变现,或增加企业在自身行业内的技术影响力。开放 API需要有一个统一对接商户的业务网关,以处理不同商户的请求,并将请求路由到内部的微服务

在企业实施业务中台战略后,公司的 App 产品会百花齐放。因为 App 产品需要统一调用后端的 API服务,所以,开发人员在暴露后端的 API给 App 时需要考虑很多非功能设计,其中,路由和流量控制就是其中比较重要的功能。

在引入业务网关后,可以将这些非功能设计从业务服务中统一抽取到网关,这样可以解耦后端的功能性和非功能性需求,让业务人员更加专注于理解自身的业务,以及按时地交付的业务功能。

Spring Cloud Gateway 作为开源社区比较稳定和高性能的业务网关,已经被很多公司在业务的生产环境中使用。

🚀 9.1 认识网关

在分析业务网关 Spring Cloud Gateway 的原理前,先来认识一下网关。

🚀 9.1.1 什么是网关

开发人员可以从两个方面来理解网关。

1.从字面含义的角度去理解

网关就是一个“关卡”,出入这个“关卡”的是外部的流量请求及其响应。如图 9-1所示,网关作为“关卡”处理出入流量,并请求实际的资源,返回资源响应的结果。

很多开发人员可能会认为网关和微服务是强相关的,其实网关并不是微服务独有的。

2.从软件分层架构的角度去理解

网关是位于客户端与其依赖的服务之间的一个层,有时它也被称为“反向代理”。如图 9-2 所示,网关被作为从客户端到其服务的单一入口点

智能终端、移动应用和 loT设备连接业务网关,访问基于RESTful API微服务,再通过 RPC 框架访问基于 Dubbo 的微服务。

🚀 9.1.2 为什么需要网关

在微服务架构中,开发人员引入业务网关,主要是为了解决一些非功能性的问题,如图9-3所示。

在图 9-3中,开发人员可以将业务网关当作一个黑盒子:只需要按照约定的规则将应用接入业务网关,应用对应的 API就可以暴露给第三方调用者。

1.网关具体解决的哪些非功能性问题

网关具体解决以下非功能性问题:

  • 业务网关将外部公共 API与内部微服务 API进行隔离。内部服务会持续选代更新,但不会影响调用外部公共 API 的客户端。业务网关为微服务提供统一的入口点,向客户屏蔽微服务的服务治理的和版本控制的技术细节
  • 业务网关能够统一地管理微服务的访问权限,解决微服务调用的安全性问题
  • 业务网关通常都支持多种通信协议,这样客户端可以使用外部 API (基于 HTTP 的 API 或者 RESTfuI API) 完成和网关的 RPC 通信。在业务网关与内部微服务之间可以采用不同的通信协议,比如 ProtoBuf或 AMQP,当然最常用的 RPC 协议是 RESTful和 Dubbo协议。业务网关可以跨这些不同的协议,提供一个外部的、统一的基于RESTful的 API 完成内部微服务 API的暴露,并允许开发人员选择不同的内部通信协议来调用该 API
  • 业务网关能够降低开发人员实施和落地微服务的技术复杂度。在业务网关中,开发人员可以统一地治理业务 API,包括服务隔离、降级和统一认证等
  • 从业务中台的角度来分析,业务网关解决了不同产品线的业务功能,在线上强耦合部署的问题。在企业落地业务中台的过程中,必然会拆“烟囱”,将通用的业务功能下沉到业务中台,从而在上层会孵化出很多 API 产品及对应的 App 客户端,通过业务网关可以将不同 API 产品和对应的 App 客户端的流完全隔离,但下层已经拆分的微服务是无感知的

2.一个自研业务网关实例

图 9-4 是一个完整的自研的业务网关。

该网关需要具备的功能说明如下:

  • 接入层需要具备的功能主要包括:流量控制、黑/白名单、路由、负载均衡、长短连接及容灾切换等。
  • 分发层需要具备数据校验功能主要包括:时间校验、方法校验、版本校验、AppKey 校验、签名校验等。
  • 业务网关需要具备的功能包括:泛化调用、线程池隔离、熔断器、通信协议适配、消息中心、服务降级和监控与告警等。其中,泛化调用的主要应用场景是业务网关对接 Dubbo API,业务网关需要使用 Dubbo 的泛化调用技术屏蔽掉对业务的 Dubbo API的 Jar 包的强依赖(即在业务上线新功能后,需要调用方变更Jar 包依赖关系才能生效)。业务网关需要具备在不重启服务的前提下,让外部客户端能够使用业务最新的业务功能
  • 如果是自研网关,则还需要统一的鉴权服务和统一的用户中心服务
  • 业务网关需要有自己独立的数据中心,用于存储在网关运行后需要持久化的一些非功能性数据。
  • 业务网关要支持注册中心,以保证自身服务的高可用性和利用业务 API的服务治理能力,增加自身服务路由的功能。
  • 业务网关要支持配置中心,以确保网关配置信息的高可用,以及动态地更新指定的配置信息。
  • 业务网关要具备“沙箱”环境,主要用于核心功能的功能测试。比如,要将支付接口开发给商户,商户按照接入文档,利用 SDK 将应用接入支付系统,但是,商户需要测试一下支付接口,确保能够跑通线上的支付业务。业务网关作为统一的入口,需要将“沙箱”环境当作一个稳定的产品来维护,以确保商户能够随时随地地调用,并保证服务接口的可用性。

🚀 9.1.3 认识Spring Cloud Gateway

Spring Cloud Gateway 是 Spring Cloud 提供的高性能网关的技术解决方案,它是基于Spring Framework 5.0 和 Spring Boot 2.0 构建的 API 网关,它的主要特性如下:

  • JDK 要求最低版本为 1.8;
  • 支持 Spring Framework 5.0;
  • 支持动态路由;
  • 用过滤器链来拦截业务网关的 API 流量请求;
  • 支持配置中心和注册中心;
  • 能够集成 Hystrix 断路器;
  • 支持基于“Redis+Lua”的分布式流量控制。

Spring Cloud Gateway 主要提供路由、断言和过滤的功能

1.路由

路由是网关的基本组件,它由 ID、目标 URI、谓词集合和过滤器集合定义。如果聚合谓词为tnue,则匹配路由。

图 9-5 描述了传统的 Nginx 请求路由的流程。当 URI 请求到达 Nginx 层后,Nginx 会触发路由算法(路由算法是在 Nginx 启动时配置的,不支持动态修改),将 URI请求路由到指定的服务实例上。如果采用的是传统的路由模式,则开发人员不能动态地控制路由规则

图 9-6描述了基于网关路由的流程。当URI请求到达网关后,网关会根据开发人员预先设置的路由规则完成服务路由,将 URI请求路由到指定的服务实例。

如果开发人员觉得路由规则不是很合理,可以动态地修改路由规则,在不重启网关的前提下让路由规则实时地生效。

2.断言

Java8引入了函数式编程,断言也是 Java8新增的语义。断言语义的核心是“输入一个条件,返回一个 Bolean 类型”,这一点非常适合作为网关路由规则的触发条件。

Spring Cloud Gateway 已经支持断言,在 Spring Cloud Gateway 中将断言作为路由转发的判断条件。目前 Spring Cloud Gateway 支持多种断言方式,常见如基于 Path、Query、Method、Header等。

在 Spring Cloud Gateway 中,路由是可以组合的。如图 9-7所示,比如路由 A、路由 B和路由C,组合后的路由会按照优先级执行;但如果断言不成功,则Spring Cloud Gateway 会不处理路由请求。

基于 Path 的断言如下所示,如果 Path 路径匹配不成功,则路由规则不生效。

yaml
routes:
  - id: user_api_route
    uri: http://127.0.0.1:28089
    predicates:
      - Path=/user/getUserInfo

3.过滤器

过滤器是 Spring Cloud Gateway 中的核心功能。在 Spring Cloud Gateway 中,处理过滤器中 HTTP 请求的核心流程如图 9-8 所示。

(1) “业务能力使用方”使用统一的域名调用应用的 Gateway Client。Gateway Client通常都是集群部署的

(2) Gateway Client 调用 Spring Cloud Gateway的 Gateway Handler Mapping(网关映射处理类)。

(3) Gateway Handler Mapping 调用 Gateway Web Handler(网关的 Web 请求处理类)。

(4) Web请求处理类会将清求转发到过滤器链。在Spring Cloud Gateway 中,真正处理 URI请求的是过滤器链中的过滤器

(5) 在按照预先设定的优先级执行完过滤器后,Spring Cloud Gateway 会将请求转发到代理过滤器( 比如 NettyRoutingFilter)。通过代理过滤器执行URI的RPC请求,Spring Cloud Gateway在代理请求执行的过程中,会变更原始请求的端口号。比如,业务人员统一访问访问IP 地址“127.0.0.1:8080/user-api/getUserinfo”,经过代理过滤器转发后,业务 API 服务收到的请求会变为“127.0.0.1:26785/user-api/getUserlnfo。

(6) 业务 API 服务在处理完成后,会通过过滤器将响应结果返回给业务 API的调用者

🚀 9.2 用Reactor Netty实现 Spring Cloud Gateway的通信渠道

Spring Cloud Gateway 是一个高性能的业务网关。为什么说它是高性能的呢?因为其底层通信采用的是 Reactor Netty,通过它来集成高性能的 RPC 框架 Netty

🚀 9.2.1 什么是Reactor Netty

为了满足微服务架构中服务之间多场景的 RPC 调用,Reactor Netty 支持基于 HTTP (WebSocket)、TCP 或 UDP 的通信渠道,其底层基于Netty框架。

Spring Cloud Gateway 运行 Reactor Netty 的软件环境是:

① JDK 版本为 1.8 及以上;

② Reactive Streams AP!的版本为 1.0及以上;

③ Rcactor Core 的版本为 3.x及以上;

④ Netty 的版本为 4.x及以上。

Reactor Netty 是高性能的 RPC 框架,主要包含如下几个核心的概念:

1.TCP Server

Reactor Netty 提供了一个易于使用和配置的 TCP Server。它隐藏了创建 TCP 服务器所需的大部分 Netty 功能,并添加了流式处理的功能。

TCP Server 的使用示例如下:

java
package com.alibaba.cloud.youxia;

import reactor.netty.DisposableServer;
import reactor.netty.tcp.TcpServer;

public class Application {
    public static void main(String[] args) {
        // 1用TcpServer类创建一个DisposableServer 对象
        DisposableServer server = TcpServer.create().bindNow();
        // 2启动 TCP Server,等待客户端连接
        server.onDispose().block();
    }
}

2.TCP Client

Reactor Netty 提供了易于使用和配置的 TCP Client。它隐藏了创建 TCP 客户端所需的大部分 Netty 功能,并添加了流式处理的功能。

TCP Client 的使用示例如下:

java
package com.alibaba.cloud.youxia;

import reactor.netty.Connection;
import reactor.netty.tcp.TcpClient;

public class Application {
    public static void main(String[] args) {
        // 1 用TcpClient类创建一个Connection 对象
        Connection connection = TcpClient.create().connectNow();
        // 2 启动TcP Client去连接 TCP Server
        connection.onDispose().block();
    }
}

3.HTTP Server

Reactor Netty 提供了易于使用和易于配置的 HTTP Server 类。它隐藏了创建 HTTP 服务器所需的大部分 Netty 功能,并添加了流式处理的功能。

HTTP Server 的使用示例如下:

java
package com.alibaba.cloud.youxia;

import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;

public class Application {
    public static void main(String[] args) {
        // 1.用HttpServer 类创建一个 DisposableServer 对象
        DisposableServer server = HttpServer.create().bindNow();
        // 2.启动 HTTP Server,等待客户端连接
        server.onDispose().block();
    }
}

4.HTTP Client

Reactor Netty 提供了易于使用和易于配置的 HTTP Client。它隐藏了创建 HTTP 客户端所需的大部分 Netty 功能,并添加了流式处理的功能。

HTTP Client 的使用示例如下:

java
package com.alibaba.cloud.youxia;

import reactor.netty.http.client.HttpClient;

public class Application {
    public static void main(String[] args) {
        // 1.用 HttpClient 类创建一个Httpclient 对象
        HttpClient client = HttpClient.create();
        // 2.用get()方法调用 HTTP Server
        client.get()
                .uri("https://127.0.0.1:8080/")
                .response()
                .block();
    }
}

5.UDP Server

Reactor Netty 提供了易于使用和易于配置的 UDP Server,它包含 Netty 的大部分功能,并且屏蔽掉了底层的技术细节,这样开发人员可以利用 Reactor Netty的 API创建一个具备流式处理功能的 UDP Server。

UDP Server 的使用示例如下:

java
package com.alibaba.cloud.youxia;

import reactor.netty.Connection;
import reactor.netty.udp.UdpServer;

import java.time.Duration;

public class Application {
    public static void main(String[] args) {
        // 1.用Udpserver类创建一个Connection 对象
        Connection server = UdpServer.create().bindNow(Duration.ofSeconds(30));
        // 2.启动UDP Server,等待客户端连接
        server.onDispose().block();
    }
}

6.UDP Client

Reactor Netty 提供了易于使用和易于配置的 UDP Client,它包含 Netty 的大部分功能,并且屏蔽掉了底层的技术细节,这样开发人员可以利用 Reactor Netty的 API 创建一个具备流式处理功能的 UDP Client。

UDP Client 的使用示例如下:

java
package com.alibaba.cloud.youxia;

import reactor.netty.Connection;
import reactor.netty.udp.UdpClient;

import java.time.Duration;

public class Application {
    public static void main(String[] args) {
        // 1.用UdpClient类创建一个Connection 对象
        Connection connection = UdpClient.create().connectNow(Duration.ofSeconds(30));
        // 2.用onDispose()方法去连接UDP Server
        connection.onDispose().block();
    }
}

9.2.2 “用过滤器代理网关请求”的原理

🚀 9.3 用“路由规则定位器”(RouteDefinitionLocator)加载网关的路由规则

Spring Cloud Gateway 支持不同类型的路由规则定位器,主要包括注册中心、内存、Redis及配置中心。

9.3.1 “基于注册中心的路由规则定位器”的原理

Spring Cloud Gateway 可以动态地从注册中心获取服务实例信息,目前主要支持 Nacos、Consul、Eureka 等注册中心。

1.从注册中心获取服务路由规则的流程

Spring Cloud Gateway 支持从注册中心获取路由规则,并完成服务路由,如图 9-9所示。

(1)基于 RESTfu API 的微服务集群,将 API 注册到 Nacos 注册中心中。

(2)微服务可以订阅注册中心的 API,完成 RPC。

(3)Spring Cloud Gateway 在启动时注册到 Nacos 注册中心中。

(4)Spring Cloud Gateway 拉取指定服务 ID 的所有健康的实例信息,并设置到本地缓存中。

(5)Spring Cloud Gateway 解析实例信息并生成服务路由信息,进行服务路由。

2.初始化注册中心定位器

Spring Cloud Gateway 用自动配置类 GatewayDiscoveryClientAutoConfiguration 来初始化注册中心定位器(DiscoveryClientRouteDefinitionLocator类),具体代码如下所示:

java
// 略

DiscoveryClientRouteDefinitionLocator 类在初始化的过程中,会从注册中心获取服务实例信息,具体代码如下所示:

java
// 略

3.解析服务实例信息并生成服务路由规则

Spring Cloud Gateway 在获取服务实例信息后,需要将服务实例信息转换为服务路由规则具体代码如下所示:

java
// 略

9.3.2 “基于内存的路由规则定位器”的原理

9.3.3 “基于Redis缓存的路由规则定位器”的原理

9.3.4 “基于属性文件的路由规则定位器”的原理

🚀 9.3.5 【实例】用“基于注册中心和配置中心的路由规则定位器”在网关统一暴露API

代码源码:

  • Demo4Book/sca/chapternine/order-api
  • Demo4Book/sca/chapternine/user-api
  • Demo4Book/sca/chapternine/use-spring-cloud-alibaba-nacos-config-gateway

本实例用“基于注册中心和配置中心的路由规则定位器”在网关统一暴露 API。

1.初始化项目

本实例主要包括3个项目:业务网关 use-spring-cloud-alibaba-nacos-config-gateway、user-api 服务和 order-api 服务。

(1) 使用 Spring Cloud Alibaba 初始化业务网关,部分 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">

    <parent>
        <artifactId>chapternine</artifactId>
        <groupId>com.alibaba.youxia</groupId>
        <version>1.0.0.release</version>
    </parent>

    <modelVersion>4.0.0</modelVersion>
    <artifactId>use-spring-cloud-alibaba-nacos-config-gateway</artifactId>
    <groupId>com.alibaba.youxia</groupId>
    <version>1.0.0.release</version>
    <packaging>jar</packaging>
    <name>use-spring-cloud-alibaba-nacos-config-gateway</name>
    <description>使用配置中心来存储路由规则</description>
    <dependencies>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.httpcomponents</groupId>
                    <artifactId>httpclient</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.33</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.6</version>
        </dependency>

        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
            <version>2.0.1.Final</version>
            <scope>compile</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.10</version>
        </dependency>
    </dependencies>
</project>

(2) 在本实例的3个项目中,添加项目启动的配置信息。其中,业务网关的配置信息如下所示。其他服务的配置信息可以参考本书配套资源中的代码。

yaml
server:
  port: 28082
spring:
  cloud:
    gateway:
      discovery:
        locator:
          lower-case-service-id: true
          enabled: false
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        namespace: c7ba173f-29e5-4c58-ae78-b102be11c4f9
        group: gateway-dynamic-route-rule
      config:
        namespace: c7ba173f-29e5-4c58-ae78-b102be11c4f9
        group: gateway-dynamic-route-rule
        enable-remote-sync-config: true
        server-addr: 127.0.0.1:8848
        file-extension: yaml
        prefix: gateway-dynamic-route-rule
  application:
    name: gateway-dynamic-route-rule
  redis:
    host: 127.0.0.1
    port: 9013
    password: ******
    database: 0
logging:
  pattern:
    level: debug

2.在配置中心中添加业务网关的路由规则

在 Nacos 控制台中添加配置文件 gateway-dynamic-route-rule,并配置如下信息:

Data ID: gateway-dynamic-route-rule.json

Group: gateway-dynamic-route-rule

配置内容

json
[
    {
        "id": "user-api-router",
        "order": 1,
        "uri": "lb://user-api",
        "predicates": [
            {
                "name": "Path",
                "args": {
                    "pattern": "/user/**"
                }
            }
        ]
    }
]

在 Nacos 配置中心中,服务 user-api 的路由规则已经生效。

3.动态读取路由规则,并将其加载到业务网关中

定义一个 DynamicRouteService 类并实现 ApplicationEventPublisherAware 接口,具体代码如下所示:

java
package com.alibaba.cloud.youxia.dynamic.route;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionLocator;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import reactor.core.publisher.Mono;

import java.util.List;

@Slf4j
@Service
public class DynamicRouteService implements ApplicationEventPublisherAware {
    // 引入路由规则写入器 RouteDefinitionWriter
    @Autowired
    private RouteDefinitionWriter routeDefinitionWriter;
    // 引入路由规则定位器 RouteDefinitionLocator
    @Autowired
    private RouteDefinitionLocator routeDefinitionLocator;
    @Autowired
    private ApplicationEventPublisher publisher;

    // Spring Framework的事件发布器,用于在路由规则变更后通知应用
    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.publisher = applicationEventPublisher;
    }

    // 删除路由规则
    public String delete(String id) {
        try {
            log.info("gateway delete route id {}", id);
            this.routeDefinitionWriter.delete(Mono.just(id)).subscribe();
            this.publisher.publishEvent(new RefreshRoutesEvent(this));
            return "delete success";
        } catch (Exception e) {
            return "delete fail";
        }
    }

    // 更新路由规则
    public String updateList(List<RouteDefinition> definitions) {
        log.info("gateway update route {}", definitions);
        // 删除缓存routerDefinition
        List<RouteDefinition> routeDefinitionsExits = routeDefinitionLocator.getRouteDefinitions().buffer().blockFirst();
        if (!CollectionUtils.isEmpty(routeDefinitionsExits)) {
            routeDefinitionsExits.forEach(routeDefinition -> {
                log.info("delete routeDefinition:{}", routeDefinition);
                delete(routeDefinition.getId());
            });
        }
        definitions.forEach(definition -> {
            updateById(definition);
        });
        return "success";
    }

    // 更新路由规则
    public String updateById(RouteDefinition definition) {
        try {
            log.info("gateway update route {}", definition);
            this.routeDefinitionWriter.delete(Mono.just(definition.getId()));
        } catch (Exception e) {
            return "update fail,not find route  routeId: " + definition.getId();
        }
        try {
            routeDefinitionWriter.save(Mono.just(definition)).subscribe();
            this.publisher.publishEvent(new RefreshRoutesEvent(this));
            return "success";
        } catch (Exception e) {
            return "update route fail";
        }
    }

    // 添加路由规则
    public String add(RouteDefinition definition) {
        log.info("gateway add route {}", definition);
        routeDefinitionWriter.save(Mono.just(definition)).subscribe();
        this.publisher.publishEvent(new RefreshRoutesEvent(this));
        return "success";
    }
}

用 NacosRouteDynamicDataSource 类实现 ApplicationRunner 接口,从配置中心动态地读取路由规则,具体代码如下所示:

java
package com.alibaba.cloud.youxia.dynamic.route;

import com.alibaba.cloud.nacos.NacosConfigManager;
//import com.alibaba.cloud.youxia.config.GatewayConfig;
import com.alibaba.fastjson.JSON;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Component
@RefreshScope
@Slf4j
public class NacosRouteDynamicDataSource implements ApplicationRunner {
    @Autowired
    private NacosConfigManager nacosConfigManager;

    private ConfigService configService;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        ExecutorService executorService= Executors.newFixedThreadPool(1);
        executorService.execute(new RefreshRouteCache());
    }

    class RefreshRouteCache implements Runnable{
        @Override
        public void run() {
            while (true) {
                log.info("gateway route init...");
                try {
                    if (null == configService) {
                        configService = nacosConfigManager.getConfigService();
                    }
                    String configInfo = configService.getConfig("gateway-dynamic-route-rule.json", "gateway-dynamic-route-rule"
                            , 4000);
                    log.info("获取网关当前配置:\r\n{}", configInfo);
                    List<RouteDefinition> definitionList = JSON.parseArray(configInfo, RouteDefinition.class);
                    for (RouteDefinition definition : definitionList) {
                        log.info("update route : {}", definition.toString());
                        dynamicRouteService.add(definition);
                    }
                    Thread.sleep(20000);
                } catch (Exception e) {
                    log.error("初始化网关路由时发生错误", e);
                }
                dynamicRouteByNacosListener("gateway-dynamic-route-rule.json", "gateway-dynamic-route-rule");
            }
        }
    }

    public void dynamicRouteByNacosListener (String dataId, String group){
        try {
            configService.addListener(dataId, group, new Listener()  {
                @Override
                public void receiveConfigInfo(String configInfo) {
                    log.info("进行网关更新:\n\r{}",configInfo);
                    List<RouteDefinition> definitionList = JSON.parseArray(configInfo, RouteDefinition.class);
                    log.info("update route : {}",definitionList.toString());
                    dynamicRouteService.updateList(definitionList);
                }
                @Override
                public Executor getExecutor() {
                    log.info("getExecutor\n\r");
                    return null;
                }
            });
        } catch (NacosException e) {
            log.error("从nacos接收动态路由配置出错!!!",e);
        }
    }

    @Resource
    private DynamicRouteService dynamicRouteService;
}

4.验证业务网关到用户 AP|的路由效果

浏览器访问http://127.0.0.1:28082/user/getUserInfo,调用订单 API 成功,这样之后对业务网关 IP 地址的访问就会直接被注册中心路由到用户 API。

5.验证动态路由效果

在没有添加订单 API 的路由规则前,访问http://127.0.0.1:28082/order/getOrderInfo,订单 API 响应 404 错误。

在 Nacos 配置中心的配置文件 gateway-dynamic-route-rule.json 中,添加订单 API的路由规则。

json
[
    {
        "id": "user-api-router",
        "order": 1,
        "uri": "lb://user-api",
        "predicates": [
            {
                "name": "Path",
                "args": {
                    "pattern": "/user/**"
                }
            }
        ]
    },
    {
        "id": "order-api-router",
        "order": 2,
        "uri": "lb://order-api",
        "predicates": [
            {
                "name": "Path",
                "args": {
                    "pattern": "/order/**"
                }
            }
        ]
    }
]

在订单 API的路由规则的配置信息添加成功后,在不重启业务网关的前提下,浏览器访问http://127.0.0.1:28082/order/getOrderInfo,调用订单 API 成功。

http://127.0.0.1:28082/order/getOrderInfo
10.0.1.1:56874 已经通过网关路由到了订单服务

🚀 9.4 用“Redis + Lua”进行网关API的限流

在微服务架构中,分布式限流是确保服务稳定性的一项关键性技术。Spring Cloud Gateway 集成 Spring Data Redis,支持开发人员采用“Redis +Lua”模式,并利用 Redis 的分布式限流算法,实现网关 API的限流。

🚀 9.4.1 “网关用Redis + Lua实现分布式限流”的原理

下面分析“网关用 Redis +Lua 实现分布式限流”的原理。

1.加载限流算法

Spring Cloud Gateway 默认实现了“Redis + Lua 模式”的限流算法——令牌桶算法。如图9-15 所示,在 Spring Cloud Gateway Core 工程的request_rate_limiter.lua 文件中,用 Lua脚本语言定义了令牌桶算法。开发人员只需要按照 Spring Cloud Gateway 制定的配置语法,在配置信息中添加令牌桶算法对应的路由限流规则,即可使用Spring Cloud Gateway 默认的限流算法

C:/MyDisk/All/SoftData/MavenRepo/org/springframework/cloud/spring-cloud-gateway-core/2.2.5.RELEASE/spring-cloud-gateway-core-2.2.5.RELEASE.jar!/META-INF/scripts/request_rate_limiter.lua

lua
local tokens_key = KEYS[1]
local timestamp_key = KEYS[2]
--redis.log(redis.LOG_WARNING, "tokens_key " .. tokens_key)

local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])

local fill_time = capacity/rate
local ttl = math.floor(fill_time*2)

--redis.log(redis.LOG_WARNING, "rate " .. ARGV[1])
--redis.log(redis.LOG_WARNING, "capacity " .. ARGV[2])
--redis.log(redis.LOG_WARNING, "now " .. ARGV[3])
--redis.log(redis.LOG_WARNING, "requested " .. ARGV[4])
--redis.log(redis.LOG_WARNING, "filltime " .. fill_time)
--redis.log(redis.LOG_WARNING, "ttl " .. ttl)

local last_tokens = tonumber(redis.call("get", tokens_key))
if last_tokens == nil then
  last_tokens = capacity
end
--redis.log(redis.LOG_WARNING, "last_tokens " .. last_tokens)

local last_refreshed = tonumber(redis.call("get", timestamp_key))
if last_refreshed == nil then
  last_refreshed = 0
end
--redis.log(redis.LOG_WARNING, "last_refreshed " .. last_refreshed)

local delta = math.max(0, now-last_refreshed)
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
local allowed_num = 0
if allowed then
  new_tokens = filled_tokens - requested
  allowed_num = 1
end

--redis.log(redis.LOG_WARNING, "delta " .. delta)
--redis.log(redis.LOG_WARNING, "filled_tokens " .. filled_tokens)
--redis.log(redis.LOG_WARNING, "allowed_num " .. allowed_num)
--redis.log(redis.LOG_WARNING, "new_tokens " .. new_tokens)

if ttl > 0 then
  redis.call("setex", tokens_key, ttl, new_tokens)
  redis.call("setex", timestamp_key, ttl, now)
end

-- return { allowed_num, new_tokens, capacity, filled_tokens, requested, new_tokens }
return { allowed_num, new_tokens }

在 Spring Cloud Gateway 中,用自动配置类 GatewayRedisAutoConfiguration 的 redisRequestRateLimiterScript() 方法来加载限流算法,具体代码如下所示:

java
package org.springframework.cloud.gateway.config;

import java.util.List;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration;
import org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiter;
import org.springframework.cloud.gateway.support.ConfigurationService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.core.ReactiveStringRedisTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.web.reactive.DispatcherHandler;

@Configuration(proxyBeanMethods = false)
@AutoConfigureAfter(RedisReactiveAutoConfiguration.class)
@AutoConfigureBefore(GatewayAutoConfiguration.class)
@ConditionalOnBean(ReactiveRedisTemplate.class)
@ConditionalOnClass({ RedisTemplate.class, DispatcherHandler.class })
class GatewayRedisAutoConfiguration {

	@Bean
	@SuppressWarnings("unchecked")
	public RedisScript redisRequestRateLimiterScript() {
		DefaultRedisScript redisScript = new DefaultRedisScript<>();
		redisScript.setScriptSource(new ResourceScriptSource(
				new ClassPathResource("META-INF/scripts/request_rate_limiter.lua")));
		redisScript.setResultType(List.class);
		return redisScript;
	}

	@Bean
	@ConditionalOnMissingBean
	public RedisRateLimiter redisRateLimiter(ReactiveStringRedisTemplate redisTemplate,
			@Qualifier(RedisRateLimiter.REDIS_SCRIPT_NAME) RedisScript<List<Long>> redisScript,
			ConfigurationService configurationService) {
		return new RedisRateLimiter(redisTemplate, redisScript, configurationService);
	}
}

2.加载 Redis 限流器

用自动配置类 GatewayRedisAutoConfiguration的redisRateLimiter()方法来加载Redis 限流器,具体代码如下所示:

java
	@Bean
	@ConditionalOnMissingBean
	public RedisRateLimiter redisRateLimiter(ReactiveStringRedisTemplate redisTemplate,
			@Qualifier(RedisRateLimiter.REDIS_SCRIPT_NAME) RedisScript<List<Long>> redisScript,
			ConfigurationService configurationService) {
		return new RedisRateLimiter(redisTemplate, redisScript, configurationService);
	}

3.初始化请求限流过滤器工厂类 RequestRateLimiterGatewayFilterFactory

在网关启动的过程中,会执行自动配置类 GatewayAutoConfiguration,并初始化请求限流过滤器工厂类 RequestRateLimiterGatewayFilterFactory,具体代码如下所示:

java
	@Bean(name = PrincipalNameKeyResolver.BEAN_NAME)
	@ConditionalOnBean(RateLimiter.class)
	@ConditionalOnMissingBean(KeyResolver.class)
	public PrincipalNameKeyResolver principalNameKeyResolver() {
		return new PrincipalNameKeyResolver();
	}

	@Bean
	@ConditionalOnBean({ RateLimiter.class, KeyResolver.class })
	public RequestRateLimiterGatewayFilterFactory requestRateLimiterGatewayFilterFactory(
			RateLimiter rateLimiter, KeyResolver resolver) {
		return new RequestRateLimiterGatewayFilterFactory(rateLimiter, resolver);
	}

4.用 GatewayProperties 类读取开发人员配置的限流规则

读取开发人员配置的限流规则的 GatewayProperties 类的具体代码如下所示,

java
/*
 * Copyright 2013-2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.cloud.gateway.config;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.validation.Valid;
import javax.validation.constraints.NotNull;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.gateway.filter.FilterDefinition;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.core.style.ToStringCreator;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;

/**
 * @author Spencer Gibb
 */
@ConfigurationProperties("spring.cloud.gateway")
@Validated
public class GatewayProperties {

	private final Log logger = LogFactory.getLog(getClass());

	/**
	 * List of Routes.
	 */
	@NotNull
	@Valid
	private List<RouteDefinition> routes = new ArrayList<>();

	/**
	 * List of filter definitions that are applied to every route.
	 */
	private List<FilterDefinition> defaultFilters = new ArrayList<>();

	private List<MediaType> streamingMediaTypes = Arrays
			.asList(MediaType.TEXT_EVENT_STREAM, MediaType.APPLICATION_STREAM_JSON);

	/**
	 * Option to fail on route definition errors, defaults to true. Otherwise, a warning
	 * is logged.
	 */
	private boolean failOnRouteDefinitionError = true;

	public List<RouteDefinition> getRoutes() {
		return routes;
	}

	public void setRoutes(List<RouteDefinition> routes) {
		this.routes = routes;
		if (routes != null && routes.size() > 0 && logger.isDebugEnabled()) {
			logger.debug("Routes supplied from Gateway Properties: " + routes);
		}
	}

	public List<FilterDefinition> getDefaultFilters() {
		return defaultFilters;
	}

	public void setDefaultFilters(List<FilterDefinition> defaultFilters) {
		this.defaultFilters = defaultFilters;
	}

	public List<MediaType> getStreamingMediaTypes() {
		return streamingMediaTypes;
	}

	public void setStreamingMediaTypes(List<MediaType> streamingMediaTypes) {
		this.streamingMediaTypes = streamingMediaTypes;
	}

	public boolean isFailOnRouteDefinitionError() {
		return failOnRouteDefinitionError;
	}

	public void setFailOnRouteDefinitionError(boolean failOnRouteDefinitionError) {
		this.failOnRouteDefinitionError = failOnRouteDefinitionError;
	}

	@Override
	public String toString() {
		return new ToStringCreator(this).append("routes", routes)
				.append("defaultFilters", defaultFilters)
				.append("streamingMediaTypes", streamingMediaTypes)
				.append("failOnRouteDefinitionError", failOnRouteDefinitionError)
				.toString();

	}

}

Spring Cloud Gateway 会读取以“spring.cloud.gateway.*”为前缀的路由配置信息。

application.yaml中的具体配置信息示例如下:

yaml
spring:
  cloud:
    gateway:
      routes:
	    # 对应 GatewayProperties类的属性字段routes。routes是一个数组链表,用于存储路由信息 RouteDefinition
        - id: redis_limit_route
          uri: http://127.0.0.1:28089
          predicates:
            - Path=/**
          filters:
            - name: RequestRateLimiter
              args:
                key-resolver: '#{@hostAddrKeyResolver}'
                redis-rate-limiter.replenishRate: 1
                redis-rate-limiter.burstCapacity: 2

在 Spring Cloud Gateway 中,路由配置信息与 RouteDefinition 类中的属性字段是——对应的,具体代码如下所示:

java
package org.springframework.cloud.gateway.route;

import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import javax.validation.Valid;
import javax.validation.ValidationException;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;

import org.springframework.cloud.gateway.filter.FilterDefinition;
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
import org.springframework.validation.annotation.Validated;

import static org.springframework.util.StringUtils.tokenizeToStringArray;

/**
 * @author Spencer Gibb
 */
@Validated
public class RouteDefinition {

	private String id;

	@NotEmpty
	@Valid
	private List<PredicateDefinition> predicates = new ArrayList<>();

	@Valid
	private List<FilterDefinition> filters = new ArrayList<>();

	@NotNull
	private URI uri;

	private Map<String, Object> metadata = new HashMap<>();

	private int order = 0;

	public RouteDefinition() {
	}

	public RouteDefinition(String text) {
		int eqIdx = text.indexOf('=');
		if (eqIdx <= 0) {
			throw new ValidationException("Unable to parse RouteDefinition text '" + text
					+ "'" + ", must be of the form name=value");
		}

		setId(text.substring(0, eqIdx));

		String[] args = tokenizeToStringArray(text.substring(eqIdx + 1), ",");

		setUri(URI.create(args[0]));

		for (int i = 1; i < args.length; i++) {
			this.predicates.add(new PredicateDefinition(args[i]));
		}
	}

	public String getId() {
		return id;
	}

	public void setId(String id) {
		this.id = id;
	}

	public List<PredicateDefinition> getPredicates() {
		return predicates;
	}

	public void setPredicates(List<PredicateDefinition> predicates) {
		this.predicates = predicates;
	}

	public List<FilterDefinition> getFilters() {
		return filters;
	}

	public void setFilters(List<FilterDefinition> filters) {
		this.filters = filters;
	}

	public URI getUri() {
		return uri;
	}

	public void setUri(URI uri) {
		this.uri = uri;
	}

	public int getOrder() {
		return order;
	}

	public void setOrder(int order) {
		this.order = order;
	}

	public Map<String, Object> getMetadata() {
		return metadata;
	}

	public void setMetadata(Map<String, Object> metadata) {
		this.metadata = metadata;
	}

	@Override
	public boolean equals(Object o) {
		if (this == o) {
			return true;
		}
		if (o == null || getClass() != o.getClass()) {
			return false;
		}
		RouteDefinition that = (RouteDefinition) o;
		return this.order == that.order && Objects.equals(this.id, that.id)
				&& Objects.equals(this.predicates, that.predicates)
				&& Objects.equals(this.filters, that.filters)
				&& Objects.equals(this.uri, that.uri)
				&& Objects.equals(this.metadata, that.metadata);
	}

	@Override
	public int hashCode() {
		return Objects.hash(this.id, this.predicates, this.filters, this.uri,
				this.metadata, this.order);
	}

	@Override
	public String toString() {
		return "RouteDefinition{" + "id='" + id + '\'' + ", predicates=" + predicates
				+ ", filters=" + filters + ", uri=" + uri + ", order=" + order
				+ ", metadata=" + metadata + '}';
	}

}

5.用 RouteDefinitionRouteLocator 类加载请求路由限流过滤器

在 Spring Cloud Gateway 中,用 RouteDefinitionRouteLocator 类加载请求路由限流过滤器

(1) 用 getRoutes()方法获取网关的路由信息,具体代码如下所示:

java
	@Override
	public Flux<Route> getRoutes() {
		Flux<Route> routes = this.routeDefinitionLocator.getRouteDefinitions()
				.map(this::convertToRoute);

		if (!gatewayProperties.isFailOnRouteDefinitionError()) {
			// instead of letting error bubble up, continue
			routes = routes.onErrorContinue((error, obj) -> {
				if (logger.isWarnEnabled()) {
					logger.warn("RouteDefinition id " + ((RouteDefinition) obj).getId()
							+ " will be ignored. Definition has invalid configs, "
							+ error.getMessage());
				}
			});
		}

		return routes.map(route -> {
			if (logger.isDebugEnabled()) {
				logger.debug("RouteDefinition matched: " + route.getId());
			}
			return route;
		});
	}

(2) 用 getFilters() 方法获取路由过滤器,具体代码如下所示:

java
	private Route convertToRoute(RouteDefinition routeDefinition) {
		AsyncPredicate<ServerWebExchange> predicate = combinePredicates(routeDefinition);
		List<GatewayFilter> gatewayFilters = getFilters(routeDefinition);

		return Route.async(routeDefinition).asyncPredicate(predicate)
				.replaceFilters(gatewayFilters).build();
	}

    private List<GatewayFilter> getFilters(RouteDefinition routeDefinition) {
		List<GatewayFilter> filters = new ArrayList<>();

		// TODO: support option to apply defaults after route specific filters?
		if (!this.gatewayProperties.getDefaultFilters().isEmpty()) {
			filters.addAll(loadGatewayFilters(DEFAULT_FILTERS,
					new ArrayList<>(this.gatewayProperties.getDefaultFilters())));
		}

		if (!routeDefinition.getFilters().isEmpty()) {
			filters.addAll(loadGatewayFilters(routeDefinition.getId(),
					new ArrayList<>(routeDefinition.getFilters())));
		}

		AnnotationAwareOrderComparator.sort(filters);
		return filters;
	}

(3) 用 loadGatewayFilters() 方法将路由 10 对应的路由规则转换为 GatewayFiter 类,具体代码如下所示:

java
	List<GatewayFilter> loadGatewayFilters(String id,
			List<FilterDefinition> filterDefinitions) {
		ArrayList<GatewayFilter> ordered = new ArrayList<>(filterDefinitions.size());
		for (int i = 0; i < filterDefinitions.size(); i++) {
			FilterDefinition definition = filterDefinitions.get(i);
			GatewayFilterFactory factory = this.gatewayFilterFactories
					.get(definition.getName());
			if (factory == null) {
				throw new IllegalArgumentException(
						"Unable to find GatewayFilterFactory with name "
								+ definition.getName());
			}
			if (logger.isDebugEnabled()) {
				logger.debug("RouteDefinition " + id + " applying filter "
						+ definition.getArgs() + " to " + definition.getName());
			}

			// @formatter:off
			Object configuration = this.configurationService.with(factory)
					.name(definition.getName())
					.properties(definition.getArgs())
					.eventFunction((bound, properties) -> new FilterArgsEvent(
							// TODO: why explicit cast needed or java compile fails
							RouteDefinitionRouteLocator.this, id, (Map<String, Object>) properties))
					.bind();
			// @formatter:on

			// some filters require routeId
			// TODO: is there a better place to apply this?
			if (configuration instanceof HasRouteId) {
				HasRouteId hasRouteId = (HasRouteId) configuration;
				hasRouteId.setRouteId(id);
			}

			GatewayFilter gatewayFilter = factory.apply(configuration);
			if (gatewayFilter instanceof Ordered) {
				ordered.add(gatewayFilter);
			}
			else {
				ordered.add(new OrderedGatewayFilter(gatewayFilter, i + 1));
			}
		}

		return ordered;
	}

🚀 9.4.2 【实例】将Spring Cloud Alibaba应用接入网关,用“Redis +Lua”进行限流

实例代码:

  • Demo4Book/sca/chapternine/order-api
  • Demo4Book/sca/chapternine/use-spring-cloud-alibaba-redis-lua-gateway

本实例包含两个项目:业务网关 use-spring-cloud-alibaba-redis-lua-gateway 和业务项目 user-api.

1.初始化项目

下面使用 Spring Cloud Alibaba 初始化业务网关和业务项目,

(1) 在业务网关中添加 Spring Cloud Gateway 相关依赖包,部分 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">

    <parent>
        <artifactId>chapternine</artifactId>
        <groupId>com.alibaba.youxia</groupId>
        <version>1.0.0.release</version>
    </parent>

    <modelVersion>4.0.0</modelVersion>
    <artifactId>use-spring-cloud-alibaba-redis-lua-gateway</artifactId>
    <groupId>com.alibaba.youxia</groupId>
    <version>1.0.0.release</version>
    <packaging>jar</packaging>
    <name>use-spring-cloud-alibaba-redis-lua-gateway</name>
    <description>使用Spring Cloud Alibaba集成Spring Cloud Gateway</description>
    <dependencies>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.httpcomponents</groupId>
                    <artifactId>httpclient</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

<!--        <dependency>-->
<!--            <groupId>org.springframework.boot</groupId>-->
<!--            <artifactId>spring-boot-starter-data-redis</artifactId>-->
<!--        </dependency>-->

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.6</version>
        </dependency>

        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
            <version>2.0.1.Final</version>
            <scope>compile</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>RELEASE</version>
            <scope>compile</scope>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.10</version>
        </dependency>
    </dependencies>
</project>

(2) 在业务网关中添加路由和限流的配置信息,部分配置如下:

文件:Demo4Book/sca/chapternine/use-spring-cloud-alibaba-redis-lua-gateway/src/main/resources/application.yaml

yaml
server:
  port: 28082
spring:
  cloud:
    gateway:
      routes:
        - id: redis_limit_route
          uri: http://127.0.0.1:28089
          predicates:
            - Path=/**
          filters:
            - name: RequestRateLimiter
              args:
                key-resolver: '#{@hostAddrKeyResolver}'
                redis-rate-limiter.replenishRate: 1
                redis-rate-limiter.burstCapacity: 2
                # redis-rate-limiter.replenishRate是您希望允许用户每秒执行多少请求,而不会丢弃任何请求。这是令牌桶填充的速率。
                # redis-rate-limiter.burstCapacity是指令牌桶的容量,允许在一秒钟内完成的最大请求数,将此值设置为零将阻止所有请求。
                # key-resolver: “#{@ipKeyResolver}” 用于通过SPEL表达式来指定使用哪一个KeyResolver
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        namespace: c7ba173f-29e5-4c58-ae78-b102be11c4f9
        group: use-spring-cloud-alibaba-redis-lua-gateway
  application:
    name: gateway-limiter
  redis:
    host: 127.0.0.1
    port: 9013
    password: ******
    database: 0

2.添加“基于 IP 地址限流键的解析器

本实例定义了 HostAddrKeyResolver 类实现 Spring Cloud Gateway 的 KeyResolver 接口,具体代码如下所示:

java
package com.alibaba.cloud.youxia.config;

import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

public class HostAddrKeyResolver implements KeyResolver {
    @Override
    public Mono<String> resolve(ServerWebExchange exchange) {
        return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
    }
}

3.启动项目

下面用多线程模拟业务网关流量去调用用户 API服务,以验证“Redis+Lua”分布式限流的效果。

(1) 用多线程模拟网关流量,具体代码如下所示:

java
package com.alibaba.cloud.youxia.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@RestController
@RequestMapping("/gateway")
public class GatewayController {
    @Resource
    private RestTemplate restTemplate;

    @GetMapping(value = "/thread")
    public String executeThread(){
        ExecutorService executorService= Executors.newFixedThreadPool(20);
        executorService.execute(new GatewayThread());
        return "成功";
    }

    class GatewayThread implements Runnable{
        @Override
        public void run() {
            while (true){
                restTemplate.getForObject("http://127.0.0.1:28082/user/getUserInfo",String.class);
                try{
                    Thread.sleep(2000);
                }catch (InterruptedException e){
                    System.out.println(e.getMessage());
                }
            }
        }
    }
}

(2) 启动业务网关和用户 API项目,在 Nacos 控制台能够看到服务已经启动并注册成功。

(3) 执行命令“cur 127.0.0.1:28082/gateway/thread”开启多线程调用网关。在Redis后台执行命令“./redis-cli -h 127.0.0.1 -p 6379”登录 Redis 的命令控制台,也可使用图形化界面工具Redis Insight。使用 Redis 客户端命令“keys *”查看当前 Redis 实例中的主键 Key 的列表。在 Redis 实例中已经生成了分布式限流 Key。

ini
request_rate_limiter.{127.0.0.1}.tokens=2