① pom 依赖

这里一定要注意,是网关引入的 redis-reactive,背压模式的 redis。

<!--基于 reactive stream 的redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>

② 配置按照请求 IP 的限流

spring:
  cloud:
    gateway:
      routes:
      - id: requestratelimiter_route
        uri: lb://pig-upms
        order: 10000
        predicates:
        - Path=/admin/**
        filters:
        - name: RequestRateLimiter
          args:
            redis-rate-limiter.replenishRate: 1  # 令牌桶每秒填充平均速率
            redis-rate-limiter.burstCapacity: 3  # 令牌桶总容量
            key-resolver: "#{@remoteAddrKeyResolver}" #SPEL 表达式去的对应的 bean
        - StripPrefix=1
  • 配置 bean,多维度限流量的入口 对应上边 key-resolver
/**
* 自定义限流标志的 key,多个维度可以从这里入手
* exchange 对象中获取服务 ID、请求信息,用户信息等
*/
@Bean
KeyResolver remoteAddrKeyResolver() {
    return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}

OK 完成。

③ 压力测试

并发 5 个线程。

Redis 数据变化

我们使用 redis 的monitor 命令,实时查看 redis 的操作情况。 会发现在 redis 中会操作两个 key

  • request_rate_limiter..timestamp
  • request_rate_limiter..tokens

④ 实现原理

Spring Cloud Gateway 默认实现 Redis 限流,如果扩展只需要实现 ratelimter 接口即可。

核心代码
  • 判断是否取到令牌的实现,通过调用 redis 的 LUA 脚本。
public Mono<Response> isAllowed(String routeId, String id) {
	Config routeConfig = getConfig().getOrDefault(routeId, defaultConfig);
	int replenishRate = routeConfig.getReplenishRate();
	int burstCapacity = routeConfig.getBurstCapacity();

	try {
		List<String> keys = getKeys(id);
		returns unixtime in seconds.
		List<String> scriptArgs = Arrays.asList(replenishRate + "", burstCapacity + "",
				Instant.now().getEpochSecond() + "", "1");
		// 这里是核心,执行 redis 的 LUA 脚本。
		Flux<List<Long>> flux =
		this.redisTemplate.execute(this.script, keys, scriptArgs);
		return flux.onErrorResume(throwable -> Flux.just(Arrays.asList(1L, -1L)))
				.reduce(new ArrayList<Long>(), (longs, l) -> {
					longs.addAll(l);
					return longs;
				}) .map(results -> {
					boolean allowed = results.get(0) == 1L;
					Long tokensLeft = results.get(1);

					Response response = new Response(allowed, getHeaders(routeConfig, tokensLeft));

					if (log.isDebugEnabled()) {
						log.debug("response: " + response);
					}
					return response;
				});
	}
	catch (Exception e) {
		log.error("Error determining if user allowed from redis", e);
	}
	return Mono.just(new Response(true, getHeaders(routeConfig, -1L)));
}
LUA 脚本

♥️ 获取支持

遇到问题?

如果您在使用过程中遇到任何问题、有功能建议或需求,请点击此卡片前往 Gitee 仓库提交 Issue。