功能说明
固定窗口限流在窗口切换的瞬间可能放过两倍流量,滑动窗口则始终只看最近一个区间的请求数,边界更精确。这个实现保存每次放行的时间戳,判定时先丢掉早于当前时间减窗口长度的记录,再比较剩余数量与上限。
时钟通过函数注入,生产环境传 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())
}