滑动窗口是一种只处理“最近一段有效范围”的思路,并不只用于算法题。
系统会不断产生新请求、新订单和新日志。新数据进入窗口,过期数据离开窗口,系统只根据窗口内的数据做判断。
例如:
- 一个用户最近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、按什么维度统计?
限流和风控不能只定义“一分钟100次”,还要说明限制谁:
- 按用户ID限制。
- 按IP限制。
- 按设备限制。
- 按手机号限制。
- 按接口限制。
- 按用户和接口组合限制。
例如,同一个用户访问不同接口,可能需要分别计数。
2、窗口有多长?
窗口长度要根据问题决定:
- 接口防刷可能看最近1秒或1分钟。
- 登录失败可能看最近10分钟。
- 短信发送可能同时看最近1分钟和最近24小时。
- 监控告警可能看最近5分钟。
窗口越短,反应越快;窗口越长,结果越稳定,但需要维护的数据也更多。
3、窗口内统计什么?
常见的统计值包括:
- 请求次数。
- 失败次数。
- 错误率。
- 订单数和成交额。
- 不同用户数量。
- 平均值、最大值或最小值。
统计活跃用户时还要去重,不能简单地把访问次数当作用户数。
4、超过条件后怎么处理?
超过阈值不一定都是直接拒绝,也可以:
- 返回“操作频繁,请稍后重试”。
- 要求输入验证码。
- 暂时锁定账号。
- 对非核心功能降级。
- 触发告警。
- 将请求放入队列稍后处理。
三、常见业务场景
| 业务需求 | 窗口范围 | 维护的数据 | 处理动作 |
|---|---|---|---|
| 接口限流 | 最近1分钟 | 用户或IP的请求次数 | 拒绝、排队或降级 |
| 短信防刷 | 最近1分钟、24小时 | 手机号、IP的发送次数 | 拒绝发送或触发风控 |
| 登录保护 | 最近10分钟 | 账号、设备或IP的失败次数 | 验证码或暂时锁定 |
| 监控告警 | 最近5分钟 | 请求总数、失败数和耗时 | 超过阈值时告警 |
| 实时统计 | 最近30分钟 | 订单数、成交额或活跃事件 | 更新看板和趋势数据 |
| 设备监控 | 最近100次采样 | 温度、延迟等指标 | 连续异常时告警 |
1、接口限流
接口限流是最常见的滑动窗口场景。
例如,订单查询接口限制同一用户一分钟最多请求100次。每次请求到来时:
- 找到该用户的请求窗口。
- 删除一分钟以前的记录。
- 判断剩余记录是否已经达到100次。
- 未达到则放行并记录本次请求,达到则拒绝。
相比固定时间段计数,滑动窗口能够减少临界时间内的突发流量。
滑动窗口只是限流方案之一。漏桶、令牌桶等方案的区别,可以参考如何限流?。
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、窗口越精确,成本越高
保存每条事件最准确,但占用更多存储。时间分桶更节省资源,但会牺牲一部分精度。选择时要看业务是否真的需要精确到每一次请求。
八、什么时候适合使用?
满足下面几个特点时,可以考虑滑动窗口:
- 只关心最近一段时间或最近一批数据。
- 新数据会不断进入,旧数据会不断失效。
- 加入和移除一条数据后,可以快速更新统计结果。
- 需要根据当前范围内的结果立即限流、告警或调整策略。
如果需要查询任意历史时间段,或者每次都要进行复杂的明细分析,直接查询统计系统、数据库或离线任务可能更合适。
一句话总结:滑动窗口就是让系统只关注“当前仍然有效的数据”,并随着时间向前持续更新判断结果。