java单机滑动窗口限流器

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


实现原理

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

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

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


代码实现

java

arduino 复制代码
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

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

erlang 复制代码
请求 1:通过
请求 2:通过
请求 3:通过
请求 4:通过
请求 5:通过
请求 6:限流
请求 7:限流
...

注意事项

  1. 内存清理 :长期不活动的用户 Key 会残留队列对象,建议定时调用 cleanEmptyKeys() 清理。
  2. 并发安全 :每个队列使用 synchronized 锁保证原子操作,锁粒度小,性能较高。
  3. 时间精度:使用毫秒级时间戳,滑动窗口边界判断精确。
  4. 适用场景:单机应用、对分布式一致性要求不高的限流场景。
相关推荐
一条Go2 小时前
OnCall 时段路由:跨午夜、左闭右开与「Local 不是时区」
后端·架构·github
Moment2 小时前
不会后期的我,发现了一个开源胶片神器😍😍😍
前端·后端·开源
数据库小学妹2 小时前
pgbouncer 事务池:prepared statement、临时表与会话状态
数据库·后端·postgresql
三小河2 小时前
SWC 凭什么比 Babel 快 ?项目优化迁移踩坑记录
前端·后端
鶴哥只手遮天3 小时前
《Redis 8.10.3 深度剖析》系列 ·第 17 章. 一个迟到十六年的类型
redis·后端·开源
知守观3 小时前
同一个团队两套分包:DDD 四层落地后,domain 层最大的类还是 5885 行
java·后端·领域驱动设计
Anymous3 小时前
我手写的 Redis 锁,输给了 10 秒的 TTL
后端·架构
Rain的Java大神之路4 小时前
设计一套生产级 API 接口:从契约、安全、幂等到限流,我踩过的坑都在这了
java·后端·面试
SimonKing4 小时前
零配置也能白嫖前沿大模型!dsh-our-free-model插件真香警告
java·后端·程序员
nice_lcj5204 小时前
【从零理解 Spring AI · 15】让检索更准:先宽召回,再精排序
java·后端·rag·spring ai·ai应用开发·检索优化·重排序