Spring Cloud Gateway限制接口调用频率

1,227 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 5 天,点击查看活动详情

假设我们使用Spring Cloud实现了一个Controller接口用于更新文本内容,并且该接口的入参是一个JSON对象,其中包含一个名为"sqlId"的字段。

要通过Spring Cloud Gateway来控制该接口的调用频率,可以使用Spring Cloud Gateway提供的RateLimiter过滤器。在过滤器中,可以使用Redis等分布式缓存来实现基于sqlId的限流策略。

以下是一个示例过滤器,它使用Redis和sqlId字段来实现限流:

public class SqlIdRateLimiterGatewayFilterFactory extends AbstractGatewayFilterFactory<SqlIdRateLimiterGatewayFilterFactory.Config> {
    private final RedisRateLimiter redisRateLimiter;

    public SqlIdRateLimiterGatewayFilterFactory(RedisRateLimiter redisRateLimiter) {
        super(Config.class);
        this.redisRateLimiter = redisRateLimiter;
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            String sqlId = exchange.getRequest().getQueryParams().getFirst("sqlId");
            if (sqlId == null) {
                return chain.filter(exchange);
            }
            String key = "sqlId:" + sqlId;
            return this.redisRateLimiter.isAllowed(key, config.getRate())
                    .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);
                        } else {
                            exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
                            return exchange.getResponse().setComplete();
                        }
                    });
        };
    }

    public static class Config {
        private int rate = 1;

        public int getRate() {
            return rate;
        }

        public void setRate(int rate) {
            this.rate = rate;
        }
    }
}

在这个示例过滤器中,我们使用sqlId参数作为限流的key,然后使用RedisRateLimiter来实现限流。Config类用于配置限流速率,您可以通过调整rate属性来设置不同sqlId的请求频率限制。

在Spring Cloud Gateway配置文件中,可以使用SqlIdRateLimiterGatewayFilterFactory类来创建限流过滤器,并将其应用于Controller接口。以下是一个示例配置文件:

spring:
  cloud:
    gateway:
      routes:
        - id: update-route
          uri: lb://update-service
          predicates:
            - Path=/update
          filters:
            - name: SqlIdRateLimiter
              args:
                rate: 1

在这个示例配置文件中,我们创建了一个名为update-route的路由,并将它映射到/update路径。我们还添加了一个名为SqlIdRateLimiter的过滤器,并将它应用于update-route路由。由于我们在过滤器中使用了sqlId参数,所以需要将该参数添加到请求路径中,例如/update?sqlId=xxxxx

不使用redis的方法

如果不使用Redis,也可以通过Spring Cloud Gateway实现上述效果,但是实现方式可能会有所不同。

一种可能的方式是使用Spring Cloud Gateway提供的InMemoryRateLimiter类。这个类提供了一个基于内存的限流实现。您可以通过在过滤器中使用InMemoryRateLimiter类来实现基于sqlId的限流。

以下是一个示例过滤器,它使用InMemoryRateLimitersqlId字段来实现限流:

public class SqlIdRateLimiterGatewayFilterFactory extends AbstractGatewayFilterFactory<SqlIdRateLimiterGatewayFilterFactory.Config> {
    private final RateLimiter<InMemoryRateLimiter.RedisRateLimiterConfig> rateLimiter;

    public SqlIdRateLimiterGatewayFilterFactory() {
        super(Config.class);
        this.rateLimiter = new InMemoryRateLimiter<>(InMemoryRateLimiter.RedisRateLimiterConfig.class);
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            String sqlId = exchange.getRequest().getQueryParams().getFirst("sqlId");
            if (sqlId == null) {
                return chain.filter(exchange);
            }
            String key = "sqlId:" + sqlId;
            RateLimiter.Response response = this.rateLimiter.isAllowed(key, config.getRate());
            if (response.isAllowed()) {
                return chain.filter(exchange);
            } else {
                exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
                return exchange.getResponse().setComplete();
            }
        };
    }

    public static class Config {
        private int rate = 1;

        public int getRate() {
            return rate;
        }

        public void setRate(int rate) {
            this.rate = rate;
        }
    }
}

在这个示例过滤器中,我们使用InMemoryRateLimiter类来实现限流。Config类用于配置限流速率,可以通过调整rate属性来设置不同sqlId的请求频率限制。

在Spring Cloud Gateway配置文件中,可以使用SqlIdRateLimiterGatewayFilterFactory类来创建限流过滤器,并将其应用于Controller接口。以下是一个示例配置文件:

spring:
  cloud:
    gateway:
      routes:
        - id: update-route
          uri: lb://update-service
          predicates:
            - Path=/update
          filters:
            - name: SqlIdRateLimiter
              args:
                rate: 1

请注意,使用InMemoryRateLimiter的限流实现可能会存在一些限制。例如,它可能无法处理大量的请求,也无法在分布式环境中使用。因此,如果需要更高级的限流策略,可能需要考虑使用Redis等外部缓存或限流服务。

获取请求体里的id

如果sqlId是在请求body中而不是请求路径中,那么可以通过对请求body进行解析来实现限流。具体来说,您可以使用Spring Cloud Gateway提供的GlobalFilter或GatewayFilter来获取请求body并解析出其中的sqlId,然后将其传递给限流算法进行限制。

以下是一个示例实现:

@Component
public class RateLimiterFilter implements GatewayFilter {

    private final Map<String, RateLimiter> limiterMap = new ConcurrentHashMap<>();

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 获取请求body中的sqlId
        Mono<String> sqlIdMono = exchange.getRequest().getBody()
                .map(dataBuffer -> {
                    byte[] bytes = new byte[dataBuffer.readableByteCount()];
                    dataBuffer.read(bytes);
                    return new String(bytes, StandardCharsets.UTF_8);
                })
                .map(json -> {
                    JSONObject obj = JSONObject.parseObject(json);
                    return obj.getString("sqlId");
                });

        // 获取或创建对应的限流器
        String sqlId = sqlIdMono.block();
        RateLimiter limiter = limiterMap.computeIfAbsent(sqlId, id -> RateLimiter.create(10));

        // 尝试获取许可
        if (limiter.tryAcquire()) {
            return chain.filter(exchange);
        } else {
            exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
            return exchange.getResponse().setComplete();
        }
    }
}

在这个例子中,我们使用了ConcurrentHashMap来保存每个sqlId对应的限流器,并使用RateLimiter库来实现令牌桶算法。我们首先获取请求body中的sqlId,然后从limiterMap中获取或创建对应的限流器。最后,我们尝试从限流器中获取许可,如果成功则允许请求通过,否则返回429 Too Many Requests响应。由于这种方式实现限流需要解析请求body,因此可能会对性能产生一定的影响。

分布式系统中应用该方案

由于限流器必须在所有节点上保持一致,因此必须将限流器状态存储在共享的分布式存储中,以便不同的节点可以共享同一个限流器状态。可以考虑使用分布式存储技术,例如Redis,Zookeeper等来存储限流器状态。具体来说,可以将限流器状态存储在Redis中,并使用Redisson等分布式工具来实现限流器的分布式同步。