场景题 2026.09.26
什么是滑动窗口?

滑动窗口是一种只处理“最近一段有效范围”的思路,并不只用于算法题。

系统会不断产生新请求、新订单和新日志。新数据进入窗口,过期数据离开窗口,系统只根据窗口内的数据做判断。

例如:

  • 一个用户最近1分钟请求了多少次?
  • 一个手机号最近24小时发送了多少次验证码?
  • 一个接口最近5分钟的错误率是多少?
  • 一台设备最近100次采样是否持续异常?

这些问题虽然业务不同,但处理方式相似,都可以使用滑动窗口。

一、滑动窗口解决什么问题?

假设接口限制每个用户一分钟最多请求100次。

最简单的做法是按自然分钟计数:

12:00:00 ~ 12:00:59 记录一个计数
12:01:00 ~ 12:01:59 重新开始计数

这种固定时间段会有临界问题:

12:00:59 收到100次请求
12:01:00 又收到100次请求

两个自然分钟都没有超过限制,但系统可能在1秒左右收到200次请求。

滑动窗口不会只看当前是哪个自然分钟,而是每次都统计“当前时间往前一分钟”的请求:

当前时间:12:01:00
统计范围:12:00:00 ~ 12:01:00

12:00:59发生的请求仍然有效,所以第二批请求会受到限制。

可以把它理解成一扇不断向前移动的窗户:

过去的数据                 当前时间
    [  已过期  ][  当前窗口  ]
                  -> 窗口持续向前移动

窗口每次移动时只做三件事:

  1. 移除已经过期的数据。
  2. 加入刚刚发生的数据。
  3. 根据窗口内的数据计算结果并执行对应动作。

二、使用前先明确四件事

滑动窗口只是处理思路,真正落地前要先明确下面四项。

1、按什么维度统计?

限流和风控不能只定义“一分钟100次”,还要说明限制谁:

  • 按用户ID限制。
  • 按IP限制。
  • 按设备限制。
  • 按手机号限制。
  • 按接口限制。
  • 按用户和接口组合限制。

例如,同一个用户访问不同接口,可能需要分别计数。

2、窗口有多长?

窗口长度要根据问题决定:

  • 接口防刷可能看最近1秒或1分钟。
  • 登录失败可能看最近10分钟。
  • 短信发送可能同时看最近1分钟和最近24小时。
  • 监控告警可能看最近5分钟。

窗口越短,反应越快;窗口越长,结果越稳定,但需要维护的数据也更多。

3、窗口内统计什么?

常见的统计值包括:

  • 请求次数。
  • 失败次数。
  • 错误率。
  • 订单数和成交额。
  • 不同用户数量。
  • 平均值、最大值或最小值。

统计活跃用户时还要去重,不能简单地把访问次数当作用户数。

4、超过条件后怎么处理?

超过阈值不一定都是直接拒绝,也可以:

  • 返回“操作频繁,请稍后重试”。
  • 要求输入验证码。
  • 暂时锁定账号。
  • 对非核心功能降级。
  • 触发告警。
  • 将请求放入队列稍后处理。

三、常见业务场景

业务需求 窗口范围 维护的数据 处理动作
接口限流 最近1分钟 用户或IP的请求次数 拒绝、排队或降级
短信防刷 最近1分钟、24小时 手机号、IP的发送次数 拒绝发送或触发风控
登录保护 最近10分钟 账号、设备或IP的失败次数 验证码或暂时锁定
监控告警 最近5分钟 请求总数、失败数和耗时 超过阈值时告警
实时统计 最近30分钟 订单数、成交额或活跃事件 更新看板和趋势数据
设备监控 最近100次采样 温度、延迟等指标 连续异常时告警

1、接口限流

接口限流是最常见的滑动窗口场景。

例如,订单查询接口限制同一用户一分钟最多请求100次。每次请求到来时:

  1. 找到该用户的请求窗口。
  2. 删除一分钟以前的记录。
  3. 判断剩余记录是否已经达到100次。
  4. 未达到则放行并记录本次请求,达到则拒绝。

相比固定时间段计数,滑动窗口能够减少临界时间内的突发流量。

滑动窗口只是限流方案之一。漏桶、令牌桶等方案的区别,可以参考如何限流?。

2、短信和操作防刷

短信验证码、发布动态、发表评论等操作通常会同时设置多个窗口:

同一手机号60秒内只能发送1次验证码
同一手机号24小时内最多发送10次验证码
同一IP一分钟内最多发送20次验证码

