接口防刷限流,这个Java滑动窗口工具类11个方法不怕校时
## 📋 目录 - 一、工具类功能 - 二、所需环境 - 三、方法与调用示例(按需调用) - 四、工具类源码(可直接复制) ## 一、工具类功能 按「资源 key + 时间窗口内最大请求数」放行请求,接口防刷、验证码频率、第三方配额保护都能用,时间基准取 nanoTime 单调时钟,系统校时与时间回拨都不会让限流突然失效。 ## 二、所需环境 JDK 17或以上 ## 三、方法与调用示例(按需调用) 🔢 **方法序号**:1 🎯 **方法用途**: - 默认单例上按「资源 key + 窗口内最大请求数」放行,一次消耗 1 个配额 - 验证码发送频率、接口防刷这类不想自己管实例的场景直接用它 🧩 **方法定义**:`public static boolean tryAcquire(String key, int maxRequests, Duration window)` 🗣️ **入参解释**:`public static boolean tryAcquire(资源key, 阈值, 窗口)` 📤 **返回值解释**:`boolean`,拿到配额返回 `true`,超限返回 `false` 💡 **边界口径**: - 先剔除窗口外的旧记录再判额度,是滑动窗口而不是固定窗口 - 判定用 `System.nanoTime()` 单调时钟,系统时间回拨、NTP 校时都不影响窗口 - 额度为 0、key 为 null 或空白、窗口为 0 或负数都抛 `IllegalArgumentException` - 这是单机限流,多实例部署时全局放行量是阈值乘实例数 🧪 **方法示例**: - 60 秒窗口额度 2,`tryAcquire(phone, 2, SMS_WINDOW)` 第 1 次拿到 `true` - 同一 key 连打到第 3 次拿到 `false`,窗口里的记录还在就不放行 - 额度传 0 抛 `maxRequests 必须大于 0,当前为0` - key 传空白串抛 `key 不能为null或空白` --- 🔢 **方法序号**:2 🎯 **方法用途**: - 默认单例上一次批量消耗多个配额,按笔计额度时用 - 一次调用要发 5 条短信、扣 5 个配额这类场景用它 🧩 **方法定义**:`public static boolean tryAcquire(String key, int permits, int maxRequests, Duration window)` 🗣️ **入参解释**:`public static boolean tryAcquire(资源key, 本次配额数, 阈值, 窗口)` 📤 **返回值解释**:`boolean`,额度够就整笔扣掉并返回 `true` 💡 **边界口径**: - 额度不足时整笔不扣,不会先扣掉一部分再说 - `permits` 大于 `maxRequests` 属于参数非法,直接抛异常,这种请求永远过不去 - `permits` 小于 1 同样抛 `IllegalArgumentException` 🧪 **方法示例**: - `tryAcquire("batch-api", 5, 8, IP_WINDOW)` 额度 8 一次扣 5,拿到 `true` - 同一 key 再来一笔要 4 个,只剩 3 个,拿到 `false` - `acquire("y", 0, 3, window)` 抛 `permits 必须大于 0,当前为0` - `permits` 比总额度还大时抛 `permits 不能大于 maxRequests,当前为5 > 3,这样的请求永远无法通过` --- 🔢 **方法序号**:3 🎯 **方法用途**: - 取默认单例,静态入口操作的就是它 - 想用 `remaining`、`retryAfter`、清理与观测这些实例方法时从它拿 🧩 **方法定义**:`public static RateLimiterUtils defaultLimiter()` 🗣️ **入参解释**:`public static RateLimiterUtils defaultLimiter()`(无入参) 📤 **返回值解释**:`RateLimiterUtils`,进程内唯一的默认实例 💡 **边界口径**: - 返回的是同一个对象,多次取回的结果可直接比引用 - 静态 `tryAcquire` 写的桶在这里都能看见 - 需要按业务隔离限流桶时另用 `new RateLimiterUtils()`,别共用这个单例 🧪 **方法示例**: - `defaultLimiter().size()` 在静态入口刚打过一次之后拿到 `1` - `defaultLimiter().retryAfter(phone, 2, SMS_WINDOW)` 拿到 `PT59.9946621S` - 连续两次 `defaultLimiter()` 比引用,拿到 `true` --- 🔢 **方法序号**:4 🎯 **方法用途**: - 在自己的实例上消耗 1 个配额,桶与其他实例互不影响 - 按业务模块隔离限流范围时用,比如接口限流和短信限流各一套 🧩 **方法定义**:`public boolean acquire(String key, int maxRequests, Duration window)` 🗣️ **入参解释**:`public boolean acquire(资源key, 阈值, 窗口)` 📤 **返回值解释**:`boolean`,放行返回 `true` 💡 **边界口径**: - 同一 key 走 `ConcurrentHashMap.compute` 串行化,不同 key 互不阻塞 - 多线程并发不会各拿一条队列,也不会超放 - 静态入口与新建实例是两套桶,同一个 key 各限各的 🧪 **方法示例**: - 独立实例上同一 IP 连打 5 次,额度 8,`acquire(ip, 8, IP_WINDOW)` 拿到 `5 次放行`,剩余 3 - 静态入口已把手机号打满,新实例上 `acquire(phone, 2, SMS_WINDOW)` 仍拿到 `true` - 16 线程各抢 20 次、总额度 100,拿到 `实际放行 100 次`,没有超放 - 300 毫秒窗口额度 2,睡过窗口后 `acquire("slide", 2, window)` 拿到 `true` --- 🔢 **方法序号**:5 🎯 **方法用途**: - 实例上的批量配额入口,一次消耗 `permits` 个 - 按批扣额度的业务(一次发多条、一批扣多个令牌)用这一版 🧩 **方法定义**:`public boolean acquire(String key, int permits, int maxRequests, Duration window)` 🗣️ **入参解释**:`public boolean acquire(资源key, 本次配额数, 阈值, 窗口)` 📤 **返回值解释**:`boolean`,额度够整笔扣掉返回 `true` 💡 **边界口径**: - 不足时整笔不扣,剩余数量保持原值 - 被拒时如果队列已经被剔空,这个 key 会从表里摘掉,不留空桶 - 校验口径与单个配额入口一致 🧪 **方法示例**: - 剩 3 个额度时 `acquire(ip, 4, 8, IP_WINDOW)` 拿到 `false`,剩余仍是 `3` - 改成要 3 个正好够,`acquire(ip, 3, 8, IP_WINDOW)` 拿到 `true`,剩余 `0` --- 🔢 **方法序号**:6 🎯 **方法用途**: - 查某个 key 在窗口内还剩多少配额,不消耗 - 给前端展示「还能发几次」、做灰度阈值判断时用 🧩 **方法定义**:`public int remaining(String key, int maxRequests, Duration window)` 🗣️ **入参解释**:`public int remaining(资源key, 阈值, 窗口)` 📤 **返回值解释**:`int`,剩余配额,不小于 0 💡 **边界口径**: - 没有限流记录的 key 直接给满额 - 查询同样会剔除过期记录,队列被剔空时顺手把 key 摘掉 - 传进来的 `maxRequests` 与 `window` 就是判定口径,和写入时不一致会得到另一个结果 🧪 **方法示例**: - 打满之后 `remaining(phone, 2, SMS_WINDOW)` 拿到 `0 次` - 从没出现过的 key `remaining("13900005678", 2, SMS_WINDOW)` 拿到 `2 次` - 剩 1 次时查完 `remaining("x", 3, window)` 拿到 `2`,查完占桶数仍是 `1` --- 🔢 **方法序号**:7 🎯 **方法用途**: - 算下一次可以放行的等待时长,直接当 `Retry-After` 响应头用 - 被限流时要告诉对方等多久,别只回 429 让人盲重试 🧩 **方法定义**:`public Duration retryAfter(String key, int maxRequests, Duration window)` 🗣️ **入参解释**:`public Duration retryAfter(资源key, 阈值, 窗口)` 📤 **返回值解释**:`Duration`,有额度给 0,满额给最早放行的等待时间 💡 **边界口径**: - 等待时长按窗口里最老那条记录算,窗口滑到它过期即可放行 - 没记录或还有额度时给 `Duration.ZERO`,不是负数 - 结果是本次调用的瞬时值,纳秒级抖动正常,给 HTTP 头记得向上取整 🧪 **方法示例**: - 额度打满后 `retryAfter(phone, 2, SMS_WINDOW)` 拿到 `PT59.9946621S`,约 59 秒 - 300 毫秒窗口刚打满时拿到 `PT0.2866983S` - 有额度时 `retryAfter("never-seen", 5, window)` 拿到 `PT0S` --- 🔢 **方法序号**:8 🎯 **方法用途**: - 清掉单个 key 的计数,让它立刻回到满额 - 用户完成验证码校验、申诉通过后手工放行时用 🧩 **方法定义**:`public void reset(String key)` 🗣️ **入参解释**:`public void reset(资源key)` 📤 **返回值解释**:`void`,直接把这个 key 从桶表里移除 💡 **边界口径**: - key 为 null 不抛异常,静默什么都不做 - 重置是整条窗口清空,不是补一个配额 - 重置后这个 key 不再占桶,`size()` 会减一 🧪 **方法示例**: - `reset("once-a-visitor")` 之后 `remaining("once-a-visitor", 1, IP_WINDOW)` 拿到 `1`,占桶数降到 `1` --- 🔢 **方法序号**:9 🎯 **方法用途**: - 一次性清空全部计数,所有 key 回到满额 - 单元测试之间复位、配置热更新后重建限流状态时用 🧩 **方法定义**:`public void clear()` 🗣️ **入参解释**:`public void clear()`(无入参) 📤 **返回值解释**:`void`,桶表清空 💡 **边界口径**: - 只清自己这个实例,默认单例与其他实例不受影响 - 清空不等于停服,下一波请求照样从满额开始扣 - 没有按窗口选择性清理的版本,要精细回收用 `reset(key)` 或 `purgeIdle` 🧪 **方法示例**: - `clear()` 之后 `size()` 拿到 `0`,全部计数被清空 --- 🔢 **方法序号**:10 🎯 **方法用途**: - 看当前有多少个 key 占着限流桶,供监控与容量观测 - 排查「内存是不是被限流 key 吃满」时先读这个数 🧩 **方法定义**:`public int size()` 🗣️ **入参解释**:`public int size()`(无入参) 📤 **返回值解释**:`int`,当前占桶的 key 数 💡 **边界口径**: - 只统计还留在表里的 key,窗口滑空并被触碰过的已被摘除 - 读它不消耗配额,也不做清理 - 数值持续只增不减,就说明该配 `purgeIdle` 了 🧪 **方法示例**: - 两个 key 各打一次之后 `size()` 拿到 `2` - `clear()` 之后拿到 `0` - 空闲回收之后 `size()` 拿到 `0` --- 🔢 **方法序号**:11 🎯 **方法用途**: - 清理长时间没人访问的 key,回收内存并返回被清掉的数量 - 限流 key 是手机号、IP 这种无界集合时,交给定时任务周期调用 🧩 **方法定义**:`public int purgeIdle(Duration idleThreshold)` 🗣️ **入参解释**:`public int purgeIdle(空闲时长阈值)` 📤 **返回值解释**:`int`,本次回收掉的 key 数 💡 **边界口径**: - 空闲时间按窗口里最后一条记录与当前时刻的差算 - 阈值给 null、0 或负数抛 `IllegalArgumentException` - 建议阈值取窗口长度的若干倍,比窗口短会清掉还在计数的 key - 只登记不清理等于内存泄漏,被刷一轮就把堆吃满 🧪 **方法示例**: - 两个桶都还新鲜时 `purgeIdle(Duration.ofHours(1))` 拿到 `回收 0 个` - 睡过 120 毫秒后用 50 毫秒阈值 `purgeIdle(Duration.ofMillis(50))` 拿到 `2 个`,占桶数降到 `0` - 阈值给 `Duration.ZERO` 抛 `idleThreshold 必须大于 0,当前为PT0S` ## 四、工具类源码(可直接复制) 📦 评论区留言