gateway网关限流

网关集成redis限流-根据用户/路径/IP限流

依赖

这里只贴出核心依赖


<dependency>
    <groupId>org.springframework.cloudgroupId>
    <artifactId>spring-cloud-starter-gatewayartifactId>
    <version>3.0.3version>
dependency>

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-redis-reactiveartifactId>
    <version>2.5.3version>
dependency>

同路由id下规则覆盖问题(同路由id单规则->同路由id适配多规则)

非自定义返回提示版
import lombok.extern.slf4j.Slf4j;
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.stereotype.Component;

/**
 * @author whitebrocade
 * @version 1.0
 * @description: 重写RequestRateLimiterGatewayFilterFactory中apply方法
 *
 */
@Slf4j
@Component
public class MyRequestRateLimiterGatewayFilterFactory extends RequestRateLimiterGatewayFilterFactory {

    /**
     * 间隔符号
     */
    private static final String interval_mark = "_";

    public MyRequestRateLimiterGatewayFilterFactory(RateLimiter defaultRateLimiter, KeyResolver defaultKeyResolver) {
        super(defaultRateLimiter, defaultKeyResolver);
    }

    @Override
    public GatewayFilter apply(RequestRateLimiterGatewayFilterFactory.Config config) {
        // 防止重新相同路由id下的规则在map出现覆盖的问题, 改写key的组成规则
        // routeId -> routeId + 限流策略的hash取值 -> 保证了同一路由下, 相同id下的限流规则不会被覆盖
        String routeId = config.getRouteId() + interval_mark + config.getKeyResolver().hashCode();
        log.info("MultiRequestRateLimiterGatewayFilterFactory::routeId={}", routeId);
        config.setRouteId(routeId);
        return super.apply(config);
    }
}
自定义返回提示信息版
import lombok.extern.slf4j.Slf4j;
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.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.Map;

/**
 * @author whitebrocade
 * @version 1.0
 * @description: 重写RequestRateLimiterGatewayFilterFactory中apply方法
 *
 */
@Slf4j
@Component
public class MyRequestRateLimiterGatewayFilterFactory extends RequestRateLimiterGatewayFilterFactory {

    /**
     * 从请求头中获取的user_id
     */
    private static final String USER_ID = "user_id";

    /**
     * 间隔符号
     */
    private static final String INTERVAL_MARK = "_";

    /**
     * 返回的提示信息
     */
    private static final String MSG = "该接口请求频繁, 请稍后重试";

    private final RateLimiter defaultRateLimiter;

    private final KeyResolver defaultKeyResolver;

    public MyRequestRateLimiterGatewayFilterFactory(RateLimiter defaultRateLimiter, KeyResolver defaultKeyResolver) {
        super(defaultRateLimiter, defaultKeyResolver);
        this.defaultRateLimiter = defaultRateLimiter;
        this.defaultKeyResolver = defaultKeyResolver;
    }

    /**
     * 重写apply自定义限流异常返回值以及解决同路由id下多个限流规则的覆盖问题
     * @param config Config
     * @return GatewayFilter
     */
    @Override
    public GatewayFilter apply(Config config) {
        KeyResolver resolver = this.getOrDefault(config.getKeyResolver(), defaultKeyResolver);
        RateLimiter<Object> limiter = this.getOrDefault(config.getRateLimiter(), defaultRateLimiter);
        return (exchange, chain) -> resolver.resolve(exchange).flatMap(key -> {
            String routeId = config.getRouteId();
            if (routeId == null) {
                Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
                // 将routeId修改成routeId + 限流策略的hash取值, 保证了同路由id的多个限流规则不会被覆盖
                routeId = route.getId() + INTERVAL_MARK + resolver.hashCode();
            } else {
                // 将routeId修改成routeId + 限流策略的hash取值, 保证了同路由id的多个限流规则不会被覆盖
                routeId = config.getRouteId() + INTERVAL_MARK + resolver.hashCode();
            }
            String finalRouteId = routeId;
            return limiter.isAllowed(routeId, key).flatMap(response -> {
                for (Map.Entry<String, String> header : response.getHeaders().entrySet()) {
                    exchange.getResponse().getHeaders().add(header.getKey(), header.getValue());
                }
                if (response.isAllowed()) {
                    return chain.filter(exchange);
                }

                String userId = exchange.getRequest().getHeaders().get(USER_ID).get(0);
                // todo 触发限流后, 这里可以尝试编码收集一些数据进行统计, 根据统计结果去做一些逻辑处理
                log.warn("用户: {}, 触发限流的路由id: {}", userId, finalRouteId);

                // 自定义返回值
                ServerHttpResponse httpResponse = exchange.getResponse();
                // 修改code为429: 请求频繁
                httpResponse.setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
                if (!httpResponse.getHeaders().containsKey("Content-Type")) {
                    // 返回类型设置为JSON
                    httpResponse.getHeaders().add("Content-Type", "application/json");
                }
                // 注意事项: 此处无法触发全局异常处理,所以手动封装返回
                DataBuffer buffer = httpResponse.bufferFactory().wrap((
                        "{\n"
                        + "  \"code\": \"" + HttpStatus.TOO_MANY_REQUESTS.value() + "\","
                        + "  \"msg\": \"" + MSG + "\""
                        + "}").getBytes(StandardCharsets.UTF_8));
                return httpResponse.writeWith(Mono.just(buffer));
            });
        });
    }

    private <T> T getOrDefault(T configValue, T defaultValue) {
        return (configValue != null) ? configValue : defaultValue;
    }
}
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.event.FilterArgsEvent;
import org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiter;
import org.springframework.cloud.gateway.support.ConfigurationService;
import org.springframework.data.redis.core.ReactiveStringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;

import java.util.List;
import java.util.Map;

/**
 * @author whitebrocade
 * @version 1.0
 * @description: 自定义的限流器
 */
@Slf4j
public class MyRequestRateLimiter extends RedisRateLimiter {

    /**
     * 配置文件中自定义流策略的的key
     */
    private static final String KEY_RESOLVER_KEY = "key-resolver";

    /**
     * 间隔符号
     */
    private static final String INTERVAL_MARK = "_";

    public MyRequestRateLimiter(ReactiveStringRedisTemplate redisTemplate, RedisScript<List<Long>> script, ConfigurationService configurationService) {
        super(redisTemplate, script, configurationService);
    }

    /**
     * 重写的是处理FilterArgsEvent事件的回调方法, 改写路由id, 保证了同一个路由下配置的多个限流规则不会出现覆盖的现象
     * @param event FilterArgsEvent
     */
    @Override
    public void onApplicationEvent(FilterArgsEvent event) {
        Map<String, Object> args = event.getArgs();
        if (args.containsKey(KEY_RESOLVER_KEY)) {
            String routeId = event.getRouteId() + INTERVAL_MARK + args.get(KEY_RESOLVER_KEY).hashCode();
            super.onApplicationEvent(new FilterArgsEvent(event.getSource(), routeId, args));
        }
        super.onApplicationEvent(event);
    }
}
实现KeyResolver接口自定义限流key
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.cloud.gateway.support.ConfigurationService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.core.ReactiveStringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import reactor.core.publisher.Mono;

import java.util.List;

/**
 * 限流规则配置类
 */
@Slf4j
@Configuration
public class KeyResolverConfiguration {

    /**
     * 从请求头中获取的值
     */
    private static final String USER_ID = "user_id";


    /**
     * userId限流解析策略
     * @return KeyResolver
     */
    @Bean
    @Primary
    KeyResolver userIdResolver(){
        return exchange -> {
            String userId = exchange.getRequest().getHeaders().get(USER_ID).get(0);
            String path = exchange.getRequest().getPath().toString();
            String ipAddress = exchange.getRequest().getRemoteAddress().getHostName();
            log.info("userIdResolver-网关:用户id:{}, 访问路径:{}, ip地址:{}", userId, path, ipAddress);
            return Mono.just(userId);
        };
    }

    /**
     * 路径限流解析策略
     * @return KeyResolver
     */
    @Bean
    KeyResolver pathResolver(){
        return exchange -> {
            String userId = exchange.getRequest().getHeaders().get(USER_ID).get(0);
            String path = exchange.getRequest().getPath().toString();
            String ipAddress = exchange.getRequest().getRemoteAddress().getHostName();
            log.info("pathResolver-网关:用户id:{}, 访问路径:{}, ip地址:{}", userId, path, ipAddress);
            return Mono.just(path);
        };
    }


    /**
     * ip限流策略
     * @return KeyResolver
     */
    @Bean
    KeyResolver ipAddressResolver() {
        return exchange -> {
            String userId = exchange.getRequest().getHeaders().get(USER_ID).get(0);
            String path = exchange.getRequest().getPath().toString();
            String ipAddress = exchange.getRequest().getRemoteAddress().getHostName();
            log.info("ipAddressResolver-网关:用户id:{}, 访问路径:{}, ip地址:{}", userId, path, ipAddress);
            return Mono.just(ipAddress);
        };
    }

    /*
    自己定义的reactiveStringRedisTemplateFlowLimit
    */
    @Qualifier("reactiveStringRedisTemplateFlowLimit")
    ReactiveStringRedisTemplate redisTemplate;

    /*
    限流的Lua脚本
    */
    @Qualifier(MyRequestRateLimiter.REDIS_SCRIPT_NAME)
    RedisScript<List<Long>> redisScript;

    @Bean
    @Primary
    public MyRequestRateLimiter myRedisRateLimiter(ConfigurationService configService) {
        return new MyRequestRateLimiter(redisTemplate, redisScript, configService);
    }
}
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.yhb.common.core.utils.StringUtils;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.data.redis.core.ReactiveStringRedisTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * redis配置
 */
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
    /**
     * 网关redis限流使用的的ReactiveStringRedisTemplate(响应式的), 专门用于网关限流, 没有做对象的序列化
     */
    @Bean
    @SuppressWarnings("rawtypes")
    public ReactiveStringRedisTemplate reactiveStringRedisTemplateFlowLimit(
                @Value("${spring.redis-flow-limit.database}") int database,
                @Value("${spring.redis.lettuce.pool.max-active}") int maxActive,
                @Value("${spring.redis.lettuce.pool.max-wait}") int maxWait,
                @Value("${spring.redis.lettuce.pool.max-idle}") int maxIdle,
                @Value("${spring.redis.lettuce.pool.min-idle}") int minIdle,
                @Value("${spring.redis.host}") String hostName,
                @Value("${spring.redis.port}") int port,
                @Value("${spring.redis.password}") String password)
    {

        /* ========= 基本配置 ========= */
        RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration();
        configuration.setHostName(hostName);
        configuration.setPort(port);
        configuration.setDatabase(database);
        if (! StringUtils.isBlank(password)) {
            RedisPassword redisPassword = RedisPassword.of(password);
            configuration.setPassword(redisPassword);
        }

        /* ========= 连接池通用配置 ========= */
        GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig();
        genericObjectPoolConfig.setMaxTotal(maxActive);
        genericObjectPoolConfig.setMinIdle(minIdle);
        genericObjectPoolConfig.setMaxIdle(maxIdle);
        genericObjectPoolConfig.setMaxWaitMillis(maxWait);

        /* ========= lettuce pool ========= */
        LettucePoolingClientConfiguration.LettucePoolingClientConfigurationBuilder builder = LettucePoolingClientConfiguration.builder();
        builder.poolConfig(genericObjectPoolConfig);
        LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(configuration, builder.build());
        connectionFactory.afterPropertiesSet();

        /* ========= 创建 template ========= */
        return new ReactiveStringRedisTemplate(connectionFactory);
    }
}
redis配置文件
spring: 
  redis:
    host: 127.0.0.1
    port: 6379
    # 有密码就配置, 没有就为空即可
    password: 
    lettuce:
      pool:
        max-idle: 30
        max-active: 8
        max-wait: 10000
        min-idle: 10
  # 网关限流, 限流相关操作的在其他索引库进行
  redis-flow-limit:
    # 使用一号索引库
    database: 1
