一、如何使用
根据SpringCloudGateway官方文档介绍,支持结合RedisRateLimiter实现分布式限流,需要引入spring-boot-starter-data-redis-reactive依赖,使用时由用户自定义的参数如下:
- redis-rate-limiter.repullishRate表示令牌桶每秒的填充速率。
- redis-rate-limiter.burstCapacity表示允许用户在一秒钟内允许消耗的最大令牌数,同时也是令牌桶可以容纳的令牌数上限,将此值设置为零将阻止所有的请求。
- redis-rate-limiter.requestedTokens表示单次请求消耗多少个令牌,默认为1。
- keyResolver表示限流的key,可以通过实现KeyResolver接口开发扩展,如设置为请求客户端IP、或请求接口URL等
The Redis implementation is based off of work done at [Stripe](https://stripe.com/blog/rate-limiters).
It requires the use of the `spring-boot-starter-data-redis-reactive` Spring Boot starter.
The redis-rate-limiter.replenishRate property is how many requests per second you want a user to be allowed to do, without any dropped requests. This is the rate at which the token bucket is filled.
The redis-rate-limiter.burstCapacity property is the maximum number of requests a user is allowed to do in a single second. This is the number of tokens the token bucket can hold. Setting this value to zero blocks all requests.
The redis-rate-limiter.requestedTokens property is how many tokens a request costs. This is the number of tokens taken from the bucket for each request and defaults to 1.
二、原理分析
限流的实现在springframework-cloud-gateway-server依赖包中,由两部分组成:
- Java限流调用方:org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiter
public Mono<Response> isAllowed(String routeId, String id) {
if (!this.initialized.get()) {
throw new IllegalStateException("RedisRateLimiter is not initialized");
} else {
RedisRateLimiter.Config routeConfig = this.loadConfiguration(routeId);
int replenishRate = routeConfig.getReplenishRate();
int burstCapacity = routeConfig.getBurstCapacity();
int requestedTokens = routeConfig.getRequestedTokens();
try {
List<String> keys = getKeys(id);
List<String> scriptArgs = Arrays.asList(replenishRate + "", burstCapacity + "", Instant.now().getEpochSecond() + "", requestedTokens + "");
// 调用lua脚本获取令牌:返回1表示成功,允许请求执行;返回0表示失败,拒绝请求;
Flux<List<Long>> flux = this.redisTemplate.execute(this.script, keys, scriptArgs);
- Lua算法实现:request_rate_limiter.lua,代码附上注释
// Redis中令牌Key,由调用方传参。举例为key_1
local tokens_key = KEYS[1]
// Redis中令牌上次更新时间Key。举例为key_1
local timestamp_key = KEYS[2]
// 令牌每秒的填充速率,由调用方传参。举例为1
local rate = tonumber(ARGV[1])
// 令牌桶的容量。举例为10
local capacity = tonumber(ARGV[2])
// 脚本调用的时间,精确到秒。举例为2023.2.9 17:25:28
local now = tonumber(ARGV[3])
// 脚本调用消耗多少个令牌,通常默认为1。举例为1
local requested = tonumber(ARGV[4])
// 令牌桶填充满需要的时间。10/1=10
local fill_time = capacity/rate
// 令牌Key超时时间。10*2=20
local ttl = math.floor(fill_time*2)
// 令牌桶中剩余的令牌数量。举例为5
local last_tokens = tonumber(redis.call("get", tokens_key))
if last_tokens == nil then
last_tokens = capacity
end
// 令牌上次更新时间。举例为2023.2.9 17:25:25
local last_refreshed = tonumber(redis.call("get", timestamp_key))
if last_refreshed == nil then
last_refreshed = 0
end
// 令牌更新时间间隔。2023.2.9 17:25:28 - 2023.2.9 17:25:25 = 3
local delta = math.max(0, now-last_refreshed)
// 令牌数量计算规则:令牌桶内剩余数量+3秒时间应该填充的令牌数量
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
// 是否通过限流的判断,通过为1,未通过为0
local allowed = filled_tokens >= requested
// 令牌桶内更新的令牌数量
local new_tokens = filled_tokens
// 返回给调用方的结果:1表示成功,2表示失败
local allowed_num = 0
if allowed then
// 令牌桶更新的令牌数量,需要减去本次请求消耗的令牌数量
new_tokens = filled_tokens - requested
allowed_num = 1
end
if ttl > 0 then
// 更新令牌key_1的过期时间,令牌数量
redis.call("setex", tokens_key, ttl, new_tokens)
// 更新令牌更新时间key_2的过期时间,为本次调用时间
redis.call("setex", timestamp_key, ttl, now)
end
// 返回调用结果:allowed_num表示调用成功或失败,new_tokens表示令牌桶内的剩余令牌数量
return { allowed_num, new_tokens }