背景介绍:
1、由于目标服务器不能执行 Redis 的 EVAL
命令,所以需要一个替代 RRateLimiter
的方案;
2、参考代码中的isLimited
方法可以用于 SpringBoot 的某个 Filter 中统一调用,在前置位置进行限流判断;
实现思路:
利用 Redis 的 Hash 结构存储2个值用于计算:
- time:用于保存上次时间间隔的时间记录点
- value:用于保存被限流的 key 值的累加值
每次请求过来时,通过isLimited
方法来判断当前URL是否满足如下条件:
- 当前时间(now)减去限流间隔时间(intervalSeconds)后与上次记录时间(time) 对比
- 判断当前 URL 对应的 value 值是否大于等于配置的 rate 值
核心参考代码:
// from config:
private final int rate = 10;
private final int intervalSeconds = 60;
public boolean isLimited(String key) {
RBucket<Object> bucket = redissonClient.getBucket(key.concat("-locker"));
// 这里的 while 循环是通过重试来解决 set 值失败的情况
int retryCount = 0;
while (!bucket.trySet(true, 100, TimeUnit.MILLISECONDS) && retryCount < 100) {
try {
retryCount++;
Thread.sleep(1);
} catch (InterruptedException e) {
log.warn("isLimited has an error:{0}.", e);
return true;
}
}
try {
return isLimitedAfterLock(key);
} finally {
bucket.delete();
}
}
private boolean isLimitedAfterLock(String key) {
Instant now = Instant.now();
RBucket<Long> limitTime = redissonClient.getBucket(key.concat("-time"));
RAtomicLong limitCount = redissonClient.getAtomicLong(key.concat("-count"));
if (!limitTime.isExists()) {
limitTime.set(now.toEpochMilli());
limitCount.set(1);
return false;
}
// reset limitMap when timeout
Instant recordTime = Instant.ofEpochMilli(limitMap.get());
Instant currentTime = now.minusSeconds(intervalSeconds);
if (currentTime.compareTo(recordTime) > 0) {
limitTime.set(now.toEpochMilli());
limitCount.set(1);
return false;
}
// increase value when not limit
return limitCount.getAndIncrement() >= rate;
}