网关配置文件
spring:
  cloud:
    gateway:
      discovery:
        locator:
          lowerCaseServiceId: true
          enabled: true
      routes:
        # 商户接口
        - id: whitebrocade-merchant
          uri: lb://whitebrocade-merchant
          predicates:
            - Path=/merchant/**
          filters:
            - StripPrefix=1
        # --- -----------网关限流相关配置-start--------------
        # 默认在服务名后添加-limit-flow区分普通路由和限流路由
        - id: whitebrocade-merchant-flow-limit
          uri: lb://whitebrocade-merchant
          predicates:
            - Path=/merchant/pay/myPay
          # 值越小越先匹配,优先级高于默认的0,保证流控的路由优先匹配
          # 如果不设置的话, 匹配的时候就是从上往下匹配, 也就是优先走了路由whitebrocade-merchant, 为了避免手动调整路由的顺序的繁琐, 所以手动指定order
          order: -1
          # 基于令牌桶实现,持有令牌才能访问, 没有令牌就限流
          filters:
            - StripPrefix=1
            # 自定义的限流过滤器
            - name: MyRequestRateLimiter
              args:
                # 使用SpEL表达式从Spring容器中获取限流策略的bean对象, SqEL表达式不清楚去百度一下
                key-resolver: "#{@userIdResolver}"
                # 令牌桶每秒填充平均速率, 允许用户每秒处理多少个请求, 不支持设置小数(即设置了也是无效的)
                redis-rate-limiter.replenishRate: 1
                # 令牌桶的容量,允许在1s内完成的最大请求数, 通常和replenishRate<=replenishRate
                redis-rate-limiter.burstCapacity: 1
            # 这是第二条, 可自行开启
            # - name: MyRequestRateLimiter
            #   args:
            #     # 使用SpEL表达式从Spring容器中获取限流策略的bean对象, SqEL表达式不清楚去百度一下
            #     key-resolver: "#{@ipAddressResolver}"
            #     # 令牌桶每秒填充平均速率, 允许用户每秒处理多少个请求, 不支持设置小数(即设置了也是无效的)
            #     redis-rate-limiter.replenishRate: 1
            #     # 令牌桶的容量,允许在1s内完成的最大请求数, 通常和replenishRate<=replenishRate
            #     redis-rate-limiter.burstCapacity: 5
        # --------------网关限流相关配置-end--------------

到此集成完毕, 测试接口会发现已经限流成功了, 这里就不演示了

注意事项

  1. 网关启动后, Nacos修改限流相关的值可以实时生效
  2. 运行过程中, 没法动态切换用到Redis索引库

阿里云redis集群模式下无法限流

问题现象

问题现象: 阿里云redis集群无法限流, 提示一下异常Error in execution; nested exception is io.lettuce.core.RedisCommandExecutionException:
ERR bad lua script for redis cluster, all the keys that the script uses should be passed using the KEYS array, and KEYS should not be in expression

翻译过来大致的意思就是Redis集群中有错误lua脚本,脚本使用的所有keys都应该使用KEYS数组传递,并且键不应在表达式中

看一下gateway网关中使用的lua脚本, 位于gateway依赖包下的META-INF/scripts/request_rate_limiter.lua

gateway网关限流_第1张图片

gateway原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 }

下边这版是删除了部分注释代码, 以及添加了部分注释的

-- token的key
local tokens_key = KEYS[1]
-- 时间戳的key
local timestamp_key = KEYS[2]

-- 往令牌桶里面放令牌的速率,一秒多少个
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
-- redis过期时间 这里为什么是放满令牌桶的两倍
-- 因为这个时间不能太长,加入太长10s,你第一秒把令牌拿完,后面9s,就会出现突刺现象
local ttl = math.floor(fill_time*2)

-- 获取令牌桶的数量,如果为空,将令牌桶容量赋值给当前token
local last_tokens = tonumber(redis.call("get", tokens_key))
if last_tokens == nil then
  last_tokens = capacity
end

-- 获取最后的更新时间戳,如果为空,设置为0
local last_refreshed = tonumber(redis.call("get", timestamp_key))
if last_refreshed == nil then
  last_refreshed = 0
end

-- 计算出时间间隔
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
-- 零代表false, 即是限流
local allowed_num = 0

-- 如果允许获取得到,计算出剩余的令牌数量,并标记可以获取
if allowed then
  new_tokens = filled_tokens - requested
  allowed_num = 1
end

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

--  lua可以返回多个字段,java获取时用List获取
return { allowed_num, new_tokens }

核心问题就在

local tokens_key = KEYS[1]

local timestamp_key = KEYS[2]

这里将KEYS[1]和 KEYS[2]赋值给了变量, 然后传递给了后续的代码

然而, 为了保证脚本里面的所有操作都在相同slot进行,云数据库Redis集群版本会对Lua脚本做如下限制

  1. 所有key都应该由KEYS数组来传递redis.call/pcall中调用的redis命令,key的位置必须是KEYS array(不能使用Lua变量替换KEYS),否则直接返回错误信息:ERR bad lua script for redis cluster, all the keys that the script uses should be passed using the KEYS arrayrn
  2. 所有key必须在一个slot上,否则返回错误信息: ERR eval/evalsha command keys must be in same slotrn
  3. 调用必须要带有key,否则直接返回错误信息: ERR for redis cluster, eval/evalsha number of keys can’t be negative or zerorn

核心: 然而gateway自带原Lua脚本违背了第一条, 使用Lua变量替换了KEYS

如何解决?
步骤一: 修改lua脚本

将脚本的进行替换, 为了方便观察我非相关的注释删除, 同时对改动的地方进行标注(每一个改动的地方都使用数字标注)

  1. 删除local tokens_key = KEYS[1]local timestamp_key = KEYS[2](这里为了方便观察我就注释掉了)
  2. 将所有用到tokens_key 的地方替换成KEYS[1]
  3. 将所有用到timestamp_key的地方替换成KEYS[2]
-- local tokens_key = KEYS[1] -- 1. 注释掉这行代码
-- local timestamp_key = KEYS[2] -- 2. 注释掉这行代码


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)

-- local last_tokens = tonumber(redis.call("get", tokens_key)) -- 3.1 将这行代码的tokens_key修改成KEYS[1]
local last_tokens = tonumber(redis.call("get", KEYS[1])) -- 3.2 修改后的代码
if last_tokens == nil then
  last_tokens = capacity
end

-- local last_refreshed = tonumber(redis.call("get", timestamp_key)) -- 4.1 将这行代码的timestamp_key修改成KEYS[2]
local last_refreshed = tonumber(redis.call("get", KEYS[2])) -- 4.2 修改后的代码
if last_refreshed == nil then
  last_refreshed = 0
end

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

if ttl > 0 then
  -- redis.call("setex", tokens_key, ttl, new_tokens) -- 5.1 将这行代码的tokens_key修改成KEYS[1]
  redis.call("setex", KEYS[1], ttl, new_tokens) -- 5.2 修改后的代码
  -- redis.call("setex", timestamp_key, ttl, now) -- 6.1 将这行代码的timestamp_key修改成KEYS[2]
  redis.call("setex", timestamp_key, ttl, now) -- 6.2 修改后的代码
end

return { allowed_num, new_tokens }
步骤二: 重写isAllowed()方法, 并将原Lua脚本替换成我们修改后的脚本
import cn.hutool.core.collection.CollUtil;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.gateway.event.FilterArgsEvent;
import org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiter;
import org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator;
import org.springframework.cloud.gateway.support.ConfigurationService;
import org.springframework.data.redis.core.ReactiveStringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @author whitebrocade
 * @version 1.0
 * @description: 自定义的限流器
 */
@Slf4j
@ConfigurationProperties("spring.cloud.gateway.redis-rate-limiter")
public class MyRequestRateLimiter extends RedisRateLimiter {

    /**
     * 配置文件中自定义流策略的的key
     */
    private static final String KEY_RESOLVER_KEY = "key-resolver";

    /**
     * 间隔符号
     */
    private static final String INTERVAL_MARK = "_";

    public MyRequestRateLimiter(ReactiveStringRedisTemplate redisTemplate, RedisScript<List<Long>> script, ConfigurationService configurationService) {
        super(redisTemplate, script, configurationService);
    }

    @Override
    public void onApplicationEvent(FilterArgsEvent event) {
        Map<String, Object> args = event.getArgs();
        if (args.containsKey(KEY_RESOLVER_KEY)) {
            String routeId = event.getRouteId() + INTERVAL_MARK + args.get(KEY_RESOLVER_KEY).hashCode();
            super.onApplicationEvent(new FilterArgsEvent(event.getSource(), routeId, args));
        }
        super.onApplicationEvent(event);
    }

    // -------------下边是重写isAllowed()需要的逻辑

    @Autowired
    @Qualifier("reactiveStringRedisTemplateFlowLimit")
    ReactiveStringRedisTemplate redisTemplate;

    private Config defaultConfig;

    private AtomicBoolean initialized = new AtomicBoolean(false);

    /**
     * 限流lua脚本
     */
    private final String luaScriptStr = "local rate = tonumber(ARGV[1])\n" +
            "local capacity = tonumber(ARGV[2])\n" +
            "local now = tonumber(ARGV[3])\n" +
            "local requested = tonumber(ARGV[4])\n" +
            "\n" +
            "local fill_time = capacity/rate\n" +
            "local ttl = math.floor(fill_time*2)\n" +
            "\n" +
            "local last_tokens = tonumber(redis.call(\"get\", KEYS[1]))\n" +
            "if last_tokens == nil then\n" +
            "  last_tokens = capacity\n" +
            "end\n" +
            "\n" +
            "local last_refreshed = tonumber(redis.call(\"get\", KEYS[2]))\n" +
            "if last_refreshed == nil then\n" +
            "  last_refreshed = 0\n" +
            "end\n" +
            "\n" +
            "local delta = math.max(0, now-last_refreshed)\n" +
            "\n" +
            "local filled_tokens = math.min(capacity, last_tokens+(delta*rate))\n" +
            "local allowed = filled_tokens >= requested\n" +
            "local new_tokens = filled_tokens\n" +
            "local allowed_num = 0\n" +
            "\n" +
            "if allowed then\n" +
            "  new_tokens = filled_tokens - requested\n" +
            "  allowed_num = 1\n" +
            "end\n" +
            "\n" +
            "if ttl > 0 then\n" +
            "  redis.call(\"setex\", KEYS[1], ttl, new_tokens)\n" +
            "  redis.call(\"setex\", KEYS[2], ttl, now)\n" +
            "end\n" +
            "\n" +
            "return { allowed_num, new_tokens }\n";

