背景介绍:

1、由于目标服务器不能执行 Redis 的 EVAL 命令,所以需要一个替代 RRateLimiter的方案;
2、参考代码中的isLimited方法可以用于 SpringBoot 的某个 Filter 中统一调用,在前置位置进行限流判断;

实现思路:

利用 Redis 的 Hash 结构存储2个值用于计算:

  • time:用于保存上次时间间隔的时间记录点
  • value:用于保存被限流的 key 值的累加值

每次请求过来时,通过isLimited方法来判断当前URL是否满足如下条件:

  • 当前时间(now)减去限流间隔时间(intervalSeconds)后与上次记录时间(time) 对比
  • 判断当前 URL 对应的 value 值是否大于等于配置的 rate 值

核心参考代码:

  1. // from config:
  2. private final int rate = 10;
  3. private final int intervalSeconds = 60;
  4. public boolean isLimited(String key) {
  5. RBucket<Object> bucket = redissonClient.getBucket(key.concat("-locker"));
  6. // 这里的 while 循环是通过重试来解决 set 值失败的情况
  7. int retryCount = 0;
  8. while (!bucket.trySet(true, 100, TimeUnit.MILLISECONDS) && retryCount < 100) {
  9. try {
  10. retryCount++;
  11. Thread.sleep(1);
  12. } catch (InterruptedException e) {
  13. log.warn("isLimited has an error:{0}.", e);
  14. return true;
  15. }
  16. }
  17. try {
  18. return isLimitedAfterLock(key);
  19. } finally {
  20. bucket.delete();
  21. }
  22. }
  23. private boolean isLimitedAfterLock(String key) {
  24. Instant now = Instant.now();
  25. RBucket<Long> limitTime = redissonClient.getBucket(key.concat("-time"));
  26. RAtomicLong limitCount = redissonClient.getAtomicLong(key.concat("-count"));
  27. if (!limitTime.isExists()) {
  28. limitTime.set(now.toEpochMilli());
  29. limitCount.set(1);
  30. return false;
  31. }
  32. // reset limitMap when timeout
  33. Instant recordTime = Instant.ofEpochMilli(limitMap.get());
  34. Instant currentTime = now.minusSeconds(intervalSeconds);
  35. if (currentTime.compareTo(recordTime) > 0) {
  36. limitTime.set(now.toEpochMilli());
  37. limitCount.set(1);
  38. return false;
  39. }
  40. // increase value when not limit
  41. return limitCount.getAndIncrement() >= rate;
  42. }