前提,已集成springcloudgateway相关pom,如lettuce-core、spring-cloud-starter-gateway等基础pom
1、从限流需要的基础pom开始:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
我使用的springboot版本:2.1.4.RELEASE springcloud版本:Greenwich.RELEASE
2、config编写,这个一般网上搜索有3种,path、host、请求参数如:user
@Configuration
public class LimitingConfig {
/**
* 使用请求 IP 作为限流键
*
* @return key
*/
@Bean(name = "hostKeyResolver")
KeyResolver pathKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
}
}
3、配置文件(以properties为例,可自行转换yml)
server.port=9001
spring.application.name=limitService
# redis配置
spring.redis.host=127.0.0.1
spring.redis.password=redis
spring.redis.port=6379
spring.redis.database=0
#routes[id] id数值以自身项目为准
# 使用请求来源 IP 作为限流键
spring.cloud.gateway.routes[4].id=limitRouter
# 如果是用注册中心可以直接写应用名,如果不是可写url地址如 http://127.0.0.1:9002
spring.cloud.gateway.routes[4].uri=lb://limitService
# 请求路径为/api/limit/get
spring.cloud.gateway.routes[4].predicates[0]=Path=/api/limit/get
# 通过截取1层,实际访问的controller接口地址为/limit/get
spring.cloud.gateway.routes[4].filters[0]=StripPrefix=1
# 不可更改
spring.cloud.gateway.routes[4].filters[1].name=RequestRateLimiter
# 使用SpEL表达式从Spring容器中获取Bean对象
spring.cloud.gateway.routes[4].filters[1].args.key-resolver=#{@hostKeyResolver}
# 令牌桶每秒填充平均速率
spring.cloud.gateway.routes[4].filters[1].args.redis-rate-limiter.replenishRate=1
# 令牌桶的上限 突发周期为 burstCapacity(2) / replenishRate(1)秒,如果周期内有2/1次请求突发的情况,则第2/1次会有部分请求丢失,返回HTTP 429 - Too Many Requests
spring.cloud.gateway.routes[4].filters[1].args.redis-rate-limiter.burstCapacity=2
4、如果使用单机redis可使用压测工具直接压测访问 http://127.0.0.1:9001/api/limit/get 接口,如使用上例中配置,超过2个请求,后面的将会返回429状态码
5、如果使用的是阿里云集群,默认request_rate_limiter.lua执行存在问题,
--在debug情况下,会有以下2个错误
-- NOSCRIPT No matching script. Please use EVAL.
-- 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
解决方法:需要在自身项目对应目录(META-INF/scripts/request_rate_limiter.lua,源码:GatewayRedisAutoConfiguration.java)下重写该文件,覆盖jar中的原始文件修改如下:
--踩坑参照 https://blog.csdn.net/qq_33996921/article/details/107204362
--错误
--NOSCRIPT No matching script. Please use EVAL.
--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
--注释掉 tokens_key、timestamp_key 在引用的地址直接填写数组
--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", KEYS[1]))
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", KEYS[2]))
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)
redis.call("setex", KEYS[1], ttl, new_tokens)
redis.call("setex", KEYS[2], ttl, now)
return { allowed_num, new_tokens }