    @Override
    public Mono<Response> isAllowed(String routeId, String id) {
        Config routeConfig = loadConfiguration(routeId);

        int replenishRate = routeConfig.getReplenishRate();

        int burstCapacity = routeConfig.getBurstCapacity();

        int requestedTokens = routeConfig.getRequestedTokens();

        try {
            // keys参数
            List<String> keys = getKeys(id);
            // args脚本参数
            List<String> scriptArgs = Arrays.asList(
                    replenishRate + "",
                    burstCapacity + "",
                    Instant.now().getEpochSecond() + "",
                    requestedTokens + "");
            // 执行lua脚本

            DefaultRedisScript<List> luaScript = new DefaultRedisScript<>(luaScriptStr, List.class);
            Flux<List> flux = this.redisTemplate.execute(luaScript, keys, scriptArgs);

            // 根据执行结果记录异常或将结果返回
            return flux.onErrorResume(throwable -> {
                log.error("无法调用rate的限流lua脚本: {}", JSONObject.toJSONString(flux), throwable);
                // 将List 转换成 ArrayList
                ArrayList<Long> arr = new ArrayList<>();
                CollUtil.addAll(arr, Arrays.asList(1L, - 1L));
                return Flux.just(arr);
            }).reduce(new ArrayList<Long>(), (longs, l) -> {
                longs.addAll(l);
                return longs;
            }).map(results -> { // 将响应结果返回
                // 0-限流 1-通过
                boolean allowed = results.get(0) == 1L;
                Long tokensLeft = results.get(1);
                Response response = new Response(allowed, getHeaders(routeConfig, tokensLeft));
                log.error("限流返回结果响应体response: " + response);
                return response;
            });
        } catch (Exception e) {
            log.error("Redis限流异常", e);
        }
        // 如果出现了异常, 那么就直接放行, 同时将剩余令牌数设置成-1
        return Mono.just(new Response(true, getHeaders(routeConfig, -1L)));
    }


    static List<String> getKeys(String id) {
        String prefix = "request_rate_limiter.{" + id;

        String tokenKey = prefix + "}.tokens";
        String timestampKey = prefix + "}.timestamp";
        return Arrays.asList(tokenKey, timestampKey);
    }

    Config loadConfiguration(String routeId) {
        Config routeConfig = getConfig().getOrDefault(routeId, defaultConfig);

        if (routeConfig == null) {
            routeConfig = getConfig().get(RouteDefinitionRouteLocator.DEFAULT_FILTERS);
        }

        if (routeConfig == null) {
            throw new IllegalArgumentException("No Configuration found for route " + routeId + " or defaultFilters");
        }
        return routeConfig;
    }
}

gateway集成sentinel限流

依赖


<dependency>
    <groupId>org.springframework.cloudgroupId>
    <artifactId>spring-cloud-starter-gatewayartifactId>
    <version>3.0.3version>
dependency>

<dependency>
    <groupId>com.alibaba.cloudgroupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
    <version>2021.1version>
dependency>

<dependency>
    <groupId>com.alibaba.cloudgroupId>
    <artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
    <version>2021.1version>
dependency>

<dependency>
    <groupId>com.alibaba.cloudgroupId>
    <artifactId>spring-cloud-alibaba-sentinel-gatewayartifactId>
    <version>2021.1version>
dependency>

<dependency>
    <groupId>com.alibaba.cspgroupId>
    <artifactId>sentinel-datasource-nacosartifactId>
    <version>1.8.0version>
dependency>

<dependency>
    <groupId>com.alibaba.cspgroupId>
    <artifactId>sentinel-spring-cloud-gateway-adapterartifactId>
    <version>1.8.0version>
dependency>

Application启动程序

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;

/**
 * 网关启动程序
 */
@Sl4j
@SpringBootApplication})
public class GatewayApplication {
    public static void main(String[] args) {
        // 启动参数设置type, 否则sentinel控制面板中是不会有API管理页面的
        System.setProperty("csp.sentinel.app.type", "1");
        SpringApplication.run(GatewayApplication.class, args);
        log.info("\n >>>>>>>>>>>>>>>>>>>>>>> 网关服务启动成功 <<<<<<<<<<<<<<<<<<<<<<<< \n");
    }
}

自定义限流提示

方式一: 程序编码中处理
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.yhb.common.core.utils.ServletUtils;
import org.springframework.http.HttpStatus;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebExceptionHandler;
import reactor.core.publisher.Mono;

/**
 * 自定义限流异常处理
 *
 */
public class SentinelFallbackHandler implements WebExceptionHandler
{
    private Mono<Void> writeResponse(ServerResponse response, ServerWebExchange exchange)
    {
        // 返回的状态码为429
        return ServletUtils.webFluxResponseWriter(exchange.getResponse(), HttpStatus.TOO_MANY_REQUESTS,"请求超过最大数,请稍后再试", HttpStatus.TOO_MANY_REQUESTS.value());
    }

    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex)
    {
        if (exchange.getResponse().isCommitted())
        {
            return Mono.error(ex);
        }
        if (!BlockException.isBlockException(ex))
        {
            return Mono.error(ex);
        }
        return handleBlockedRequest(exchange, ex).flatMap(response -> writeResponse(response, exchange));
    }

    private Mono<ServerResponse> handleBlockedRequest(ServerWebExchange exchange, Throwable throwable)
    {
        return GatewayCallbackManager.getBlockHandler().handleRequest(exchange, throwable);
    }
}
方式二:配置文件中配置限流提示
spring:
  cloud:
    sentinel:
      scg:
        fallback:
          mode: response
          # 响应码
          response-status: 429
          # 限流响应内容
          response-body: '{"code":429,"message":"触发限流了"}'
          # 响应类型
          content-type: "application/json"

application.yaml配置文件

博主修改sentinel的账号密码以及端口, 大家根据自己的参数修改即可

spring:
  cloud:
    sentinel:
      # 支持链路限流
      # web-context-unify: false
      # 关闭官方默认收敛所有context
      # filter:
        # enabled: true
      # 取消控制台懒加载
      eager: true
      # 限流触发的提示信息
      scg:
        fallback:
          mode: response
          # 响应码
          response-status: 429
          # 限流响应内容
          response-body: '{"code":429,"message":"触发限流了"}'
          # 响应类型
          content-type: "application/json"
      transport:
        # todo 控制台地址 目前是启用本地的, 默认端口是8848
        dashboard: 127.0.0.1:8848
        # todo 跟sentinel控制台交流的端口,随意指定一个未使用的端口即可,默认是8719
        port: 8719
      # sentinel规则持久化配置
      datasource:
        # 网关api分组
        gw-api-group:
          # nacos相关配置
          nacos:
            # 配置中心地址
            server-addr: 127.0.0.1:8848
            # 命名空间
            namespace: whitebrocade
            # 配置文件名
            dataId: sentinel-gw-api-group
            # 文件类型
            data-type: json
            # 规则类型: api分组
            rule-type: gw-api-group
        # 网关限流配置
        gw-flow:
          # nacos的下述参数不在赘述
          nacos:
            server-addr: 127.0.0.1:8848
            namespace: whitebrocade
            dataId: sentinel-gw-flow
            data-type: json
            rule-type: gw-flow

sentinel-gw-api-group.yaml

sentienl规则持久化, 里面的存储的规则都是JSON格式的

注意! 写入nacos的时候一定要把注释移除!!

[
    {
        // API名称
        "apiName": "sentinel-gw-api-group",
        "predicateItems": [
             {
                 // 匹配策略
                "matchStrategy": 0,
                 // 参数值
                "pattern": "/whitebrocade/wallet/myWallet"
            }
        ]
    }
]
  • apiName: API名称
  • matchStrategy: 匹配策略
  • pattern: 匹配的值

sentinel-gw-flow.yaml

[
    {
        // API类型: API分组
        "resourceMode": 1,
        // api名
        "resource": "sentinel-gw-api-group",
        // 流控效果: 默认(直接拒绝)
        "controlBehavior": 0,
        // 阈值类型: QPS模式
        "grade": 1,
        // 限流阈值
        "count": 1,
        // 滑动窗口时间
        "intervalSec": 600,
        // 流控模式: 直接
        "strategy": 0,
        // 突发请求允许流量
        "burst": 0
    }
]
  • resourceMode: aip类型: 0-Route ID, 1-API分组, 默认为Route ID
  • resource: Route ID或者API分组名
  • controlBehavior: 流控效果: 0-默认(直接拒绝), 1-直接拒绝, 2-预热启动, 3-匀速排队
  • grade: 阈值类型: 0-并发线程数, 1-QPS模式, 默认为QPS模式
  • count: 限流阈值, double类型, 默认为0
  • intervalSec: 时间窗口长度, 单位秒, 默认为0
  • strategy: 流控模式: 0-直接, 1-关联, 2-链路, 默认为直接
  • burst: 突发请求允许量

还有部分参数, 由于目前没有用到不, 所以不选出来解释

注意事项

  1. 配置好持久化后, 那么即使重启sentinel, 规则也不会消失!
  2. 而且这里在Nacos可以实时修改限流相关的值, 并且会同步到sentinel控制台, 但是!!!sentinel控制台修改的值不会同步到Nacos配置(博主没有修改sentinel的源码)

sentinel和nacos的双向持久化

需要修改sentinel的源码, 并且这里采用的是push模式, 将sentinel规则持久化到nacos中

1. sentinel-dashboard的pom文件修改

将sentinel-datasource-nacos的test范围注释掉

<dependency>
    <groupId>com.alibaba.cspgroupId>
    <artifactId>sentinel-datasource-nacosartifactId>
    
dependency>

引入下述依赖, 方便改造


<dependency>
    <groupId>org.projectlombokgroupId>
    <artifactId>lombokartifactId>
    <version>1.18.24version>
dependency>

<dependency>
    <groupId>org.apache.commonsgroupId>
    <artifactId>commons-lang3artifactId>
    <version>3.11version>
dependency>

<dependency>
    <groupId>cn.hutoolgroupId>
    <artifactId>hutool-allartifactId>
    <version>5.8.25version>
dependency>

<dependency>
    <groupId>com.alibaba.nacosgroupId>
    <artifactId>nacos-clientartifactId>
    <version>2.2.1version>
dependency>

2. application.properties添加nacos数据源相关信息

修改成yaml格式也可以, 这里就不修改了

# nacos数据源配置
# nacos所在服务器地址
sentinel.nacos.serverAddr=127.0.0.1
# 账号
sentinel.nacos.username=nacos
# 密码
sentinel.nacos.password=nacos
# 命名空间
sentinel.nacos.namespace=whitebrocade

