/** * Copyright (c) 2018-2019, Jie Li 李杰 ([email protected]). * <p> * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <p> * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.momo.momopermissiongateway.limit; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeansException; import org.springframework.cloud.gateway.filter.ratelimit.AbstractRateLimiter; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.annotation.Primary; import org.springframework.data.redis.core.ReactiveRedisTemplate; import org.springframework.data.redis.core.script.RedisScript; import org.springframework.stereotype.Component; import org.springframework.validation.Validator; import org.springframework.validation.annotation.Validated; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import javax.validation.constraints.Min; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; /** * @ClassName: RateCheckRedisRateLimiter * @Author: Jie Li * @Date 2019-10-15 18:00 * @description: redis限流 * @Version: 1.0 * <p>Copyright: Copyright (c) 2019</p> **/ @Component @Primary @Slf4j public class RateCheckRedisRateLimiter extends AbstractRateLimiter<RateCheckRedisRateLimiter.Config> implements ApplicationContextAware { public static final String CONFIGURATION_PROPERTY_NAME = "redis-rate-limiter"; public static final String REDIS_SCRIPT_NAME = "redisRequestRateLimiterScript"; private ReactiveRedisTemplate<String, String> redisTemplate; private RedisScript<List<Long>> script; private AtomicBoolean initialized = new AtomicBoolean(false); private Config defaultConfig; public RateCheckRedisRateLimiter() { super(Config.class, CONFIGURATION_PROPERTY_NAME, null); } public RateCheckRedisRateLimiter(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 RateCheckRedisRateLimiter(int defaultReplenishRate, int defaultBurstCapacity) { super(Config.class, CONFIGURATION_PROPERTY_NAME, null); this.defaultConfig = new Config() .setReplenishRate(defaultReplenishRate) .setBurstCapacity(defaultBurstCapacity); } private Config setConfig(String key) { //TODO 根据key(接口)找到对应的限流配置 int replenishRate = 1;//令牌桶每秒填充平均速率 int burstCapacity = 10;//令牌桶总容量 defaultConfig = new Config() .setReplenishRate(replenishRate) .setBurstCapacity(burstCapacity); return defaultConfig; } @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 public Mono<Response> isAllowed(String routeId, String id) { if (!this.initialized.get()) { throw new IllegalStateException("RedisRateLimiter is not initialized"); } log.info("key:{}", id); //根据key(接口)找到对应的限流配置 Config routeConfig = setConfig(id); // 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.info("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, 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, -1)); } private 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); } @Validated public static class Config { @Min(1) private int replenishRate; @Min(0) private int burstCapacity = 0; public int getReplenishRate() { return replenishRate; } public Config setReplenishRate(int replenishRate) { this.replenishRate = replenishRate; return this; } public int getBurstCapacity() { return burstCapacity; } public Config setBurstCapacity(int burstCapacity) { this.burstCapacity = burstCapacity; return this; } @Override public String toString() { return "Config{" + "replenishRate=" + replenishRate + ", burstCapacity=" + burstCapacity + '}'; } } }