java单机滑动窗口限流器

12 阅读2分钟

下面使用 ConcurrentHashMap + ConcurrentLinkedDeque 实现一个滑动窗口限流器,限制在 10 秒内最多 N 次请求(例如 5 次)。这种方案只依赖本地内存,适合单机限流场景。


实现原理

  • 为每个用户(或 IP)维护一个 双端队列,存储每次请求的时间戳(毫秒)。

  • 每次请求时,先清理队列中 超过 10 秒 的旧时间戳,再判断队列大小是否小于阈值:

    • 若小于,则添加当前时间戳,返回 允许
    • 否则,返回 限流
  • 使用 ConcurrentHashMap 保证对用户 Key 的线程安全,使用 synchronized 对每个队列做同步,避免并发修改。


代码实现

java

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.TimeUnit;

/**
 * 基于 Map 的滑动窗口限流器(10秒限频)
 */
public class LocalSlidingWindowRateLimiter {
    // 存储每个用户的请求时间戳队列
    private final Map<String, ConcurrentLinkedDeque<Long>> userQueueMap = new ConcurrentHashMap<>();
    private final int windowSeconds;   // 窗口大小(秒)
    private final int maxRequests;     // 窗口内最大请求数

    public LocalSlidingWindowRateLimiter(int windowSeconds, int maxRequests) {
        this.windowSeconds = windowSeconds;
        this.maxRequests = maxRequests;
    }

    /**
     * 检查是否允许请求
     * @param key 限流标识(如 userId、ip)
     * @return true 允许,false 被限流
     */
    public boolean allowRequest(String key) {
        long now = System.currentTimeMillis();
        long windowStart = now - TimeUnit.SECONDS.toMillis(windowSeconds);

        // 获取或创建该用户的队列
        ConcurrentLinkedDeque<Long> deque = userQueueMap.computeIfAbsent(key,
                k -> new ConcurrentLinkedDeque<>());

        // 对队列加锁,保证清理和添加的原子性
        synchronized (deque) {
            // 1. 清理过期记录(窗口外的旧时间戳)
            while (!deque.isEmpty() && deque.peekFirst() < windowStart) {
                deque.pollFirst();
            }

            // 2. 判断是否超过阈值
            if (deque.size() < maxRequests) {
                // 允许请求,记录当前时间戳
                deque.addLast(now);
                return true;
            } else {
                return false;
            }
        }
    }

    // 可选:定期清理无数据的用户 Key,避免内存泄漏(可以用 ScheduledExecutorService 定时清理)
    public void cleanEmptyKeys() {
        userQueueMap.entrySet().removeIf(entry -> entry.getValue().isEmpty());
    }
}

使用示例

java

public class Main {
    public static void main(String[] args) throws InterruptedException {
        // 限流规则:10秒内最多5次请求
        LocalSlidingWindowRateLimiter limiter = new LocalSlidingWindowRateLimiter(10, 5);
        String userId = "user_123";

        for (int i = 0; i < 10; i++) {
            boolean allowed = limiter.allowRequest(userId);
            System.out.println("请求 " + (i + 1) + ":" + (allowed ? "通过" : "限流"));
            Thread.sleep(1000); // 模拟间隔 1 秒
        }
    }
}

输出示例(第 6 次请求被限流,因为 10 秒内已有 5 次):

text

请求 1:通过
请求 2:通过
请求 3:通过
请求 4:通过
请求 5:通过
请求 6:限流
请求 7:限流
...

注意事项

  1. 内存清理:长期不活动的用户 Key 会残留队列对象,建议定时调用 cleanEmptyKeys() 清理。
  2. 并发安全:每个队列使用 synchronized 锁保证原子操作,锁粒度小,性能较高。
  3. 时间精度:使用毫秒级时间戳,滑动窗口边界判断精确。
  4. 适用场景:单机应用、对分布式一致性要求不高的限流场景。