3. 复制test的nacos配置类, 工具类到指定的地方

  1. 在src/main/java/com/alibaba/csp/sentinel/dashboard/rule下新建一个nacos目录
  2. 复制 src/test/java/com/alibaba/csp/sentinel/dashboard/rule/nacos 下的NacosConfig、NacosConfigUtil类到src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos 下
    • ps: test下的是官方给出的持久化示例, 咋们参考使用

gateway网关限流_第2张图片

复制到nacos目录中
gateway网关限流_第3张图片

4. 修改Nacos配置类

修改NacosConfig类

可以直接复制我的

package com.alibaba.csp.sentinel.dashboard.rule.nacos;

import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.ApiDefinitionEntity;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayFlowRuleEntity;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.*;
import com.alibaba.csp.sentinel.datasource.Converter;
import com.alibaba.fastjson.JSON;
import com.alibaba.nacos.api.PropertyKeyConst;
import com.alibaba.nacos.api.config.ConfigFactory;
import com.alibaba.nacos.api.config.ConfigService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;
import java.util.Properties;

/**
 * @author whitebrocade
 * @version 1.0
 * @description: nacos配置类
 *               1. nacos配置文件读取, 将ConfigService注册成Bean对象
 *               2. 配置相关流控规则的转换器(对象 -> JSON, JSON -> 对象), 具体有下列规则
 *                 - 普通流控规则
 *                 - 授权规则
 *                 - 降级规则
 *                 - 热点规则
 *                 - 系统规则
 *                 - 网关API分组管理规则
 *                 - 网关流控规则
 */
@Slf4j
@Configuration
public class NacosConfig {
    // -------------------- 配置文件读取 --------------------
    /**
     * nacos所在的服务器
     */
    @Value("${sentinel.nacos.serverAddr}")
    private String serverAddr;

    /**
     * nacos用户名
     */
    @Value("${sentinel.nacos.username}")
    private String username;

    /**
     * nacos密码
     */
    @Value("${sentinel.nacos.password}")
    private String password;

    /**
     * nacos命名空间
     */
    @Value("${sentinel.nacos.namespace}")
    private String namespace;

    /**
     * 读取nacos配置, 并将nacos配置服务注册为bean对象
     */
    @Bean
    public ConfigService nacosConfigService() throws Exception {
        Properties properties = new Properties();
        properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr);
        properties.put(PropertyKeyConst.NAMESPACE, namespace);
        properties.put(PropertyKeyConst.USERNAME, username);
        properties.put(PropertyKeyConst.PASSWORD, password);
        return ConfigFactory.createConfigService(properties);
    }


    // -------------------- 相关流控规则 --------------------
    // ==================== 普通流控规则 ====================
    /**
     * 普通流控规则: 将List转成JSON字符串, 用于传输到nacos
     */
    @Bean
    public Converter<List<FlowRuleEntity>, String> flowRuleEntityEncoder() {
        return JSON::toJSONString;
    }

    /**
     * 普通流控规则: 将nacos中的JSON字符串转成List, 后续sentinel-dashboard中进行可视化展示
     */
    @Bean
    public Converter<String, List<FlowRuleEntity>> flowRuleEntityDecoder() {
        return s -> JSON.parseArray(s, FlowRuleEntity.class);
    }


    // ==================== 授权规则 ====================
    /**
     * 授权规则: 将List转成JSON字符串, 用于传输到nacos
     */
    @Bean
    public Converter<List<AuthorityRuleEntity>, String> authorRuleEntityEncoder() {
        return JSON::toJSONString;
    }

    /**
     * 授权规则: 将nacos中的JSON字符串转成List, 后续sentinel-dashboard中进行可视化展示
     */
    @Bean
    public Converter<String, List<AuthorityRuleEntity>> authorRuleEntityDecoder() {
        return s -> JSON.parseArray(s, AuthorityRuleEntity.class);
    }


    // ==================== 降级规则 ====================
    /**
     * 降级规则: 将List转成JSON字符串, 用于传输到nacos
     */
    @Bean
    public Converter<List<DegradeRuleEntity>, String> degradeRuleEntityEncoder() {
        return JSON::toJSONString;
    }

    /**
     *  降级规则: 将nacos中的JSON字符串转成List, 后续sentinel-dashboard中进行可视化展示
     */
    @Bean
    public Converter<String, List<DegradeRuleEntity>> degradeRuleEntityDecoder() {
        return s -> JSON.parseArray(s, DegradeRuleEntity.class);
    }


    // ==================== 热点规则 ====================
    /**
     * 热点规则: 将List转成JSON字符串, 用于传输到nacos
     */
    @Bean
    public Converter<List<ParamFlowRuleEntity>, String> paramRuleEntityEncoder() {
        return JSON::toJSONString;
    }

    /**
     *  热点规则: 将nacos中的JSON字符串转成List, 后续sentinel-dashboard中进行可视化展示
     */
    @Bean
    public Converter<String, List<ParamFlowRuleEntity>> paramRuleEntityDecoder() {
        return s -> JSON.parseArray(s, ParamFlowRuleEntity.class);
    }


    // ==================== 系统规则 ====================
    /**
     * 系统规则: 将List转成JSON字符串, 用于传输到nacos
     */
    @Bean
    public Converter<List<SystemRuleEntity>, String> systemRuleEntityEncoder() {
        return JSON::toJSONString;
    }

    /**
     *  系统规则: 将nacos中的JSON字符串转成List, 后续sentinel-dashboard中进行可视化展示
     */
    @Bean
    public Converter<String, List<SystemRuleEntity>> systemRuleEntityDecoder() {
        return s -> JSON.parseArray(s, SystemRuleEntity.class);
    }


    // ==================== 网关API分组管理规则 ====================
    /**
     * 网关API分组管理规则: 将List转成JSON字符串, 用于传输到nacos
     */
    @Bean
    public Converter<List<ApiDefinitionEntity>, String> apiDefinitionEntityEncoder() {
        return JSON::toJSONString;
    }

    /**
     * 网关API分组管理规则: 将nacos中的JSON字符串转成List, 后续sentinel-dashboard中进行可视化展示
     */
    @Bean
    public Converter<String, List<ApiDefinitionEntity>> apiDefinitionEntityDecoder() {
        return s -> JSON.parseArray(s, ApiDefinitionEntity.class);
    }


    // ==================== 网关流控规则 ====================
     /**
     * 网关流控规则: 将List转成JSON字符串, 用于传输到nacos
     */
    @Bean
    public Converter<List<GatewayFlowRuleEntity>, String> gatewayFlowRuleEntityEncoder() {
        return entityList -> {
            List<GatewayFlowRule> ruleList = entityList.stream()
                    .map(GatewayFlowRuleEntity::toGatewayFlowRule)
                    .collect(Collectors.toList());
            String jsonStr = JSONObject.toJSONString(ruleList);
            log.info("转换后的JSON字符串:{}", jsonStr);
            return jsonStr;
        };
    }

    /**
     * 网关流控规则: 将nacos中的JSON字符串转成List, 后续sentinel-dashboard中进行可视化展示
     */
    @Bean
    public Converter<String, List<GatewayFlowRuleEntity>> gatewayFlowRuleEntityDecoder() {
        return s -> JSON.parseArray(s, GatewayFlowRuleEntity.class);
    }
}
修改NacosConfigUtil类
package com.alibaba.csp.sentinel.dashboard.rule.nacos;

/**
 * @author whitebrocade
 * @version 1.0
 * @description: nacos配置类工具, 主要定义一些流控规则文件的后缀名, 便于区分
 */
public class NacosConfigUtil {
    /**
     * nacos中sentinel流控使用的分组
     */
    public static final String GROUP_ID = "SENTINEL_GROUP";


    // -------------------- 流控规则的后缀 --------------------
    /**
     * 普通流控规则
     */
    public static final String FLOW_DATA_ID_POSTFIX = "-flow-rules";

    /**
     * 权限规则
     */
    public static final String AUTHORITY_DATA_ID_POSTFIX = "-authority-rules";

    /**
     * 降级规则
     */
    public static final String DEGRADE_DATA_ID_POSTFIX = "-degrade-rules";

    /**
     * 热点规则
     */
    public static final String PARAM_FLOW_DATA_ID_POSTFIX = "-param-flow-rules";

    /**
     * 系统规则
     */
    public static final String SYSTEM_DATA_ID_POSTFIX = "-system-rules";

    /**
     * 网关API分组管理规则
     */
    public static final String GATEWAY_API_GROUP_DATA_ID_POSTFIX = "-gw-api-group-rules";

    /**
     * 网关流控规则
     */
    public static final String GATEWAY_FLOW_DATA_ID_POSTFIX = "-gw-flow-rules";
}
创建网关规则目录类

在src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos如下结构(目前使用网关限流进行说明, 如果需要其他的, 请自行拓展)

  1. 需要新建包limit, limit下还要新建子包gateway
    • limit: 用于存储限流规则相关持久化
    • gateway: 存储网关限流规则持久化相关实现
  2. gateway下还需要新建两个子包api和flow
    • api: 存储网关API分组管理规则的推送者和发布者
    • flow: 存储网关限流规则的推送者和发布者
  3. 如果后续需要追加持久化, 那么增加相应包的即可, 便于管理
    gateway网关限流_第4张图片
读取网关API分组管理规则
package com.alibaba.csp.sentinel.dashboard.rule.limit.gateway.api;

import cn.hutool.core.util.StrUtil;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.ApiDefinitionEntity;
import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider;
import com.alibaba.csp.sentinel.dashboard.rule.nacos.NacosConfigUtil;
import com.alibaba.csp.sentinel.datasource.Converter;
import com.alibaba.nacos.api.config.ConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

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

/**
 * @author whitebrocade
 * @version 1.0
 * @description: 网关API分组规则的生产者, 拉取Nacos中存储的网关API分组规则配置信息, 到时候用于sentinel-dashboard的规则可视化
 */
@Component("gatewayApiNacosProvider")
public class GatewayApiNacosProvider implements DynamicRuleProvider<List<ApiDefinitionEntity>> {
    /*
    DynamicRuleProvider, 这里的T是要获取的流控实体类型, 可以填写下边的任意一个
    1. 普通限流规则实体 -> FlowRuleEntity
    2. 授权规则实体 -> AuthorityRuleEntity
    3. 降级规则实体 -> DegradeRuleEntity
    4. 热点规则实体 -> ParamFlowRuleEntity
    5. 网关API分组管理规则 -> ApiDefinitionEntity
    6. 网关流控规则 -> GatewayFlowRuleEntity
    */

    /**
     * 注入configService, 用于从nacos中读取相应的流控规则
     */
    @Autowired
    private ConfigService configService;

    /**
     * 注入转换器, 将nacos中相应的流控JSON字符串转换成与之对应的流控实体类对象
     */
    @Autowired
    private Converter<String, List<ApiDefinitionEntity>> converter;

