开启掘金成长之旅!这是我参与「掘金日新计划 · 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的限流。
以下是一个示例过滤器,它使用InMemoryRateLimiter和sqlId字段来实现限流:
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等分布式工具来实现限流器的分布式同步。