滑动窗口限流案例

wen java案例 1

本文目录导读:

滑动窗口限流案例

  1. 基础版:固定窗口计数器(演示问题)
  2. 滑动窗口日志版(精确但耗内存)
  3. 滑动窗口计数器版(高效实用)
  4. Redis实现(分布式场景)
  5. 完整实战案例:API网关限流
  6. 高级优化版本:自适应限流

我来给你几个不同场景的滑动窗口限流案例,从简单到复杂。

基础版:固定窗口计数器(演示问题)

import time
import threading
class FixedWindowRateLimiter:
    """固定窗口限流器 - 演示边界问题"""
    def __init__(self, max_requests, window_seconds):
        self.max_requests = max_requests  # 窗口内最大请求数
        self.window_seconds = window_seconds  # 窗口大小
        self.current_window_start = time.time()
        self.request_count = 0
        self.lock = threading.Lock()
    def allow_request(self):
        with self.lock:
            now = time.time()
            # 如果当前时间超出窗口,重置计数
            if now - self.current_window_start >= self.window_seconds:
                self.current_window_start = now
                self.request_count = 0
            # 检查是否超限
            if self.request_count < self.max_requests:
                self.request_count += 1
                return True
            return False
# 演示边界问题
limiter = FixedWindowRateLimiter(max_requests=5, window_seconds=60)
# 在窗口边界处可能绕过限流

滑动窗口日志版(精确但耗内存)

from collections import deque
import time
import threading
class SlidingWindowLogRateLimiter:
    """滑动窗口日志限流器 - 使用时间戳记录"""
    def __init__(self, max_requests, window_seconds):
        self.max_requests = max_requests
        self.window_seconds = window_seconds
        self.requests = deque()  # 存储请求时间戳
        self.lock = threading.Lock()
    def allow_request(self):
        with self.lock:
            now = time.time()
            # 移除窗口外的旧请求
            while self.requests and now - self.requests[0] >= self.window_seconds:
                self.requests.popleft()
            # 检查当前窗口内请求数
            if len(self.requests) < self.max_requests:
                self.requests.append(now)
                return True
            return False
    def get_current_count(self):
        """获取当前窗口内请求数"""
        with self.lock:
            now = time.time()
            while self.requests and now - self.requests[0] >= self.window_seconds:
                self.requests.popleft()
            return len(self.requests)
# 使用示例
limiter = SlidingWindowLogRateLimiter(max_requests=10, window_seconds=60)
for i in range(15):
    if limiter.allow_request():
        print(f"请求 {i+1}: 允许")
    else:
        print(f"请求 {i+1}: 拒绝")

滑动窗口计数器版(高效实用)

import time
import threading
from collections import deque
class SlidingWindowCounterRateLimiter:
    """滑动窗口计数器限流器 - 使用子窗口聚合"""
    def __init__(self, max_requests, window_seconds, sub_window_count=10):
        self.max_requests = max_requests
        self.window_seconds = window_seconds
        self.sub_window_size = window_seconds / sub_window_count  # 每个子窗口大小
        self.windows = deque()  # (window_start_time, count) 对
        self.total_count = 0
        self.lock = threading.Lock()
    def allow_request(self):
        with self.lock:
            now = time.time()
            self._cleanup(now)
            if self.total_count < self.max_requests:
                self._add_request(now)
                return True
            return False
    def _cleanup(self, now):
        """清理过期的子窗口"""
        while self.windows and now - self.windows[0][0] >= self.window_seconds:
            _, count = self.windows.popleft()
            self.total_count -= count
    def _add_request(self, now):
        """添加请求到当前子窗口"""
        # 找到当前子窗口的起始时间
        current_window_start = now - (now % self.sub_window_size)
        # 如果当前子窗口不存在或已过期,创建新的
        if not self.windows or self.windows[-1][0] < current_window_start:
            self.windows.append((current_window_start, 1))
        else:
            # 更新当前子窗口的计数
            window_start, count = self.windows[-1]
            self.windows[-1] = (window_start, count + 1)
        self.total_count += 1
    def get_stats(self):
        """获取统计信息"""
        with self.lock:
            now = time.time()
            self._cleanup(now)
            return {
                'total_requests': self.total_count,
                'window_size': self.window_seconds,
                'sub_windows': [(time.strftime('%H:%M:%S', time.localtime(ws)), c) 
                               for ws, c in self.windows]
            }
# 使用示例
limiter = SlidingWindowCounterRateLimiter(max_requests=100, window_seconds=60, sub_window_count=6)

Redis实现(分布式场景)

