Java中用循环数组实现固定长度时间窗口统计,核心是环形缓冲区存储时间槽计数,结合时间戳判断数据有效性;支持惰性清理与原子更新,适用于QPS统计等高频低延迟场景。

在 Java 中用数组实现固定长度时间窗口统计,核心思路是:用循环数组(环形缓冲区)存储每个时间槽的计数,配合时间戳判断数据是否落在当前窗口内。这种方式轻量、无依赖、适合高频低延迟场景,比如 QPS 统计、错误率采样等。
设计一个带时间槽的循环数组
假设窗口长度为 60 秒,精度为 1 秒 → 需要长度为 60 的 long[] 数组;若精度为 100ms、窗口 10 秒,则需 100 个槽。关键点:
- 每个数组下标对应一个时间槽,例如 index = (timestamp / slotDuration) % array.length
- 用一个 volatile long 记录当前“基准时间起点”,或每次直接用 System.currentTimeMillis() / slotDuration 计算逻辑时间戳
- 写入前先清理过期槽位:遍历所有槽,若其代表的时间范围已完全落在窗口外,就清零(也可惰性清理,见下文)
支持自动过期与原子更新
避免每次写都遍历全部槽,可采用“惰性清理 + 当前窗口边界校验”策略:
- 维护两个变量:currentWindowStart(毫秒级,当前窗口起始时间,对齐到 slot 边界)和 lastCleanupTime
- 每次 add() 时,先检查 timestamp 是否在 [currentWindowStart, currentWindowStart + windowSize) 内;不在则跳过或触发窗口滑动
- 滑动时重新计算 currentWindowStart,并只清空那些 所属逻辑时间戳 的槽位(最多清一次,非全扫)
- 使用 AtomicLongArray 或 synchronized 块保障单槽写入线程安全(通常单槽只会被同一逻辑时间戳的请求命中,冲突低)
提供常用统计接口(如 sum、avg、max)
数组本身不保存原始数据,只存聚合值(如请求数、耗时总和、失败次数),因此统计方法直接遍历有效槽位即可:
你接手一个需求时,是不是经常这样:"改一个小字段 → 结果把不相关的代码也改了"、"需求说得很模糊 → 做着做着发现跑偏了"、"数据库加个字段 → 上线才发现漏了数据兼容"、"做了个新功能 → 一个月后自己都忘了当时怎么设计的"、"写到一半被中断 → 再回来不知道从哪继续";这个 Skill 就是专门解决这些问题的。 它会自动探测你的项目环境(JDK、Spring Boot、ORM、数据库、前端框架),记在
立即学习“Java免费学习笔记(深入)”;
- getTotal():累加所有未过期槽的值(注意:需再次校验每个槽对应的时间是否仍在窗口内)
- getQps():getTotal() / windowSeconds(浮点结果,或返回 long 表示平均每秒整数次)
- getMaxInSlot():遍历有效槽,取最大值(适合监控峰值)
- 若需分位数(如 P95),此方案不适用——数组只存聚合值,无法还原原始分布,需换用 TDigest 或 HdrHistogram
一个极简可运行示例(60 秒窗口,1 秒粒度)
(生产中建议封装为类并加单元测试)
public class TimeWindowCounter {
private final long windowSeconds = 60;
private final long slotMillis = 1_000;
private final int capacity = (int) (windowSeconds * 1000 / slotMillis); // 60
private final long[] slots = new long[capacity];
private final Object lock = new Object();
<pre class="brush:php;toolbar:false;">public void add(long timestamp, long value) {
int idx = (int) ((timestamp / slotMillis) % capacity);
long windowStart = (timestamp / slotMillis) * slotMillis - (timestamp / slotMillis) % capacity * slotMillis;
// 实际中建议用更鲁棒的窗口对齐方式,此处仅为示意
synchronized (lock) {
// 清理过期槽:仅清理明显超出窗口的(保守做法)
long cutoff = timestamp - windowSeconds * 1000;
for (int i = 0; i < capacity; i++) {
long slotTime = (timestamp / slotMillis - i + capacity) % capacity * slotMillis;
if (slotTime < cutoff) slots[i] = 0;
}
slots[idx] += value;
}
}
public long getTotal() {
long sum = 0;
long now = System.currentTimeMillis();
long cutoff = now - windowSeconds * 1000;
synchronized (lock) {
for (int i = 0; i < capacity; i++) {
long slotTime = (now / slotMillis - i + capacity) % capacity * slotMillis;
if (slotTime >= cutoff) sum += slots[i];
}
}
return sum;
}}

