任何一个窗口超过阈值,都可以拒绝请求或进入额外校验。

只按手机号限制不够,因为攻击者可能不断更换手机号;只按IP限制也可能误伤共享网络下的正常用户。实际使用时通常会组合用户、手机号、设备和IP等维度。

3、登录风控

系统可以统计同一账号或IP最近10分钟的登录失败次数:

  • 少于5次:允许继续尝试。
  • 达到5次:要求输入验证码。
  • 继续失败:暂时锁定账号或限制该IP。

新失败记录进入窗口,10分钟以前的记录自动过期。相比永久累加失败次数,这种方式不会让很久以前的偶发失败一直影响用户。

4、监控和告警

监控系统可以维护最近5分钟的请求总数和失败数:

错误率 = 最近5分钟失败数 / 最近5分钟请求总数

当错误率连续超过阈值时再告警,可以及时发现故障,也能避免一次偶发失败立即触发告警。

实际判断时还要设置最小请求量。例如最近5分钟只有1次请求且刚好失败,错误率虽然是100%,但样本太少,不一定代表服务发生故障。

5、实时统计

运营看板经常需要展示:

  • 最近30分钟的订单数量和成交额。
  • 最近24小时的活跃用户数。
  • 最近100笔订单的平均金额。
  • 最近一分钟的消息消费速度。

窗口移动时,只需要增加新数据、扣除过期数据,不必每次重新扫描全部历史记录。

四、Golang实现单机限流

下面实现一个简单的单机限流器:一分钟最多允许3次请求。

它使用切片保存请求时间,每次请求到来时先清理过期记录,再决定是否放行。

package main

import (
	"fmt"
	"sync"
	"time"
)

type SlidingWindowLimiter struct {
	mu       sync.Mutex
	limit    int
	window   time.Duration
	requests []time.Time
}

func NewSlidingWindowLimiter(limit int, window time.Duration) *SlidingWindowLimiter {
	return &SlidingWindowLimiter{
		limit:  limit,
		window: window,
	}
}

func (l *SlidingWindowLimiter) Allow(now time.Time) bool {
	l.mu.Lock()
	defer l.mu.Unlock()

	cutoff := now.Add(-l.window)
	firstValid := 0

	// 移除窗口外的请求记录。
	for firstValid < len(l.requests) && !l.requests[firstValid].After(cutoff) {
		firstValid++
	}
	l.requests = l.requests[firstValid:]

	if len(l.requests) >= l.limit {
		return false
	}

	l.requests = append(l.requests, now)
	return true
}

func main() {
	limiter := NewSlidingWindowLimiter(3, time.Minute)
	start := time.Date(2026, 9, 24, 12, 0, 0, 0, time.UTC)

	requestTimes := []time.Duration{
		0,
		10 * time.Second,
		20 * time.Second,
		30 * time.Second,
		61 * time.Second,
	}

	for _, elapsed := range requestTimes {
		allowed := limiter.Allow(start.Add(elapsed))
		fmt.Printf("第%2d秒,是否允许:%t\n", int(elapsed.Seconds()), allowed)
	}
}

输出:

第 0秒,是否允许:true
第10秒,是否允许:true
第20秒,是否允许:true
第30秒,是否允许:false
第61秒,是否允许:true

前三次请求进入一分钟窗口,第四次请求被拒绝。第61秒时,最早的一次请求已经过期,所以新请求可以进入。

这里使用sync.Mutex是因为多个Goroutine可能同时调用Allow。如果不加锁,检查次数和写入记录之间可能发生并发问题,导致实际放行数量超过限制。

五、Golang统计最近错误率

滑动窗口不只能限流,也可以用来计算接口最近一段时间的错误率。

下面的示例保存最近5分钟的请求结果。新结果进入时,会同时移除过期结果并更新失败数量。

package main

import (
	"fmt"
	"sync"
	"time"
)

type RequestResult struct {
	at     time.Time
	failed bool
}

type ErrorRateWindow struct {
	mu           sync.Mutex
	window       time.Duration
	results      []RequestResult
	failureCount int
}

func NewErrorRateWindow(window time.Duration) *ErrorRateWindow {
	return &ErrorRateWindow{window: window}
}

func (w *ErrorRateWindow) Record(now time.Time, failed bool) (int, float64) {
	w.mu.Lock()
	defer w.mu.Unlock()

	cutoff := now.Add(-w.window)
	firstValid := 0

	for firstValid < len(w.results) && !w.results[firstValid].at.After(cutoff) {
		if w.results[firstValid].failed {
			w.failureCount--
		}
		firstValid++
	}
	w.results = w.results[firstValid:]

	w.results = append(w.results, RequestResult{at: now, failed: failed})
	if failed {
		w.failureCount++
	}

	total := len(w.results)
	errorRate := float64(w.failureCount) / float64(total)
	return total, errorRate
}