import redis
import time
import json
class RedisSlidingWindowRateLimiter:
    """使用Redis实现分布式滑动窗口限流"""
    def __init__(self, redis_client, key_prefix='ratelimit', max_requests=100, window_seconds=60):
        self.redis = redis_client
        self.key_prefix = key_prefix
        self.max_requests = max_requests
        self.window_seconds = window_seconds
    def allow_request(self, user_id):
        """
        使用Redis ZSET实现滑动窗口
        每个元素为请求时间戳,score为时间戳
        """
        key = f"{self.key_prefix}:{user_id}"
        now = time.time()
        # 使用pipeline保证原子性
        pipe = self.redis.pipeline()
        # 添加当前请求
        pipe.zadd(key, {str(now): now})
        # 移除窗口外的旧请求
        pipe.zremrangebyscore(key, 0, now - self.window_seconds)
        # 获取当前窗口内请求数
        pipe.zcard(key)
        # 设置过期时间
        pipe.expire(key, self.window_seconds)
        _, _, request_count, _ = pipe.execute()
        if request_count <= self.max_requests:
            return True
        return False
    def get_remaining(self, user_id):
        """获取剩余可用请求数"""
        key = f"{self.key_prefix}:{user_id}"
        now = time.time()
        # 清理过期数据
        self.redis.zremrangebyscore(key, 0, now - self.window_seconds)
        current_count = self.redis.zcard(key)
        return max(0, self.max_requests - current_count)
# 分布式使用示例
redis_client = redis.Redis(host='localhost', port=6379, decode_responses=True)
limiter = RedisSlidingWindowRateLimiter(
    redis_client, 
    max_requests=100, 
    window_seconds=60
)

完整实战案例:API网关限流

from fastapi import FastAPI, HTTPException, Request
from typing import Optional
import time
import threading
from collections import defaultdict, deque
from datetime import datetime
import asyncio
class APIRateLimiter:
    """完整API限流器,支持多用户、多接口"""
    def __init__(self):
        self.limits = {
            'default': {'max_requests': 100, 'window': 60},
            'user': {'max_requests': 10, 'window': 60},
            'path': {'max_requests': 30, 'window': 60},
            'admin': {'max_requests': 1000, 'window': 60},
        }
        self.user_records = defaultdict(deque)
        self.path_records = defaultdict(deque)
        self.ip_records = defaultdict(deque)
        self.lock = threading.Lock()
        self.blocked_ips = {}
    def allow_request(self, user_id: Optional[str] = None, 
                     path: str = None, ip: str = None) -> dict:
        """
        检查请求是否允许
        返回:{'allowed': bool, 'retry_after': seconds, 'remaining': int}
        """
        now = time.time()
        with self.lock:
            # 检查IP是否被临时封禁
            if ip in self.blocked_ips:
                block_until = self.blocked_ips[ip]
                if now < block_until:
                    return {
                        'allowed': False,
                        'retry_after': int(block_until - now),
                        'remaining': 0,
                        'reason': 'IP暂时被封禁'
                    }
                else:
                    del self.blocked_ips[ip]
            # 检查各类限制
            checks = []
            # 用户限制
            if user_id:
                check = self._check_limit(
                    self.user_records[user_id],
                    self.limits['user']['max_requests'],
                    self.limits['user']['window'],
                    now
                )
                checks.append(check)
            # 路径限制
            if path:
                check = self._check_limit(
                    self.path_records[path],
                    self.limits['path']['max_requests'],
                    self.limits['path']['window'],
                    now
                )
                checks.append(check)
            # IP限制
            if ip:
                check = self._check_limit(
                    self.ip_records[ip],
                    self.limits['default']['max_requests'],
                    self.limits['default']['window'],
                    now
                )
                checks.append(check)
            # 汇总结果
            if checks and all(c['allowed'] for c in checks):
                return {
                    'allowed': True,
                    'remaining': min(c['remaining'] for c in checks),
                    'retry_after': 0
                }
            else:
                # 找到最长的等待时间
                retry_after = max(c['retry_after'] for c in checks if c['retry_after'] > 0)
                # 连续失败可能触发临时封禁
                if checks and any(c['remaining'] == 0 for c in checks) and ip:
                    self._block_ip(ip, now)
                return {
                    'allowed': False,
                    'retry_after': retry_after,
                    'remaining': 0,
                    'reason': '达到限流阈值'
                }
    def _check_limit(self, records: deque, max_requests: int, 
                     window: int, now: float) -> dict:
        """检查单个限流规则"""
        # 清理过期记录
        while records and now - records[0] >= window:
            records.popleft()
        remaining = max_requests - len(records)
        if remaining > 0:
            records.append(now)
            return {'allowed': True, 'remaining': remaining, 'retry_after': 0}
        else:
            oldest = records[0] if records else now
            retry_after = max(0, window - (now - oldest))
            return {'allowed': False, 'remaining': 0, 'retry_after': retry_after}
    def _block_ip(self, ip: str, now: float, duration: int = 300):
        """临时封禁IP"""
        self.blocked_ips[ip] = now + duration
# FastAPI集成示例
app = FastAPI()
limiter = APIRateLimiter()
@app.middleware("http")
async def rate_limit_middleware(request: Request, call_next):
    """全局限流中间件"""
    user_id = request.headers.get('X-User-ID')
    ip = request.client.host
    path = request.url.path
    result = limiter.allow_request(
        user_id=user_id,
        path=path,
        ip=ip
    )
    if not result['allowed']:
        raise HTTPException(
            status_code=429,
            detail=f"请求过于频繁,请在 {result['retry_after']} 秒后重试",
            headers={
                'Retry-After': str(result['retry_after']),
                'X-RateLimit-Remaining': str(result['remaining'])
            }
        )
    response = await call_next(request)
    # 添加限流响应头
    response.headers['X-RateLimit-Remaining'] = str(result['remaining'])
    return response
