← 中文资源

简体中文版本

滑动窗口限流器

记录时间戳并裁剪过期事件,在任意滑动区间内精确限制请求数量,时钟可注入便于测试。

阅读英文原文

功能说明

固定窗口限流在窗口切换的瞬间可能放过两倍流量,滑动窗口则始终只看最近一个区间的请求数,边界更精确。这个实现保存每次放行的时间戳,判定时先丢掉早于当前时间减窗口长度的记录,再比较剩余数量与上限。

时钟通过函数注入,生产环境传 nil 使用 time.Now,测试时传一个可推进的假时钟,因此无需真实等待就能验证窗口滑动。同一把互斥锁保护事件切片,多个协程并发调用也安全。

关键代码

prune 用 sort.Search 找到第一个晚于截止时间的下标,把切片前移并复用底层数组,避免每次分配。Allow 在锁内取当前时间、裁剪、判断是否达到上限,未达到才追加时间戳。Remaining 复用同一套逻辑返回剩余额度。main 每次推进一百毫秒调用一次,前三次放行,第四次因为一秒窗口内已有三条记录被拒绝;再前进七百毫秒后最早的事件过期,重新放行。

使用注意

事件切片最多保存上限个时间戳,内存有界。时间戳按写入顺序递增,依赖这个前提才能使用二分查找;如果注入的时钟会回退,需要改为全量扫描。多实例部署时每个实例各自计数,整体额度是实例数的倍数,需要共享存储或网关层限流。窗口长度与上限要匹配业务,过短会造成抖动。拒绝的请求不消耗额度。

验证方式

时间点 100、200、300 毫秒的请求被放行,400 毫秒的请求被拒绝且剩余额度为零。继续推进到 1100 毫秒时,100 毫秒的记录已严格早于截止时间,窗口内只剩两条,因此再次放行。把上限改为一可以快速验证拒绝与恢复逻辑。

代码示例

package main

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

type SlidingWindowLimiter struct {
	mu     sync.Mutex
	limit  int
	window time.Duration
	now    func() time.Time
	events []time.Time
}

func NewSlidingWindowLimiter(limit int, window time.Duration, now func() time.Time) *SlidingWindowLimiter {
	if limit <= 0 || window <= 0 {
		panic("limit and window must be positive")
	}
	if now == nil {
		now = time.Now
	}
	return &SlidingWindowLimiter{limit: limit, window: window, now: now}
}

func (l *SlidingWindowLimiter) prune(current time.Time) []time.Time {
	cutoff := current.Add(-l.window)
	first := sort.Search(len(l.events), func(i int) bool {
		return l.events[i].After(cutoff)
	})
	l.events = append(l.events[:0], l.events[first:]...)
	return l.events
}

func (l *SlidingWindowLimiter) Allow() bool {
	l.mu.Lock()
	defer l.mu.Unlock()
	current := l.now()
	active := l.prune(current)
	if len(active) >= l.limit {
		return false
	}
	l.events = append(active, current)
	return true
}

func (l *SlidingWindowLimiter) Remaining() int {
	l.mu.Lock()
	defer l.mu.Unlock()
	active := l.prune(l.now())
	if remaining := l.limit - len(active); remaining > 0 {
		return remaining
	}
	return 0
}

func main() {
	clock := time.Date(2026, 10, 21, 12, 0, 0, 0, time.UTC)
	now := func() time.Time { return clock }
	limiter := NewSlidingWindowLimiter(3, time.Second, now)

	for i := 0; i < 4; i++ {
		clock = clock.Add(100 * time.Millisecond)
		fmt.Printf("t=%dms allow=%v remaining=%d\n", (i+1)*100, limiter.Allow(), limiter.Remaining())
	}

	clock = clock.Add(700 * time.Millisecond)
	fmt.Println("after window slides:", limiter.Allow())
}