func main() {
	window := NewErrorRateWindow(5 * time.Minute)
	start := time.Date(2026, 9, 24, 12, 0, 0, 0, time.UTC)

	samples := []struct {
		after  time.Duration
		failed bool
	}{
		{after: 0, failed: false},
		{after: time.Minute, failed: true},
		{after: 2 * time.Minute, failed: true},
		{after: 6 * time.Minute, failed: false},
	}

	for _, sample := range samples {
		total, errorRate := window.Record(start.Add(sample.after), sample.failed)
		fmt.Printf("第%d分钟:窗口内%d次请求,错误率%.1f%%\n",
			int(sample.after.Minutes()), total, errorRate*100)
	}
}

输出:

第0分钟:窗口内1次请求,错误率0.0%
第1分钟:窗口内2次请求,错误率50.0%
第2分钟:窗口内3次请求,错误率66.7%
第6分钟:窗口内2次请求,错误率50.0%

到了第6分钟,第0分钟和第1分钟的数据已经离开5分钟窗口,因此结果只根据仍然有效的数据计算。

生产中的告警一般还会增加两个条件:

  • 窗口内请求数达到最小样本量。
  • 错误率连续多个窗口超过阈值。

这样可以减少小流量和偶发错误造成的误报。

六、生产环境怎么实现?

前面的代码主要用于理解思路。实际项目要根据请求量、部署方式和精度要求选择实现。

1、保存每条事件

像示例一样保存每次请求的时间。

  • 优点:判断准确,实现直观。
  • 缺点:请求量越大,保存的数据越多。
  • 适合:单机、小流量、高精度场景。

2、按时间分桶

把一分钟拆成60个一秒小桶,每个桶只保存请求数:

[12:00:01 -> 3次] [12:00:02 -> 8次] [12:00:03 -> 5次]

统计时累加当前一分钟内的桶。

  • 优点:不需要保存每一次请求,内存占用更低。
  • 缺点:结果精度取决于桶的大小。
  • 适合:高流量的限流、监控和指标统计。

3、使用共享存储

服务有多个实例时,只保存在本机内存会导致每台机器分别计数,实际总量可能超过限制。

这时需要把窗口状态放到所有实例都能访问的位置,并保证“清理、统计、写入”是一组不可被打断的操作。常见做法是使用Redis或统一的限流组件。

还需要统一时间来源、设置数据过期时间,并考虑共享存储不可用时是放行还是拒绝。

4、使用现成组件

网关限流、服务保护和监控统计通常已有成熟组件。业务只需要配置统计维度、窗口长度、阈值和处理动作,不一定要自己维护完整实现。

七、常见问题

1、把固定时间段当成滑动窗口

按自然分钟清零是固定窗口。每次都统计“当前时间往前一分钟”才是滑动窗口。

2、统计维度太粗

整个接口共用一个计数可能让少数用户占满额度。需要根据业务选择用户、IP、设备或组合维度。

3、只记录新数据,不清理旧数据

过期数据不清理会让统计越来越大,也会造成内存持续增长。

4、多实例各自统计

单机限流器只保护当前进程。服务扩容后,如果仍然各自计数,就要重新计算每台机器的额度,或者改为共享计数。

5、忽略最小样本量

错误率、成功率等比例指标在样本很少时容易误导。除了比例,还要同时检查窗口内的总数量。

6、窗口越精确,成本越高

保存每条事件最准确,但占用更多存储。时间分桶更节省资源,但会牺牲一部分精度。选择时要看业务是否真的需要精确到每一次请求。

八、什么时候适合使用?

满足下面几个特点时,可以考虑滑动窗口:

  1. 只关心最近一段时间或最近一批数据。
  2. 新数据会不断进入,旧数据会不断失效。
  3. 加入和移除一条数据后,可以快速更新统计结果。
  4. 需要根据当前范围内的结果立即限流、告警或调整策略。

如果需要查询任意历史时间段,或者每次都要进行复杂的明细分析,直接查询统计系统、数据库或离线任务可能更合适。

一句话总结:滑动窗口就是让系统只关注“当前仍然有效的数据”,并随着时间向前持续更新判断结果。

相关案例


文章作者: GaryLee
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 GaryLee !
赏
  目录