    @Override
    public List<ApiDefinitionEntity> getRules(String appName) throws Exception {
        /*
        getConfig(String dataId, String group, long timeoutMs)
        作用: 从nacos读取配置
        方法参数解析:
            - dataId: 这个规则在nacos中的dataId, 这里我们采用 服务名 + 指定后缀作为dataId, 比如我的服务名是"my-gateway", 而这里后缀我们定义的是"-gw-api-group-rules"
            - group: 该配置文件的分组, 我们使用的是"SENTINEL_GROUP"
            - timeoutMs: 超时毫秒数
         */
        String rules = configService.getConfig(appName + NacosConfigUtil.GATEWAY_API_GROUP_DATA_ID_POSTFIX, 
                                               NacosConfigUtil.GROUP_ID, 
                                               3000);
        if (StrUtil.isEmpty(rules)) {
            return new ArrayList<>();
        }
        return converter.convert(rules);
    }
}
推送网关API分组管理规则
package com.alibaba.csp.sentinel.dashboard.rule.limit.gateway.api;

import cn.hutool.core.collection.CollUtil;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.ApiDefinitionEntity;
import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher;
import com.alibaba.csp.sentinel.dashboard.rule.nacos.NacosConfigUtil;
import com.alibaba.csp.sentinel.datasource.Converter;
import com.alibaba.csp.sentinel.util.AssertUtil;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.ConfigType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * @author whitebrocade
 * @version 1.0
 * @description: 网关API分组管理规则的发布者, 将sentinel-dashboard的配置的网关API分组管理规则推送到nacos中持久化存储
 */
@Component("gatewayApiNacosPublisher")
public class GatewayApiNacosPublisher implements DynamicRulePublisher<List<ApiDefinitionEntity>> { // DynamicRulePublisher> 这个T和之前的含义差不多, 这里就是你要推送的流控类型

    /**
     * 注入configService, 用于将流控规则推送到nacos中
     */
    @Autowired
    private ConfigService configService;

    /**
     * 注入转换器, 用于将流控规则转换为JSON字符串
     */
    @Autowired
    private Converter<List<ApiDefinitionEntity>, String> converter;

    @Override
    public void publish(String app, List<ApiDefinitionEntity> rules) throws Exception {
        AssertUtil.notEmpty(app, "app的name不能为空");
        // 如果规则为空就直接返回
        if (CollUtil.isEmpty(rules)) {
            return;
        }
        /*
        configService.publishConfig(String dataId, String group, String content, String type)
        作用: 将流控规则转换为JSON字符串, 并推送到nacos中
        方法参数解析:
            - dataId: nacos的dataId
            - group: 该配置文件所在的分组
            - content: 流控实体对象转换后的JSON字符串内容
            - type: 该配置文件内容所属的类型, 一般为JSON
         */
        configService.publishConfig(
            app + NacosConfigUtil.GATEWAY_API_GROUP_DATA_ID_POSTFIX,
            NacosConfigUtil.GROUP_ID,
            converter.convert(rules),
            ConfigType.JSON.getType());
    }
}

读取网关限流规则

注意了, 这里没有继承DynamicRulePublisher!而是直接自定义getRules方法

package com.alibaba.csp.sentinel.dashboard.rule.limit.gateway.flow;

import cn.hutool.core.util.StrUtil;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayFlowRuleEntity;
import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider;
import com.alibaba.csp.sentinel.dashboard.rule.nacos.NacosConfigUtil;
import com.alibaba.csp.sentinel.datasource.Converter;
import com.alibaba.nacos.api.config.ConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

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

/**
 * @author whitebrocade
 * @version 1.0
 * @description:  网关限流规则的发布者, 将sentinel-dashboard的配置的网关限流规则推送到nacos中持久化存储
 */
@Slf4j
@Component("gatewayFlowRulesNacosProvider")
public class GatewayFlowRulesNacosProvider {
    @Autowired
    private ConfigService configService;

    /**
     * 获取网关流控规则
     * @param app application服务名
     * @param ip ip地址
     * @param port 端口
     * @return 转换号的List
     * @throws Exception
     */
    public List<GatewayFlowRuleEntity> getRules(String app, String ip, Integer port) throws Exception {
        String jsonStr = configService.getConfig(
                app + NacosConfigUtil.GATEWAY_FLOW_DATA_ID_POSTFIX,
                NacosConfigUtil.GROUP_ID,
                3000);
        if (StrUtil.isEmpty(jsonStr)) {
            return new ArrayList<>();
        }

        // 将获取到的JSON字符串转换成GatewayFlowRule列表
        List<GatewayFlowRule> ruleList = JSON.parseArray(jsonStr, GatewayFlowRule.class);
        // 将GatewayFlowRule列表转换成GatewayFlowRuleEntity列表
        List<GatewayFlowRuleEntity> entityList = ruleList.stream()
                .map(rule -> GatewayFlowRuleEntity.fromGatewayFlowRule(app, ip, port, rule))
                .collect(Collectors.toList());
        log.info("JSON字符串:{}, " +
                "JSON->List:{}, " +
                "List->List:{},",
                jsonStr, JSONObject.toJSONString(ruleList), JSONObject.toJSONString(entityList));
        return entityList;
    }
}

推送网关限流规则

注意了, 这里没有继承DynamicRulePublisher!而是直接自定义getRules方法

package com.alibaba.csp.sentinel.dashboard.rule.limit.gateway.flow;

import cn.hutool.core.collection.CollUtil;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.ApiDefinitionEntity;
import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher;
import com.alibaba.csp.sentinel.dashboard.rule.nacos.NacosConfigUtil;
import com.alibaba.csp.sentinel.datasource.Converter;
import com.alibaba.csp.sentinel.util.AssertUtil;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.ConfigType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * @author whitebrocade
 * @version 1.0
 * @description: 网关限流规则的发布者, 将sentinel-dashboard的配置的网关限流规则推送到nacos中持久化存储
 */
@Slf4j
@Component("gatewayFlowRulesNacosPublisher")
public class GatewayFlowRulesNacosPublisher implements DynamicRulePublisher<List<ApiDefinitionEntity>> {
    @Autowired
    private ConfigService configService;

    @Autowired
    private Converter<List<ApiDefinitionEntity>, String> converter;

    @Override
    public void publish(String app, List<ApiDefinitionEntity> rules) throws Exception {
        AssertUtil.notEmpty(app, "app的name不能为空");
        if (CollUtil.isEmpty(rules)) {
            return;
        }
        log.info("推送的限流规则:{}", JSONObject.toJSONString(rules));
        configService.publishConfig(
            app + NacosConfigUtil.GATEWAY_FLOW_DATA_ID_POSTFIX,
            NacosConfigUtil.GROUP_ID,
            converter.convert(rules),
            ConfigType.JSON.getType());
    }
}

其他的流控规则持久化写法大差不差, 唯一需要注意的点就是规则之间的转换这里由于篇幅限制, 不再逐个列举说明

5. 修改GatewayApiController接口

GatewayApiController类所在路径:src/main/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayApiController.java

需要修改的接口概览

URL 方法名 请求方式 接口说明
/gateway/api/list.json queryApis GET 获取所有的API分组管理规则列表
/gateway/api/new.json addApi POST 新增API分组
/gateway/api/save.json updateApi POST 修改当前存在的API分组信息
/gateway/api/delete.json deleteApi POST 删除一条API分组信息

改动点概览:

  1. 获取规则: 内存获取 -> nacos获取
  2. 持久化规则: 持久化到内存 -> 持久化到nacos
1. 注入自定义的网关API分组管理规则相关的生产者和发布者
@RestController
@RequestMapping(value = "/gateway/api")
public class GatewayApiController {

    private final Logger logger = LoggerFactory.getLogger(GatewayApiController.class);

    @Autowired
    private InMemApiDefinitionStore repository;
    
	// ---------------- 改动 start ----------------
    /*
    1. SentinelApiClient类主要负责与 Sentinel 客户端通信,会发送HTTP调用客户端的API接口进行数据交互。其中主要是通过定义的一些方法将网关规则从内存中进行存取操作,具体方法可以查看相关代码。由于当前需要对网关规则进行从nacos存取操作,所以这里将其进行注释掉
    */
    // @Autowired
    // private SentinelApiClient sentinelApiClient;

    // 2. 注入自己的实现的GatewayApiNacosProvider和GatewayApiNacosPublisher
    @Autowired
    @Qualifier("gatewayApiNacosProvider")
    private DynamicRuleProvider<List<ApiDefinitionEntity>> gatewayApiProvider;

    @Autowired
    @Qualifier("gatewayApiNacosPublisher")
    private DynamicRulePublisher<List<ApiDefinitionEntity>> gatewayApiPublisher;
    // ---------------- 改动 end ----------------
    
    // 其余代码...
}
修改queryApis

修改前
gateway网关限流_第5张图片

修改后
gateway网关限流_第6张图片

修改后的代码如下

@GetMapping("/list.json")
@AuthAction(AuthService.PrivilegeType.READ_RULE)
public Result<List<ApiDefinitionEntity>> queryApis(String app, String ip, Integer port) {

    if (StringUtil.isEmpty(app)) {
        return Result.ofFail(-1, "app can't be null or empty");
    }
    if (StringUtil.isEmpty(ip)) {
        return Result.ofFail(-1, "ip can't be null or empty");
    }
    if (port == null) {
        return Result.ofFail(-1, "port can't be null");
    }

    try {
        
        // ---------------- 改动 start ----------------
        // 1. 注释掉sentinelApiClient.fetchApis, 因为这是从内存中获取的
        // List apis = sentinelApiClient.fetchApis(app, ip, port).get();
        // 2. 引入我们自己的获取方法逻辑
        // 获取规则
        List<ApiDefinitionEntity> apis = gatewayApiProvider.getRules(app);
        // 如果不为空就为规则设置上appName, ip, port, 然后保存到sentinel-dashboard中
        if (CollUtil.isNotEmpty(apis)) {
            for (ApiDefinitionEntity rule : apis) {
                rule.setApp(app.trim());
                rule.setIp(ip);
                rule.setPort(port);
            }
            repository.saveAll(apis);
        }
        // ---------------- 改动 end ----------------
        
        return Result.ofSuccess(apis);
    } catch (Throwable throwable) {
        logger.error("queryApis error:", throwable);
        return Result.ofThrowable(-1, throwable);
    }
}
修改addApi方法

修改前
gateway网关限流_第7张图片

修改后
gateway网关限流_第8张图片

修改后的代码如下

private boolean publishApis(String app, String ip, Integer port) {
    List<ApiDefinitionEntity> apis = repository.findAllByMachine(MachineInfo.of(app, ip, port));
    return sentinelApiClient.modifyApis(app, ip, port, apis);
}