# 示例接口
@app.get("/api/data")
async def get_data(request: Request):
    return {"message": "成功获取数据"}
# 测试代码
def test_rate_limiter():
    """测试限流器"""
    limiter = APIRateLimiter()
    # 测试用户限流
    print("=== 用户限流测试 ===")
    for i in range(15):
        result = limiter.allow_request(user_id="user1", path="/api/data", ip="192.168.1.1")
        if result['allowed']:
            print(f"请求 {i+1}: ✓ 允许 (剩余: {result['remaining']})")
        else:
            print(f"请求 {i+1}: ✗ 拒绝 (等待: {result['retry_after']}秒, {result.get('reason', '')})")
    # 模拟等待
    print("\n=== 等待5秒后 ===")
    time.sleep(5)
    # 测试管理员
    print("\n=== 管理员限流测试 ===")
    for i in range(5):
        result = limiter.allow_request(user_id="admin", path="/api/data", ip="192.168.1.2")
        print(f"请求 {i+1}: {'✓ 允许' if result['allowed'] else '✗ 拒绝'}")
if __name__ == "__main__":
    test_rate_limiter()

高级优化版本:自适应限流

import threading
import time
from collections import deque
from typing import Dict, Tuple
class AdaptiveSlidingWindowLimiter:
    """自适应滑动窗口限流器"""
    def __init__(self, max_requests, window_seconds, 
                 threshold_high=0.8, threshold_low=0.3):
        self.max_requests = max_requests
        self.window_seconds = window_seconds
        self.threshold_high = threshold_high  # 触发扩缩容的高阈值
        self.threshold_low = threshold_low    # 触发扩容的低阈值
        self.windows: Dict[int, int] = {}  # {子窗口起始时间: 请求数}
        self.total_count = 0
        self.lock = threading.Lock()
        # 动态调整参数
        self.current_max = max_requests
        self.adjustment_counter = 0
    def allow_request(self) -> bool:
        with self.lock:
            now = time.time()
            self._cleanup(now)
            if self.total_count < self.current_max:
                self._add_request(now)
                self._adjust_capacity(now)
                return True
            return False
    def _cleanup(self, now: float):
        """清理过期的子窗口"""
        expire_threshold = now - self.window_seconds
        for start_time in list(self.windows.keys()):
            if start_time < expire_threshold:
                self.total_count -= self.windows.pop(start_time)
    def _add_request(self, now: float):
        """添加请求"""
        window_start = int(now // self.window_seconds)
        self.windows[window_start] = self.windows.get(window_start, 0) + 1
        self.total_count += 1
    def _adjust_capacity(self, now: float):
        """根据负载动态调整容量"""
        self.adjustment_counter += 1
        if self.adjustment_counter % 10 != 0:  # 每10个请求评估一次
            return
        usage_ratio = self.total_count / self.current_max
        if usage_ratio > self.threshold_high:
            # 高负载,降低限制
            self.current_max = max(1, int(self.current_max * 0.8))
        elif usage_ratio < self.threshold_low:
            # 低负载,提高限制
            self.current_max = min(self.max_requests * 2, 
                                  int(self.current_max * 1.2))
    def get_stats(self) -> Dict:
        """获取统计信息"""
        with self.lock:
            return {
                'current_max': self.current_max,
                'total_count': self.total_count,
                'usage_percentage': (self.total_count / self.current_max * 100) 
                                    if self.current_max else 0,
                'windows': len(self.windows)
            }
# 测试自适应限流
def test_adaptive_limiter():
    limiter = AdaptiveSlidingWindowLimiter(
        max_requests=100,
        window_seconds=60,
        threshold_high=0.8,
        threshold_low=0.3
    )
    print("=== 自适应限流测试 ===")
    for i in range(200):
        allowed = limiter.allow_request()
        if i % 20 == 0:
            stats = limiter.get_stats()
            print(f"请求 {i:3d}: {'✓' if allowed else '✗'} | 当前容量: {stats['current_max']:4d} | "
                  f"已使用: {stats['total_count']:3d} | 使用率: {stats['usage_percentage']:.1f}%")
if __name__ == "__main__":
    test_adaptive_limiter()
  1. 固定窗口问题:存在边界突发,可以通过滑动窗口解决
  2. 日志方式精确:记录每个请求的时间戳,但内存消耗大
  3. 计数器方式高效:通过子窗口聚合,权衡内存和精度
  4. Redis方案适合分布式:保证一致性,支持多实例部署
  5. 实际应用建议
    • 使用子窗口(如10个小窗口)平衡精度和性能
    • 设置合理的缓存过期时间
    • 支持动态调整参数
    • 记录限流日志用于分析

这些案例覆盖了从简单到复杂的场景,你可以根据实际需求选择适合的实现方式。

抱歉,评论功能暂时关闭!