Spring Cloud Gateway源码解析-12-令牌桶限流(RequestRateLimiterGatewayFilterFactory)


系列文章

创作不易,如果对您有帮助,麻烦辛苦下小手点个关注,有任何问题都可以私信交流哈。
祝您虎年虎虎生威。


SCG中默认使用了Redis来实现令牌桶限流,通过Java代码调用lua脚本实现。

RequestRateLimiterGatewayFilterFactory


RequestRateLimiterGatewayFilterFactory是SCG的限流GatewayFilter的工厂

public GatewayFilter apply(Config config) {
		KeyResolver resolver = getOrDefault(config.keyResolver, defaultKeyResolver);
		RateLimiter<Object> limiter = getOrDefault(config.rateLimiter,
				defaultRateLimiter);
		boolean denyEmpty = getOrDefault(config.denyEmptyKey, this.denyEmptyKey);
		HttpStatusHolder emptyKeyStatus = HttpStatusHolder
				.parse(getOrDefault(config.emptyKeyStatus, this.emptyKeyStatusCode));

		return (exchange, chain) -> resolver.resolve(exchange).defaultIfEmpty(EMPTY_KEY)
				.flatMap(key -> {
					if (EMPTY_KEY.equals(key)) {
						if (denyEmpty) {
							setResponseStatus(exchange, emptyKeyStatus);
							return exchange.getResponse().setComplete();
						}
						return chain.filter(exchange);
					}
					String routeId = config.getRouteId();
					if (routeId == null) {
						Route route = exchange
								.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
						routeId = route.getId();
					}
					//重点,调用具体的限流实现isAllowed方法判断当前请求是否允许被执行,默认为RedisRateLimiter
					return limiter.isAllowed(routeId, key).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);
						}

						setResponseStatus(exchange, config.getStatusCode());
						return exchange.getResponse().setComplete();
					});
				});
	}

RedisRateLimiter

public Mono<Response> isAllowed(String routeId, String id) {
		if (!this.initialized.get()) {
			throw new IllegalStateException("RedisRateLimiter is not initialized");
		}

		Config routeConfig = loadConfiguration(routeId);

		/**
		 * 从字面意思理解为补充率,也就是令牌的补充率,但是SCG的注释为每秒允许用户的请求数
		 * 可以想象,一个桶往外流水,一个人用勺子取水,我们想控制这个人取水的速度,是不是可以通过控制往桶里面加水的速度从而控制取水的速度
		 * 也就是通过流入可以控制流出,这里就是这个意思
		 */
		// How many requests per second do you want a user to be allowed to do?
		int replenishRate = routeConfig.getReplenishRate();
		//令牌桶的容量
		// How much bursting do you want to allow?
		int burstCapacity = routeConfig.getBurstCapacity();
		//每次请求消耗的令牌数量
		// How many tokens are requested per request?
		int requestedTokens = routeConfig.getRequestedTokens();

		try {
			List<String> keys = getKeys(id);

			// The arguments to the LUA script. time() returns unixtime in seconds.
			List<String> scriptArgs = Arrays.asList(replenishRate + "",
					burstCapacity + "", Instant.now().getEpochSecond() + "",
					requestedTokens + "");
			// allowed, tokens_left = redis.eval(SCRIPT, keys, args)
			//调用lua脚本
			Flux<List<Long>> flux = this.redisTemplate.execute(this.script, keys,
					scriptArgs);
			// .log("redisratelimiter", Level.FINER);
			return flux.onErrorResume(throwable -> {
				if (log.isDebugEnabled()) {
					log.debug("Error calling rate limiter lua", throwable);
				}
				return Flux.just(Arrays.asList(1L, -1L));
			}).reduce(new ArrayList<Long>(), (longs, l) -> {
				//将lua脚本返回的两个值放入list中
				longs.addAll(l);
				return longs;
			}).map(results -> {
				//判断lua脚本返回的是否是1,如果是1,表示允许请求
				boolean allowed = results.get(0) == 1L;
				//获取到lua脚本返回的剩余令牌数
				Long tokensLeft = results.get(1);
				//拼装返回Response
				Response response = new Response(allowed,
						getHeaders(routeConfig, tokensLeft));

				if (log.isDebugEnabled()) {
					log.debug("response: " + response);
				}
				return response;
			});
		}
		catch (Exception e) {
			//当Redis发生异常时的操作
			/*
			 * We don't want a hard dependency on Redis to allow traffic. Make sure to set
			 * an alert so you know if this is happening too much. Stripe's observed
			 * failure rate is 0.01%.
			 */
			log.error("Error determining if user allowed from redis", e);
		}
		//最后的兜底,当上边异常时,都允许请求通过
		return Mono.just(new Response(true, getHeaders(routeConfig, -1L)));
	}