/**
* 将网关API分组管理规则推送到nacos
* @param app 应用名
*/
// ---------------- 改动 end ----------------
private void publishApis(String app) {
    List<ApiDefinitionEntity> apis = repository.findAllByApp(app);
    try {
        // 注意了, 这个是我们自定书写的重构方法, 没有返回值
        gatewayApiPublisher.publish(app, apis);
    } catch (Exception e) {
        logger.warn("网关API管理规则推送nacos失败", e);
    }
}
// ---------------- 改动 end ----------------
@PostMapping("/new.json")
@AuthAction(AuthService.PrivilegeType.WRITE_RULE)
public Result<ApiDefinitionEntity> addApi(HttpServletRequest request, @RequestBody AddApiReqVo reqVo) {

    String app = reqVo.getApp();
    if (StringUtil.isBlank(app)) {
        return Result.ofFail(-1, "app can't be null or empty");
    }

    ApiDefinitionEntity entity = new ApiDefinitionEntity();
    entity.setApp(app.trim());

    String ip = reqVo.getIp();
    if (StringUtil.isBlank(ip)) {
        return Result.ofFail(-1, "ip can't be null or empty");
    }
    entity.setIp(ip.trim());

    Integer port = reqVo.getPort();
    if (port == null) {
        return Result.ofFail(-1, "port can't be null");
    }
    entity.setPort(port);

    // API名称
    String apiName = reqVo.getApiName();
    if (StringUtil.isBlank(apiName)) {
        return Result.ofFail(-1, "apiName can't be null or empty");
    }
    entity.setApiName(apiName.trim());

    // 匹配规则列表
    List<ApiPredicateItemVo> predicateItems = reqVo.getPredicateItems();
    if (CollectionUtils.isEmpty(predicateItems)) {
        return Result.ofFail(-1, "predicateItems can't empty");
    }

    List<ApiPredicateItemEntity> predicateItemEntities = new ArrayList<>();
    for (ApiPredicateItemVo predicateItem : predicateItems) {
        ApiPredicateItemEntity predicateItemEntity = new ApiPredicateItemEntity();

        // 匹配模式
        Integer matchStrategy = predicateItem.getMatchStrategy();
        if (!Arrays.asList(URL_MATCH_STRATEGY_EXACT, URL_MATCH_STRATEGY_PREFIX, URL_MATCH_STRATEGY_REGEX).contains(matchStrategy)) {
            return Result.ofFail(-1, "invalid matchStrategy: " + matchStrategy);
        }
        predicateItemEntity.setMatchStrategy(matchStrategy);

        // 匹配串
        String pattern = predicateItem.getPattern();
        if (StringUtil.isBlank(pattern)) {
            return Result.ofFail(-1, "pattern can't be null or empty");
        }
        predicateItemEntity.setPattern(pattern);

        predicateItemEntities.add(predicateItemEntity);
    }
    entity.setPredicateItems(new LinkedHashSet<>(predicateItemEntities));

    // 检查API名称不能重复
    List<ApiDefinitionEntity> allApis = repository.findAllByMachine(MachineInfo.of(app.trim(), ip.trim(), port));
    if (allApis.stream().map(o -> o.getApiName()).anyMatch(o -> o.equals(apiName.trim()))) {
        return Result.ofFail(-1, "apiName exists: " + apiName);
    }

    Date date = new Date();
    entity.setGmtCreate(date);
    entity.setGmtModified(date);

    try {
        
        // ---------------- 改动 start ----------------
        // 设置ID
        // 获取所有规则
        List<ApiDefinitionEntity> rules = gatewayApiProvider.getRules(entity.getApp());
        // 如果不为空, 就获取最大的ID, 然后新添加的规则就设置为这个ID+1
        if (CollUtil.isNotEmpty(rules)) {
            Optional<ApiDefinitionEntity> apiRule = rules.stream()
                .max(Comparator.comparingLong(ApiDefinitionEntity::getId));
            entity.setId(apiRule.get().getId() + 1L);
        }
        // ---------------- 改动 end ----------------
    } catch (Throwable throwable) {
        logger.error("add gateway api error:", throwable);
        return Result.ofThrowable(-1, throwable);
    }
    // ---------------- 改动 start ----------------
    /*if (!publishApis(app, ip, port)) {
            logger.warn("publish gateway apis fail after add");
        }*/
    publishApis(app);
    // ---------------- 改动 end ----------------
    return Result.ofSuccess(entity);
}
修改deleteApi方法

修改前
gateway网关限流_第9张图片

修改后
gateway网关限流_第10张图片

修改后的代码如下

@PostMapping("/delete.json")
@AuthAction(AuthService.PrivilegeType.DELETE_RULE)
public Result<Long> deleteApi(Long id) {
    if (id == null) {
        return Result.ofFail(-1, "id can't be null");
    }

    ApiDefinitionEntity oldEntity = repository.findById(id);
    if (oldEntity == null) {
        return Result.ofSuccess(null);
    }

    try {
        repository.delete(id);
    } catch (Throwable throwable) {
        logger.error("delete gateway api error:", throwable);
        return Result.ofThrowable(-1, throwable);
    }

    // ---------------- 改动 start ----------------
    /*if (!publishApis(oldEntity.getApp(), oldEntity.getIp(), oldEntity.getPort())) {
            logger.warn("publish gateway apis fail after delete");
        }*/
    publishApis(oldEntity.getApp());
    // ---------------- 改动 start ----------------

    return Result.ofSuccess(id);
}

6. 修改GatewayFlowRuleController接口

GatewayFlowRuleController类所在路径:src/main/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayFlowRuleController.java

需要修改的接口概览

URL 方法名 请求方式 接口说明
/gateway/flow/list.json queryFlowRules GET 获取所有的网关流控规则列表
/gateway/flow/new.json addFlowRule POST 新增网关流控规则
/gateway/flow/save.json updateFlowRule POST 修改当前存在的网关流控规则
/gateway/flow/delete.json deleteFlowRule POST 删除一条网关流控规则
1. 注入自定义的网关限流规则的生产者和发布者
// @Autowired
// private SentinelApiClient sentinelApiClient;

@Autowired
@Qualifier("gatewayFlowRulesNacosProvider")
private GatewayFlowRulesNacosProvider gatewayFlowProvider;

@Autowired
@Qualifier("gatewayFlowRulesNacosPublisher")
private DynamicRulePublisher<List<GatewayFlowRuleEntity>> gatewayFlowPublisher;
修改queryFlowRules方法

修改前
gateway网关限流_第11张图片

修改后
gateway网关限流_第12张图片

修改代码如下

@GetMapping("/list.json")
@AuthAction(AuthService.PrivilegeType.READ_RULE)
public Result<List<GatewayFlowRuleEntity>> queryFlowRules(String app, String ip, Integer port) {

    if (StringUtil.isEmpty(app)) {
        return Result.ofFail(-1, "app can't be null or empty");
    }
    if (StringUtil.isEmpty(ip)) {
        return Result.ofFail(-1, "ip can't be null or empty");
    }
    if (port == null) {
        return Result.ofFail(-1, "port can't be null");
    }

    try {
        // ---------------- 改动 start ----------------
        // List rules = sentinelApiClient.fetchGatewayFlowRules(app, ip, port).get();
        List<GatewayFlowRuleEntity> rules = gatewayFlowProvider.getRules(app, ip, port);
        if (CollUtil.isNotEmpty(rules)) {
            for (GatewayFlowRuleEntity rule : rules) {
                rule.setApp(app.trim());
                rule.setIp(ip);
                rule.setPort(port);
                // interval: 统计时间窗口的大小, 如果没有设置默认就为 1
                if (ObjUtil.isNull(rule.getInterval())) {
                    rule.setInterval(1L);
                }
                // intervalUnit: 统计时间窗口的时间单位, 如果没有设置, 那么就默认为秒
                if (ObjUtil.isNull(rule.getIntervalUnit())) {
                    rule.setIntervalUnit(INTERVAL_UNIT_SECOND);
                }

                // controlBehavior:流量整型的控制效果,同限流规则的 controlBehavior 字段
                // 目前支持快速失败和匀速排队两种模式,默认是快速失败
                // 0-快速失败 1-匀速排队
                if (ObjUtil.isNull(rule.getControlBehavior())) {
                    rule.setControlBehavior(0);
                }

                // burst:应对突发请求时额外允许的请求数目
                if (ObjUtil.isNull(rule.getBurst())) {
                    rule.setBurst(0);
                }
            }
            repository.saveAll(rules);
        }
        // ---------------- 改动 end ----------------
        return Result.ofSuccess(rules);
    } catch (Throwable throwable) {
        logger.error("query gateway flow rules error:", throwable);
        return Result.ofThrowable(-1, throwable);
    }
}
修改addFlowRule方法

修改前
gateway网关限流_第13张图片

修改后
gateway网关限流_第14张图片

gateway网关限流_第15张图片

修改代码如下

