public Mono<Response> isAllowed(String routeId, String id) {
if (!this.initialized.get()) {
throw new IllegalStateException("RedisRateLimiter is not initialized");
}
Config routeConfig = loadConfiguration(routeId);
// How many requests per second do you want a user to be allowed to do?
// 一秒钟的请求次数,即qps,就是往令牌桶放的速度
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.
//LUA 脚本的参数,注意的是取出的时候从1开始,lua好像是这样定义的
List<String> scriptArgs = Arrays.asList(replenishRate + "",
burstCapacity + "", Instant.now().getEpochSecond() + "", "1");
// allowed, tokens_left = redis.eval(SCRIPT, keys, args)
// eval和evalsha是redis2.6支持lua的执行,Redis使用Lua解释器解释Lua脚本过程中,会保证脚本是以原子性的方式执行
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 -> {
//可以看到lua脚本返回多个返回值,一个是时候能够拿到令牌的标识,一个是剩余的令牌
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);
}
分析这个lua脚本
-- token的key
local tokens_key = KEYS[1]
-- 时间戳的key
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
-- redis 过期时间 这里为什么是放满令牌桶的两倍
-- 这个时间不能太长,加入太长10s,你第一秒把令牌拿完,后面9s,就会出现突刺现象
local ttl = math.floor(fill_time*2)
-- 获取令牌桶的数量,如果为空,将令牌桶容量赋值
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)
-- 获取最后的更新时间戳,如果为空,设置为0
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
-- 零代表false
local allowed_num = 0
-- 如果允许获取得到,计算出剩余的令牌数量,并标记可以获取
if allowed then
new_tokens = filled_tokens - requested
allowed_num = 1
end
-- 存到redis
redis.call("setex", tokens_key, ttl, new_tokens)
redis.call("setex", timestamp_key, ttl, now)
-- lua可以返回多个字段,java获取时用List获取
return { allowed_num, new_tokens }
开始根据业务需求重构
创建RateLimiterConf,这个用于配置限流策略
@Component
@ConfigurationProperties(prefix = "ratelimiter-conf")
public class RateLimiterConf {
//处理速度
private static final String DEFAULT_REPLENISHRATE = "default.replenishRate";
//容量
private static final String DEFAULT_BURSTCAPACITY = "default.burstCapacity";
private Map<String, Integer> rateLimitMap = new ConcurrentHashMap<String, Integer>() {
{
put(DEFAULT_REPLENISHRATE, 100);
put(DEFAULT_BURSTCAPACITY, 1000);
}
};
public Map<String, Integer> getRateLimitMap() {
return rateLimitMap;
}
public void setRateLimitMap(Map<String, Integer> rateLimitMap) {
this.rateLimitMap = rateLimitMap;
}
}