背景介绍:
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 timeoutInstant 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 limitreturn limitCount.getAndIncrement() >= rate;}