@PostMapping("/new.json")
@AuthAction(AuthService.PrivilegeType.WRITE_RULE)
public Result<GatewayFlowRuleEntity> addFlowRule(@RequestBody AddFlowRuleReqVo reqVo) {

    String app = reqVo.getApp();
    if (StringUtil.isBlank(app)) {
        return Result.ofFail(-1, "app can't be null or empty");
    }

    GatewayFlowRuleEntity entity = new GatewayFlowRuleEntity();
    entity.setApp(app.trim());

    String ip = reqVo.getIp();
    if (StringUtil.isBlank(ip)) {
        return Result.ofFail(-1, "ip can't be null or empty");
    }
    entity.setIp(ip.trim());

    Integer port = reqVo.getPort();
    if (port == null) {
        return Result.ofFail(-1, "port can't be null");
    }
    entity.setPort(port);

    // API类型, Route ID或API分组
    Integer resourceMode = reqVo.getResourceMode();
    if (resourceMode == null) {
        return Result.ofFail(-1, "resourceMode can't be null");
    }
    if (!Arrays.asList(RESOURCE_MODE_ROUTE_ID, RESOURCE_MODE_CUSTOM_API_NAME).contains(resourceMode)) {
        return Result.ofFail(-1, "invalid resourceMode: " + resourceMode);
    }
    entity.setResourceMode(resourceMode);

    // API名称
    String resource = reqVo.getResource();
    if (StringUtil.isBlank(resource)) {
        return Result.ofFail(-1, "resource can't be null or empty");
    }
    entity.setResource(resource.trim());

    // 针对请求属性
    GatewayParamFlowItemVo paramItem = reqVo.getParamItem();
    if (paramItem != null) {
        GatewayParamFlowItemEntity itemEntity = new GatewayParamFlowItemEntity();
        entity.setParamItem(itemEntity);

        // 参数属性 0-ClientIP 1-Remote Host 2-Header 3-URL参数 4-Cookie
        Integer parseStrategy = paramItem.getParseStrategy();
        if (!Arrays.asList(PARAM_PARSE_STRATEGY_CLIENT_IP, PARAM_PARSE_STRATEGY_HOST, PARAM_PARSE_STRATEGY_HEADER
                           , PARAM_PARSE_STRATEGY_URL_PARAM, PARAM_PARSE_STRATEGY_COOKIE).contains(parseStrategy)) {
            return Result.ofFail(-1, "invalid parseStrategy: " + parseStrategy);
        }
        itemEntity.setParseStrategy(paramItem.getParseStrategy());

        // 当参数属性为2-Header 3-URL参数 4-Cookie时,参数名称必填
        if (Arrays.asList(PARAM_PARSE_STRATEGY_HEADER, PARAM_PARSE_STRATEGY_URL_PARAM, PARAM_PARSE_STRATEGY_COOKIE).contains(parseStrategy)) {
            // 参数名称
            String fieldName = paramItem.getFieldName();
            if (StringUtil.isBlank(fieldName)) {
                return Result.ofFail(-1, "fieldName can't be null or empty");
            }
            itemEntity.setFieldName(paramItem.getFieldName());
        }

        String pattern = paramItem.getPattern();
        // 如果匹配串不为空,验证匹配模式
        if (StringUtil.isNotEmpty(pattern)) {
            itemEntity.setPattern(pattern);
            Integer matchStrategy = paramItem.getMatchStrategy();
            if (!Arrays.asList(PARAM_MATCH_STRATEGY_EXACT, PARAM_MATCH_STRATEGY_CONTAINS, PARAM_MATCH_STRATEGY_REGEX).contains(matchStrategy)) {
                return Result.ofFail(-1, "invalid matchStrategy: " + matchStrategy);
            }
            itemEntity.setMatchStrategy(matchStrategy);
        }
    }

    // 阈值类型 0-线程数 1-QPS
    Integer grade = reqVo.getGrade();
    if (grade == null) {
        return Result.ofFail(-1, "grade can't be null");
    }
    if (!Arrays.asList(FLOW_GRADE_THREAD, FLOW_GRADE_QPS).contains(grade)) {
        return Result.ofFail(-1, "invalid grade: " + grade);
    }
    entity.setGrade(grade);

    // QPS阈值
    Double count = reqVo.getCount();
    if (count == null) {
        return Result.ofFail(-1, "count can't be null");
    }
    if (count < 0) {
        return Result.ofFail(-1, "count should be at lease zero");
    }
    entity.setCount(count);

    // 间隔
    Long interval = reqVo.getInterval();
    if (interval == null) {
        return Result.ofFail(-1, "interval can't be null");
    }
    if (interval <= 0) {
        return Result.ofFail(-1, "interval should be greater than zero");
    }
    entity.setInterval(interval);

    // 间隔单位
    Integer intervalUnit = reqVo.getIntervalUnit();
    if (intervalUnit == null) {
        return Result.ofFail(-1, "intervalUnit can't be null");
    }
    if (!Arrays.asList(INTERVAL_UNIT_SECOND, INTERVAL_UNIT_MINUTE, INTERVAL_UNIT_HOUR, INTERVAL_UNIT_DAY).contains(intervalUnit)) {
        return Result.ofFail(-1, "Invalid intervalUnit: " + intervalUnit);
    }
    entity.setIntervalUnit(intervalUnit);

    // 流控方式 0-快速失败 2-匀速排队
    Integer controlBehavior = reqVo.getControlBehavior();
    if (controlBehavior == null) {
        return Result.ofFail(-1, "controlBehavior can't be null");
    }
    if (!Arrays.asList(CONTROL_BEHAVIOR_DEFAULT, CONTROL_BEHAVIOR_RATE_LIMITER).contains(controlBehavior)) {
        return Result.ofFail(-1, "invalid controlBehavior: " + controlBehavior);
    }
    entity.setControlBehavior(controlBehavior);

    if (CONTROL_BEHAVIOR_DEFAULT == controlBehavior) {
        // 0-快速失败, 则Burst size必填
        Integer burst = reqVo.getBurst();
        if (burst == null) {
            return Result.ofFail(-1, "burst can't be null");
        }
        if (burst < 0) {
            return Result.ofFail(-1, "invalid burst: " + burst);
        }
        entity.setBurst(burst);
    } else if (CONTROL_BEHAVIOR_RATE_LIMITER == controlBehavior) {
        // 1-匀速排队, 则超时时间必填
        Integer maxQueueingTimeoutMs = reqVo.getMaxQueueingTimeoutMs();
        if (maxQueueingTimeoutMs == null) {
            return Result.ofFail(-1, "maxQueueingTimeoutMs can't be null");
        }
        if (maxQueueingTimeoutMs < 0) {
            return Result.ofFail(-1, "invalid maxQueueingTimeoutMs: " + maxQueueingTimeoutMs);
        }
        entity.setMaxQueueingTimeoutMs(maxQueueingTimeoutMs);
    }

    Date date = new Date();
    entity.setGmtCreate(date);
    entity.setGmtModified(date);

    try {
        entity = repository.save(entity);
    } catch (Throwable throwable) {
        logger.error("add gateway flow rule error:", throwable);
        return Result.ofThrowable(-1, throwable);
    }

    // if (!publishRules(app, ip, port)) {
    //     logger.warn("publish gateway flow rules fail after add");
    // }
    try {
        List<GatewayFlowRuleEntity> rules = gatewayFlowProvider.getRules(entity.getApp());
        if (CollectionUtil.isNotEmpty(rules)) {
            Optional<GatewayFlowRuleEntity> gatewayFlowRule = rules.stream()
                .max(Comparator.comparingLong(GatewayFlowRuleEntity::getId));
            entity.setId(gatewayFlowRule.get().getId() + 1L);
        }
    } catch (Exception e) {
        logger.warn("get gateway flow rules error:", e);
    }

    return Result.ofSuccess(entity);
}

@PostMapping("/save.json")
@AuthAction(AuthService.PrivilegeType.WRITE_RULE)
public Result<GatewayFlowRuleEntity> updateFlowRule(@RequestBody UpdateFlowRuleReqVo reqVo) {

    String app = reqVo.getApp();
    if (StringUtil.isBlank(app)) {
        return Result.ofFail(-1, "app can't be null or empty");
    }

    Long id = reqVo.getId();
    if (id == null) {
        return Result.ofFail(-1, "id can't be null");
    }

    GatewayFlowRuleEntity entity = repository.findById(id);
    if (entity == null) {
        return Result.ofFail(-1, "gateway flow rule does not exist, id=" + id);
    }

    // 针对请求属性
    GatewayParamFlowItemVo paramItem = reqVo.getParamItem();
    if (paramItem != null) {
        GatewayParamFlowItemEntity itemEntity = new GatewayParamFlowItemEntity();
        entity.setParamItem(itemEntity);

        // 参数属性 0-ClientIP 1-Remote Host 2-Header 3-URL参数 4-Cookie
        Integer parseStrategy = paramItem.getParseStrategy();
        if (!Arrays.asList(PARAM_PARSE_STRATEGY_CLIENT_IP, PARAM_PARSE_STRATEGY_HOST, PARAM_PARSE_STRATEGY_HEADER
                           , PARAM_PARSE_STRATEGY_URL_PARAM, PARAM_PARSE_STRATEGY_COOKIE).contains(parseStrategy)) {
            return Result.ofFail(-1, "invalid parseStrategy: " + parseStrategy);
        }
        itemEntity.setParseStrategy(paramItem.getParseStrategy());

        // 当参数属性为2-Header 3-URL参数 4-Cookie时,参数名称必填
        if (Arrays.asList(PARAM_PARSE_STRATEGY_HEADER, PARAM_PARSE_STRATEGY_URL_PARAM, PARAM_PARSE_STRATEGY_COOKIE).contains(parseStrategy)) {
            // 参数名称
            String fieldName = paramItem.getFieldName();
            if (StringUtil.isBlank(fieldName)) {
                return Result.ofFail(-1, "fieldName can't be null or empty");
            }
            itemEntity.setFieldName(paramItem.getFieldName());
        }

        String pattern = paramItem.getPattern();
        // 如果匹配串不为空,验证匹配模式
        if (StringUtil.isNotEmpty(pattern)) {
            itemEntity.setPattern(pattern);
            Integer matchStrategy = paramItem.getMatchStrategy();
            if (!Arrays.asList(PARAM_MATCH_STRATEGY_EXACT, PARAM_MATCH_STRATEGY_CONTAINS, PARAM_MATCH_STRATEGY_REGEX).contains(matchStrategy)) {
                return Result.ofFail(-1, "invalid matchStrategy: " + matchStrategy);
            }
            itemEntity.setMatchStrategy(matchStrategy);
        }
    } else {
        entity.setParamItem(null);
    }

    // 阈值类型 0-线程数 1-QPS
    Integer grade = reqVo.getGrade();
    if (grade == null) {
        return Result.ofFail(-1, "grade can't be null");
    }
    if (!Arrays.asList(FLOW_GRADE_THREAD, FLOW_GRADE_QPS).contains(grade)) {
        return Result.ofFail(-1, "invalid grade: " + grade);
    }
    entity.setGrade(grade);

    // QPS阈值
    Double count = reqVo.getCount();
    if (count == null) {
        return Result.ofFail(-1, "count can't be null");
    }
    if (count < 0) {
        return Result.ofFail(-1, "count should be at lease zero");
    }
    entity.setCount(count);

    // 间隔
    Long interval = reqVo.getInterval();
    if (interval == null) {
        return Result.ofFail(-1, "interval can't be null");
    }
    if (interval <= 0) {
        return Result.ofFail(-1, "interval should be greater than zero");
    }
    entity.setInterval(interval);

    // 间隔单位
    Integer intervalUnit = reqVo.getIntervalUnit();
    if (intervalUnit == null) {
        return Result.ofFail(-1, "intervalUnit can't be null");
    }
    if (!Arrays.asList(INTERVAL_UNIT_SECOND, INTERVAL_UNIT_MINUTE, INTERVAL_UNIT_HOUR, INTERVAL_UNIT_DAY).contains(intervalUnit)) {
        return Result.ofFail(-1, "Invalid intervalUnit: " + intervalUnit);
    }
    entity.setIntervalUnit(intervalUnit);

    // 流控方式 0-快速失败 2-匀速排队
    Integer controlBehavior = reqVo.getControlBehavior();
    if (controlBehavior == null) {
        return Result.ofFail(-1, "controlBehavior can't be null");
    }
    if (!Arrays.asList(CONTROL_BEHAVIOR_DEFAULT, CONTROL_BEHAVIOR_RATE_LIMITER).contains(controlBehavior)) {
        return Result.ofFail(-1, "invalid controlBehavior: " + controlBehavior);
    }
    entity.setControlBehavior(controlBehavior);

    if (CONTROL_BEHAVIOR_DEFAULT == controlBehavior) {
        // 0-快速失败, 则Burst size必填
        Integer burst = reqVo.getBurst();
        if (burst == null) {
            return Result.ofFail(-1, "burst can't be null");
        }
        if (burst < 0) {
            return Result.ofFail(-1, "invalid burst: " + burst);
        }
        entity.setBurst(burst);
    } else if (CONTROL_BEHAVIOR_RATE_LIMITER == controlBehavior) {
        // 2-匀速排队, 则超时时间必填
        Integer maxQueueingTimeoutMs = reqVo.getMaxQueueingTimeoutMs();
        if (maxQueueingTimeoutMs == null) {
            return Result.ofFail(-1, "maxQueueingTimeoutMs can't be null");
        }
        if (maxQueueingTimeoutMs < 0) {
            return Result.ofFail(-1, "invalid maxQueueingTimeoutMs: " + maxQueueingTimeoutMs);
        }
        entity.setMaxQueueingTimeoutMs(maxQueueingTimeoutMs);
    }
    Date date = new Date();

    entity.setGmtModified(date);

    // ---------------- 改动 start ----------------
    try {
        // 设置ID
        List<GatewayFlowRuleEntity> rules = gatewayFlowProvider.getRules(app, ip, port);
        if (CollectionUtil.isNotEmpty(rules)) {
            Optional<GatewayFlowRuleEntity> gatewayFlowRule = rules.stream()
                .max(Comparator.comparingLong(GatewayFlowRuleEntity::getId));
            entity.setId(gatewayFlowRule.get().getId() + 1L);
        }

        entity = repository.save(entity);
    } catch (Throwable throwable) {
        logger.error("add gateway flow rule error:", throwable);
        return Result.ofThrowable(-1, throwable);
    }

    // if (!publishRules(app, entity.getIp(), entity.getPort())) {
    //     logger.warn("publish gateway flow rules fail after update");
    // }
    publishRules(app);
    // ---------------- 改动 end ----------------

    return Result.ofSuccess(entity);
}
/**
* 将网关限流规则推送到nacos中
* @param app application的应用程序名称
*/
private void publishRules(String app) {
    List<GatewayFlowRuleEntity> rules = repository.findAllByApp(app);
    try {
        gatewayFlowPublisher.publish(app, rules);
    } catch (Exception e) {
        logger.warn("publish gateway flow rules fail", e);;
    }
}
修改updateFlowRule方法

