本文主要研究下spring cloud gateway的RedisRateLimiterhtml
spring-cloud-gateway-core-2.0.0.RELEASE-sources.jar!/org/springframework/cloud/gateway/config/GatewayRedisAutoConfiguration.javajava
@Configuration @AutoConfigureAfter(RedisReactiveAutoConfiguration.class) @AutoConfigureBefore(GatewayAutoConfiguration.class) @ConditionalOnBean(ReactiveRedisTemplate.class) @ConditionalOnClass({RedisTemplate.class, DispatcherHandler.class}) class GatewayRedisAutoConfiguration { @Bean @SuppressWarnings("unchecked") public RedisScript redisRequestRateLimiterScript() { DefaultRedisScript redisScript = new DefaultRedisScript<>(); redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("META-INF/scripts/request_rate_limiter.lua"))); redisScript.setResultType(List.class); return redisScript; } @Bean //TODO: replace with ReactiveStringRedisTemplate in future public ReactiveRedisTemplate<String, String> stringReactiveRedisTemplate( ReactiveRedisConnectionFactory reactiveRedisConnectionFactory) { RedisSerializer<String> serializer = new StringRedisSerializer(); RedisSerializationContext<String , String> serializationContext = RedisSerializationContext .<String, String>newSerializationContext() .key(serializer) .value(serializer) .hashKey(serializer) .hashValue(serializer) .build(); return new ReactiveRedisTemplate<>(reactiveRedisConnectionFactory, serializationContext); } @Bean @ConditionalOnMissingBean public RedisRateLimiter redisRateLimiter(ReactiveRedisTemplate<String, String> redisTemplate, @Qualifier(RedisRateLimiter.REDIS_SCRIPT_NAME) RedisScript<List<Long>> redisScript, Validator validator) { return new RedisRateLimiter(redisTemplate, redisScript, validator); } }
這裏建立了3個bean,分別是RedisScript、ReactiveRedisTemplate、RedisRateLimiter
spring-cloud-gateway-core-2.0.0.RELEASE-sources.jar!/org/springframework/cloud/gateway/filter/ratelimit/RedisRateLimiter.javareact
@ConfigurationProperties("spring.cloud.gateway.redis-rate-limiter") public class RedisRateLimiter extends AbstractRateLimiter<RedisRateLimiter.Config> implements ApplicationContextAware { //...... public static final String CONFIGURATION_PROPERTY_NAME = "redis-rate-limiter"; public static final String REDIS_SCRIPT_NAME = "redisRequestRateLimiterScript"; public static final String REMAINING_HEADER = "X-RateLimit-Remaining"; public static final String REPLENISH_RATE_HEADER = "X-RateLimit-Replenish-Rate"; public static final String BURST_CAPACITY_HEADER = "X-RateLimit-Burst-Capacity"; //...... public RedisRateLimiter(ReactiveRedisTemplate<String, String> redisTemplate, RedisScript<List<Long>> script, Validator validator) { super(Config.class, CONFIGURATION_PROPERTY_NAME, validator); this.redisTemplate = redisTemplate; this.script = script; initialized.compareAndSet(false, true); } public RedisRateLimiter(int defaultReplenishRate, int defaultBurstCapacity) { super(Config.class, CONFIGURATION_PROPERTY_NAME, null); this.defaultConfig = new Config() .setReplenishRate(defaultReplenishRate) .setBurstCapacity(defaultBurstCapacity); } //...... @Override @SuppressWarnings("unchecked") public void setApplicationContext(ApplicationContext context) throws BeansException { if (initialized.compareAndSet(false, true)) { this.redisTemplate = context.getBean("stringReactiveRedisTemplate", ReactiveRedisTemplate.class); this.script = context.getBean(REDIS_SCRIPT_NAME, RedisScript.class); if (context.getBeanNamesForType(Validator.class).length > 0) { this.setValidator(context.getBean(Validator.class)); } } } /* for testing */ Config getDefaultConfig() { return defaultConfig; } /** * This uses a basic token bucket algorithm and relies on the fact that Redis scripts * execute atomically. No other operations can run between fetching the count and * writing the new count. */ @Override @SuppressWarnings("unchecked") public Mono<Response> isAllowed(String routeId, String id) { if (!this.initialized.get()) { throw new IllegalStateException("RedisRateLimiter is not initialized"); } Config routeConfig = getConfig().getOrDefault(routeId, defaultConfig); if (routeConfig == null) { throw new IllegalArgumentException("No Configuration found for route " + routeId); } // 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(); 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() + "", "1"); // allowed, tokens_left = redis.eval(SCRIPT, keys, args) Flux<List<Long>> flux = this.redisTemplate.execute(this.script, keys, scriptArgs); // .log("redisratelimiter", Level.FINER); 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) { /* * 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))); } static List<String> getKeys(String id) { // use `{}` around keys to use Redis Key hash tags // this allows for using redis cluster // Make a unique key per user. String prefix = "request_rate_limiter.{" + id; // You need two Redis keys for Token Bucket. String tokenKey = prefix + "}.tokens"; String timestampKey = prefix + "}.timestamp"; return Arrays.asList(tokenKey, timestampKey); } //...... }
spring-cloud-gateway-core-2.0.0.RELEASE-sources.jar!/META-INF/scripts/request_rate_limiter.luaredis
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", tokens_key)) 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", timestamp_key)) 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", tokens_key, ttl, new_tokens) redis.call("setex", timestamp_key, ttl, now) return { allowed_num, new_tokens }
public HashMap<String, String> getHeaders(Config config, Long tokensLeft) { HashMap<String, String> headers = new HashMap<>(); headers.put(this.remainingHeader, tokensLeft.toString()); headers.put(this.replenishRateHeader, String.valueOf(config.getReplenishRate())); headers.put(this.burstCapacityHeader, String.valueOf(config.getBurstCapacity())); return headers; }
RELEASE版本新增返回了rate limit相關的header:X-RateLimit-Remaining、X-RateLimit-Replenish-Rate、X-RateLimit-Burst-Capacity
spring cloud gateway默認提供了一個基於redis的限流filter,須要添加依賴spring-boot-starter-data-redis-reactive才能夠自動開啓。該filter使用的是redisScript來進行判斷,該script使用的是request_rate_limiter.lua腳本。spring