📋 目录
一、工具类功能
按「资源 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,当前为0permits比总额度还大时抛permits 不能大于 maxRequests,当前为5 > 3,这样的请求永远无法通过
🔢 方法序号:3
🎯 方法用途:
- 取默认单例,静态入口操作的就是它
- 想用
remaining、retryAfter、清理与观测这些实例方法时从它拿
🧩 方法定义:public static RateLimiterUtils defaultLimiter()
🗣️ 入参解释:public static RateLimiterUtils defaultLimiter()(无入参)
📤 返回值解释:RateLimiterUtils,进程内唯一的默认实例
💡 边界口径:
- 返回的是同一个对象,多次取回的结果可直接比引用
- 静态
tryAcquire写的桶在这里都能看见 - 需要按业务隔离限流桶时另用
new RateLimiterUtils(),别共用这个单例
🧪 方法示例:
defaultLimiter().size()在静态入口刚打过一次之后拿到1defaultLimiter().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
四、工具类源码(可直接复制)
📦 源码文件
该博客介绍了一个基于Java实现的滑动窗口限流工具类,支持单机场景下的接口防刷、验证码频率控制与第三方配额保护。工具采用System.nanoTime()单调时钟,规避系统校时与时间回拨导致的限流失效问题。提供11个核心方法,涵盖配额获取、批量消耗、剩余查询、等待时长计算、手动重置、内存清理等能力,适用于JDK 17+环境。

180

被折叠的 条评论
为什么被折叠?