修改前
gateway网关限流_第16张图片

修改后
gateway网关限流_第17张图片

修改代码如下

@PostMapping("/save.json")
@AuthAction(AuthService.PrivilegeType.WRITE_RULE)
public Result<GatewayFlowRuleEntity> updateFlowRule(@RequestBody UpdateFlowRuleReqVo reqVo) {

    String app = reqVo.getApp();
    if (StringUtil.isBlank(app)) {
        return Result.ofFail(-1, "app can't be null or empty");
    }

    Long id = reqVo.getId();
    if (id == null) {
        return Result.ofFail(-1, "id can't be null");
    }

    GatewayFlowRuleEntity entity = repository.findById(id);
    if (entity == null) {
        return Result.ofFail(-1, "gateway flow rule does not exist, id=" + id);
    }

    // 针对请求属性
    GatewayParamFlowItemVo paramItem = reqVo.getParamItem();
    if (paramItem != null) {
        GatewayParamFlowItemEntity itemEntity = new GatewayParamFlowItemEntity();
        entity.setParamItem(itemEntity);

        // 参数属性 0-ClientIP 1-Remote Host 2-Header 3-URL参数 4-Cookie
        Integer parseStrategy = paramItem.getParseStrategy();
        if (!Arrays.asList(PARAM_PARSE_STRATEGY_CLIENT_IP, PARAM_PARSE_STRATEGY_HOST, PARAM_PARSE_STRATEGY_HEADER
                           , PARAM_PARSE_STRATEGY_URL_PARAM, PARAM_PARSE_STRATEGY_COOKIE).contains(parseStrategy)) {
            return Result.ofFail(-1, "invalid parseStrategy: " + parseStrategy);
        }
        itemEntity.setParseStrategy(paramItem.getParseStrategy());

        // 当参数属性为2-Header 3-URL参数 4-Cookie时,参数名称必填
        if (Arrays.asList(PARAM_PARSE_STRATEGY_HEADER, PARAM_PARSE_STRATEGY_URL_PARAM, PARAM_PARSE_STRATEGY_COOKIE).contains(parseStrategy)) {
            // 参数名称
            String fieldName = paramItem.getFieldName();
            if (StringUtil.isBlank(fieldName)) {
                return Result.ofFail(-1, "fieldName can't be null or empty");
            }
            itemEntity.setFieldName(paramItem.getFieldName());
        }

        String pattern = paramItem.getPattern();
        // 如果匹配串不为空,验证匹配模式
        if (StringUtil.isNotEmpty(pattern)) {
            itemEntity.setPattern(pattern);
            Integer matchStrategy = paramItem.getMatchStrategy();
            if (!Arrays.asList(PARAM_MATCH_STRATEGY_EXACT, PARAM_MATCH_STRATEGY_CONTAINS, PARAM_MATCH_STRATEGY_REGEX).contains(matchStrategy)) {
                return Result.ofFail(-1, "invalid matchStrategy: " + matchStrategy);
            }
            itemEntity.setMatchStrategy(matchStrategy);
        }
    } else {
        entity.setParamItem(null);
    }

    // 阈值类型 0-线程数 1-QPS
    Integer grade = reqVo.getGrade();
    if (grade == null) {
        return Result.ofFail(-1, "grade can't be null");
    }
    if (!Arrays.asList(FLOW_GRADE_THREAD, FLOW_GRADE_QPS).contains(grade)) {
        return Result.ofFail(-1, "invalid grade: " + grade);
    }
    entity.setGrade(grade);

    // QPS阈值
    Double count = reqVo.getCount();
    if (count == null) {
        return Result.ofFail(-1, "count can't be null");
    }
    if (count < 0) {
        return Result.ofFail(-1, "count should be at lease zero");
    }
    entity.setCount(count);

    // 间隔
    Long interval = reqVo.getInterval();
    if (interval == null) {
        return Result.ofFail(-1, "interval can't be null");
    }
    if (interval <= 0) {
        return Result.ofFail(-1, "interval should be greater than zero");
    }
    entity.setInterval(interval);

    // 间隔单位
    Integer intervalUnit = reqVo.getIntervalUnit();
    if (intervalUnit == null) {
        return Result.ofFail(-1, "intervalUnit can't be null");
    }
    if (!Arrays.asList(INTERVAL_UNIT_SECOND, INTERVAL_UNIT_MINUTE, INTERVAL_UNIT_HOUR, INTERVAL_UNIT_DAY).contains(intervalUnit)) {
        return Result.ofFail(-1, "Invalid intervalUnit: " + intervalUnit);
    }
    entity.setIntervalUnit(intervalUnit);

    // 流控方式 0-快速失败 2-匀速排队
    Integer controlBehavior = reqVo.getControlBehavior();
    if (controlBehavior == null) {
        return Result.ofFail(-1, "controlBehavior can't be null");
    }
    if (!Arrays.asList(CONTROL_BEHAVIOR_DEFAULT, CONTROL_BEHAVIOR_RATE_LIMITER).contains(controlBehavior)) {
        return Result.ofFail(-1, "invalid controlBehavior: " + controlBehavior);
    }
    entity.setControlBehavior(controlBehavior);

    if (CONTROL_BEHAVIOR_DEFAULT == controlBehavior) {
        // 0-快速失败, 则Burst size必填
        Integer burst = reqVo.getBurst();
        if (burst == null) {
            return Result.ofFail(-1, "burst can't be null");
        }
        if (burst < 0) {
            return Result.ofFail(-1, "invalid burst: " + burst);
        }
        entity.setBurst(burst);
    } else if (CONTROL_BEHAVIOR_RATE_LIMITER == controlBehavior) {
        // 2-匀速排队, 则超时时间必填
        Integer maxQueueingTimeoutMs = reqVo.getMaxQueueingTimeoutMs();
        if (maxQueueingTimeoutMs == null) {
            return Result.ofFail(-1, "maxQueueingTimeoutMs can't be null");
        }
        if (maxQueueingTimeoutMs < 0) {
            return Result.ofFail(-1, "invalid maxQueueingTimeoutMs: " + maxQueueingTimeoutMs);
        }
        entity.setMaxQueueingTimeoutMs(maxQueueingTimeoutMs);
    }
    Date date = new Date();

    entity.setGmtModified(date);

    try {
        entity = repository.save(entity);
    } catch (Throwable throwable) {
        logger.error("add gateway flow rule error:", throwable);
        return Result.ofThrowable(-1, throwable);
    }

    // ---------------- 改动 start ----------------
    // if (!publishRules(app, entity.getIp(), entity.getPort())) {
    //     logger.warn("publish gateway flow rules fail after update");
    // }
    publishRules(app);
    // ---------------- 改动 end ----------------

    return Result.ofSuccess(entity);
}
修改deleteFlowRule方法

修改前
gateway网关限流_第18张图片

修改后
gateway网关限流_第19张图片

修改代码如下

@PostMapping("/delete.json")
@AuthAction(AuthService.PrivilegeType.DELETE_RULE)
public Result<Long> deleteFlowRule(Long id) {

    if (id == null) {
        return Result.ofFail(-1, "id can't be null");
    }

    GatewayFlowRuleEntity oldEntity = repository.findById(id);
    if (oldEntity == null) {
        return Result.ofSuccess(null);
    }

    try {
        repository.delete(id);
    } catch (Throwable throwable) {
        logger.error("delete gateway flow rule error:", throwable);
        return Result.ofThrowable(-1, throwable);
    }

    // ---------------- 改动 start ----------------
    // if (!publishRules(oldEntity.getApp(), oldEntity.getIp(), oldEntity.getPort())) {
    //     logger.warn("publish gateway flow rules fail after delete");
    // }
    publishRules(oldEntity.getApp());
    // ---------------- 改动 end ----------------

    return Result.ofSuccess(id);
}

参考资料

gateway集成redis

Spring Cloud Gateway 限流适配多规则的解决方案)

限流10万QPS、跨域、过滤器、令牌桶算法-网关Gateway内容都在这儿)

面试必备:四种经典限流算法讲解)

SpringCloudGateway源码(四)限流组件)

[分布式限流方案(gateway限流,redis+lua实现限流,nginx限流)](https://zhuanlan.zhihu.com/p/571518397#:~:text=实现方案 :Guava RateLimiter限流 Guava RateLimiter是一个谷歌提供的限流,其基于令牌桶算法,比较适用于单实例的系统。 限流具体实现,网关限流: Spring Cloud Gateway中提供了RequestRateLimiterGatewayFilterFactory类,这个是基于令牌桶实现的。 它内置RedisReteLimiter%2C依赖于Redis存储限流配置和统计数据,我们也可以通过继承 org.springframework.cloud.gateway.filter.ratelimit.AbstractRateLimiter 或者是实现)

gateway集成sentinel

阿里巴巴开源限流系统 Sentinel 全解析)

Spring Cloud Gateway 限流实战,终于有人写清楚了)

sentinel官方文档)

Spring Cloud Alibaba:Sentinel实现熔断与限流)

SpringCloudAlibaba全网最全讲解6️⃣之Sentinel)

Sentinel系列源码)

阿里sentinel与springboot整合实践——根据request信息限流

Sentinel规则持久化(1.8.+版)

Sentinel规则持久化到Nacos及规则数据双向同步

Sentinel1.8.6 Gateway网关流控+动态Nacos数据源双向持久化

你可能感兴趣的:(gateway,java,sentinel,redis)