You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

高吞吐多时间窗口滑动事件计数的数据结构/数据库选型咨询

针对高并发滑动窗口计数的最优方案

针对你的场景——每秒10万条事件、百万级IP的滑动窗口计数(1分钟/1小时/1天),且无需持久化、追求极致性能,我有几个非常贴合的方案,都是业内处理这类问题的标准做法:

一、分层时间桶(Time Buckets):本地内存实现首选

这是最适合单进程场景的轻量方案,核心思路是把大时间窗口拆分成多个小的时间桶,用固定数量的桶来维护滑动窗口的计数,彻底解决内存爆炸的问题。

原理

比如:

  • 1分钟窗口拆成60个1秒桶,每个桶记录该秒内的事件数
  • 1小时窗口拆成60个1分钟桶,每个桶记录该分钟内的事件数
  • 1天窗口拆成24个1小时桶,每个桶记录该小时内的事件数

每个IP对应一组这样的桶数组:

  • 新事件到来时,找到当前时间对应的桶,原子递增计数
  • 查询时,把当前滑动窗口覆盖的所有桶的计数求和
  • 定时或在操作时清理过期的桶(比如1分钟窗口的桶,超过60秒就重置为0)

优势

  • 内存占用极低:每个IP的内存仅和桶数量成正比(比如60+60+24=144个整数),百万IP仅需约3-4GB内存,完全可控
  • 性能极致:插入和查询都是O(k)复杂度(k是桶数量,比如60),单进程轻松扛住每秒10万+的事件
  • 自定义性强:可以根据需求调整桶的粒度(比如把1分钟拆成12个5秒桶,进一步降低内存)

示例代码(Java)

import java.util.Arrays;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

public class IpSlidingWindowCounter {
    private final ConcurrentHashMap<String, IpStats> ipStatsMap = new ConcurrentHashMap<>();

    private static class IpStats {
        private final AtomicInteger[] oneMinuteBuckets;
        private final AtomicInteger[] oneHourBuckets;
        private final AtomicInteger[] oneDayBuckets;

        public IpStats() {
            // 初始化各时间窗口的桶,初始值为0
            oneMinuteBuckets = new AtomicInteger[60];
            oneHourBuckets = new AtomicInteger[60];
            oneDayBuckets = new AtomicInteger[24];
            Arrays.fill(oneMinuteBuckets, new AtomicInteger(0));
            Arrays.fill(oneHourBuckets, new AtomicInteger(0));
            Arrays.fill(oneDayBuckets, new AtomicInteger(0));
        }

        public void increment() {
            long nowSec = System.currentTimeMillis() / 1000;
            // 更新1秒粒度的1分钟桶
            int minBucketIdx = (int) (nowSec % 60);
            oneMinuteBuckets[minBucketIdx].incrementAndGet();
            // 更新1分钟粒度的1小时桶
            int hourBucketIdx = (int) ((nowSec / 60) % 60);
            oneHourBuckets[hourBucketIdx].incrementAndGet();
            // 更新1小时粒度的1天桶
            int dayBucketIdx = (int) ((nowSec / 3600) % 24);
            oneDayBuckets[dayBucketIdx].incrementAndGet();
        }

        public long getOneMinuteCount() {
            long nowSec = System.currentTimeMillis() / 1000;
            long count = 0;
            // 遍历所有桶,累加过去60秒内的计数
            for (int i = 0; i < 60; i++) {
                long bucketTime = nowSec - (nowSec % 60 - i + 60) % 60;
                if (bucketTime >= nowSec - 60) {
                    count += oneMinuteBuckets[i].get();
                }
            }
            return count;
        }

        // 同理实现getOneHourCount()和getOneDayCount()
    }

    public void recordEvent(String ip) {
        ipStatsMap.computeIfAbsent(ip, k -> new IpStats()).increment();
    }

    public long getIpCountInLastMinute(String ip) {
        IpStats stats = ipStatsMap.get(ip);
        return stats != null ? stats.getOneMinuteCount() : 0;
    }
}

二、Redis时间桶方案:分布式场景首选

如果你的服务是多实例部署,需要共享统计数据,Redis是绝佳选择——它的内存性能极强,且能轻松处理高并发请求。

原理

和本地时间桶思路一致,只是把桶存储在Redis中:

  • 每个IP的每个时间桶对应一个Redis键,比如ip:1min:192.168.1.1:1690000000(最后一段是秒级时间戳)
  • 事件到来时,用INCR命令递增对应桶的计数,同时给键设置过期时间(比如1分钟桶设为61秒过期)
  • 查询时,用Lua脚本批量获取并累加过去窗口内的所有桶计数(避免多次网络请求)

优势

  • 分布式支持:多实例共享统计数据,无需额外同步逻辑
  • 运维简单:Redis自带过期键清理,无需自己维护桶的过期
  • 性能强劲:Redis每秒可处理数十万次INCR和Lua脚本请求,完全满足你的吞吐量需求

示例Lua脚本(查询过去1分钟计数)

local ip = ARGV[1]
local now = tonumber(ARGV[2])
local total = 0
-- 遍历过去60秒的所有桶
for i = 0, 59 do
    local key = "ip:1min:" .. ip .. ":" .. (now - i)
    local count = redis.call('GET', key)
    total = total + (count and tonumber(count) or 0)
end
return total

三、为什么其他方案不适合?

  • InfluxDB:时序库更适合存储原始事件并做复杂分析,对于纯计数场景来说太重,插入和聚合性能都跟不上高并发
  • 内存链表:每个事件都存储会导致内存随事件量线性增长,百万IP每秒10万条的话,内存会迅速爆炸,完全不可行
  • 固定窗口计数器:无法实现真正的滑动窗口,会出现“窗口边界突增”的问题(比如窗口切换时,前一秒的计数突然清零)

内容的提问来源于stack exchange,提问作者Aliakbar Abbasi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 06:43:32