用System.currentTimeMillis()配合原子计数器实现单机限流,核心是按时间戳划分固定窗口并用AtomicInteger安全累加;关键在窗口对齐、原子操作防丢失、阈值判断时机选getAndIncrement或incrementAndGet。

用 System.currentTimeMillis() 配合原子计数器做单机限流,核心是靠时间戳判断是否进入新窗口,并用 AtomicInteger 或 AtomicLong 安全累加请求数。关键不在“能不能用”,而在于“怎么用才不漏、不超、不卡死”。
用时间戳划分固定窗口
把每秒(或每 N 毫秒)看作一个独立计数周期。每次请求来时,先算出当前所属的时间窗口起点,比如:
-
按秒切分:窗口 key =
System.currentTimeMillis() / 1000 -
按 500ms 切分:窗口 key =
System.currentTimeMillis() / 500
这个 key 决定了该请求归入哪个计数桶。注意:除法结果是向下取整,天然对齐窗口边界。
用原子变量安全计数
不能用普通 int 加 ++,因为读-改-写三步非原子,高并发下会丢计数。必须用:
-
AtomicInteger.incrementAndGet()—— 先加再返回,适合“加完立刻判断”场景 -
AtomicInteger.getAndIncrement()—— 先返回再加,适合“加前判断阈值”逻辑
例如:if (counter.getAndIncrement() 表示“如果加之前还没到上限,就放行”。
清理过期窗口要克制
窗口只保留最近几个(如当前 + 前 1 秒),老的 key 要删,否则 map 无限膨胀。但别每次请求都遍历清理:
- 推荐惰性清理:在插入新 key 前,顺手删掉
key < nowSecond - 1的旧项 - 或用定时任务(如 ScheduledExecutorService)每秒扫一次,避免请求线程阻塞
- 绝对不要在
tryAcquire()里调map.keySet().removeIf(...)—— 这会锁整个 map,拖慢所有请求
警惕窗口切换的临界问题
固定窗口天然存在“双倍突刺”风险:比如 0.9s 到 1.0s 放行了 100 次,1.0s 瞬间重置,1.0s 到 1.1s 又放行 100 次 → 实际 0.2s 内来了 200 次。这不是 bug,是算法特性。
若业务敏感(如支付、库存扣减),就得升级方案:
- 改用滑动窗口(维护多个子窗口,按时间加权)
- 或直接上令牌桶(允许平滑突发,更适合真实流量)
- 单机场景下,也可折中:把窗口缩到 200ms,用 5 个桶滚动计数,精度提升,开销可控

