request_rate_limiter.lua

SCG的Redis lua脚本

local tokens_key = KEYS[1] --request_rate_limiter.{'id'}.tokens
local timestamp_key = KEYS[2] --request_rate_limiter.{'id'}.timestamp
--redis.log(redis.LOG_WARNING, "tokens_key " .. tokens_key)

local rate = tonumber(ARGV[1]) -- 允许用户每秒执行的请求数(填充令牌的速度) 20
local capacity = tonumber(ARGV[2]) -- 令牌桶的容量 50
--第一次请求1618841535
--第二次请求1618841537
local now = tonumber(ARGV[3]) -- 当前时间戳
local requested = tonumber(ARGV[4]) -- 每个请求消耗的令牌数 2

local fill_time = capacity/rate --容量/填充令牌的速度 ---  填充的次数
local ttl = math.floor(fill_time*2) -- key的过期时间

--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)

--第一次请求 last_tokens = 50
--第二次请求 last_tokens = 48
local last_tokens = tonumber(redis.call("get", tokens_key)) --获取最后一次请求后剩余的令牌数量
if last_tokens == nil then  --如果剩余的令牌为空,则初始化为容量
  last_tokens = capacity
end
--redis.log(redis.LOG_WARNING, "last_tokens " .. last_tokens)

--第一次请求 last_refreshed = 0
--第二次请求 last_refreshed = 1618841535
local last_refreshed = tonumber(redis.call("get", timestamp_key)) -- 获取最后一次请求的时间
if last_refreshed == nil then -- 如果最后一次请求的时间为空,则设置为0
  last_refreshed = 0
end
--redis.log(redis.LOG_WARNING, "last_refreshed " .. last_refreshed)

--第一次请求 delta=(0 , 1618841535 - 0 ) = 1618841535
--第二次请求 delta = (0 , 1618841537 - 1618841535) = 2
local delta = math.max(0, now-last_refreshed) --当前时间减去最后刷新时间 -----  两次请求的时间差
--第一次请求 filled_tokens = math.min(50 , 50 + (1618841535 * 20)) = 50 --- 总容量
--第二次请求 filled_tokens = math.min(50 , 48 + (2 * 20)) = 50,这里我们可以发现,当两次请求的间隔大于1s时,都会将filled_tokens重新设置为桶的最大容量
    --当两次请求的间隔小于1s时,math.min函数才会取","后边的值,当然,小于1s时,delta就是0了,因此filled_tokens就是上次剩余的令牌数量
local filled_tokens = math.min(capacity, last_tokens+(delta*rate)) -- 计算
-- 第一次请求 50 >= 2 允许
local allowed = filled_tokens >= requested --当filled_tokens >= 一次请求消耗的令牌数时,则允许

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)

if ttl > 0 then
  --设置剩余令牌数
  redis.call("setex", tokens_key, ttl, new_tokens)
  --设置本次请求时间戳
  redis.call("setex", timestamp_key, ttl, now)
end

-- return { allowed_num, new_tokens, capacity, filled_tokens, requested, new_tokens }
return { allowed_num, new_tokens }

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

壹氿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值