限流算法

4. 限流的底层原理解析

01 限流的目的

限流主要有两个目的:

  • 防止系统过载:确保系统在高负载情况下仍能保持稳定运行。
  • 保证服务质量:为所有用户提供公平的服务,避免某些用户占用过多资源。

02 限流算法的实现

2.1 固定窗口计数器算法

固定窗口计数器算法是一种基本的限流方法,它通过在固定时间窗口内跟踪请求的数量来实现限流。

// 这是一个简单的 C++ 实现案例
#include <iostream>
#include <mutex>
#include <chrono>

// FixedWindowCounter 类实现固定窗口计数器限流算法。
class FixedWindowCounter {
private:
    std::mutex mu;                                 // 用于同步访问,保证并发安全
    int count;                                     // 记录当前时间窗口内的请求数量
    int limit;                                     // 时间窗口内允许的最大请求数量
    std::chrono::steady_clock::time_point window;  // 记录当前时间窗口的开始时间
    std::chrono::steady_clock::duration duration;  // 时间窗口的持续时间

public:
    FixedWindowCounter(int limit, std::chrono::steady_clock::duration duration)
        : count(0), limit(limit), window(std::chrono::steady_clock::now()), duration(duration) {}

    // allow 方法用于判断当前请求是否被允许。
    bool allow() {
        std::lock_guard<std::mutex> lock(mu); // 自动管理锁,保证原子性
        auto now = std::chrono::steady_clock::now();
        // 如果当前时间超过了窗口的结束时间,重置计数器和窗口开始时间。
        if (now - window > duration) {
            count = 0;
            window = now;
        }
        // 如果当前计数小于限制,则增加计数并允许请求。
        if (count < limit) {
            count++;
            return true;
        }
        return false; // 达到限制,拒绝请求。
    }
};

int main() {
    // 设置每秒只允许10个请求
    FixedWindowCounter limiter(10, std::chrono::seconds(1));
    for (int i = 0; i < 15; i++) {
        if (limiter.allow()) {
            std::cout << "Request " << i + 1 << " allowed\n";
        } else {
            std::cout << "Request " << i + 1 << " rejected\n";
        }
    }
    return 0;
}
  • 实现原理:通过设置固定的时间窗口和请求限制数量,在每个窗口开始时清零计数器,请求到来时递增。达到阈值后拒绝后续请求,直至窗口重置。
  • 优点:实现简单直观,能保证固定窗口内的绝对限制。
  • 缺点:在窗口切换的瞬间可能有请求高峰(临界点问题);无法平滑处理突发流量。 固定窗口适用于请求分布相对均匀的场景。

2.2 滑动窗口算法

滑动窗口通过覆盖多个小时间段来平滑请求流量,避免瞬时高峰。

// 滑动窗口 C++ 演示代码
#include <iostream>
#include <vector>
#include <mutex>
#include <chrono>

class SlidingWindowLimiter {
    std::mutex mtx;
    std::vector<int> counters;
    int limit;
    std::chrono::steady_clock::time_point windowStart;
    std::chrono::milliseconds windowDuration;
    std::chrono::milliseconds interval;

public:
    SlidingWindowLimiter(int limit, std::chrono::milliseconds windowDuration, std::chrono::milliseconds interval)
        : limit(limit), windowStart(std::chrono::steady_clock::now()), windowDuration(windowDuration), interval(interval) {
        counters.resize(windowDuration.count() / interval.count(), 0);
    }

    bool allow() {
        std::lock_guard<std::mutex> lock(mtx);
        auto now = std::chrono::steady_clock::now();
        
        if (now - windowStart > windowDuration) slideWindow(now);

        auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - windowStart);
        int index = (elapsed.count() / interval.count()) % counters.size();

        if (counters[index] < limit) {
            counters[index]++;
            return true;
        }
        return false;
    }

private:
    void slideWindow(std::chrono::steady_clock::time_point now) {
        // 滑动窗口,忽略最旧的时间段
        for (size_t i = 0; i < counters.size() - 1; ++i) {
            counters[i] = counters[i + 1];
        }
        counters.back() = 0; // 重置最后一个时间段
        windowStart = now;
    }
};

int main() {
    SlidingWindowLimiter limiter(1, std::chrono::seconds(1), std::chrono::milliseconds(10));
    for (int i = 0; i < 100; i++) {
        std::cout << "Request " << i+1 << (limiter.allow() ? " allowed\n" : " rejected\n");
    }
    return 0;
}
  • 实现原理:将大时间窗口划分为多个小格子,每个请求落在对应的小格子内。随时间推移,废弃旧格子并引入新格子,统计总格子内的请求数。
  • 优点:相比固定窗口,能更平滑地处理请求,避免瞬时高峰。
  • 缺点:实现复杂,需要维护多个计数器,内存和计算开销更高。

2.3 漏桶算法

漏桶算法适合平滑突发流量,确保数据以均匀的速率被处理。

// 漏桶算法 C++ 演示代码
#include <iostream>
#include <queue>
#include <mutex>
#include <thread>
#include <chrono>

class LeakyBucket {
    std::queue<int> q; // 请求队列
    int capacity;
    std::mutex mtx;

public:
    LeakyBucket(int cap) : capacity(cap) {}

    bool push() {
        std::lock_guard<std::mutex> lock(mtx);
        if (q.size() < capacity) {
            q.push(1); // 模拟放入请求
            return true;
        }
        return false; // 桶满,丢弃请求
    }

    void process() {
        while (true) {
            bool hasRequest = false;
            {
                std::lock_guard<std::mutex> lock(mtx);
                if (!q.empty()) {
                    q.pop();
                    hasRequest = true;
                }
            }
            if (hasRequest) {
                std::cout << "Request processed\n";
            }
            std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 恒定速率处理
        }
    }
};

int main() {
    LeakyBucket lb(5);
    std::thread processor(&LeakyBucket::process, &lb); // 启动后台处理线程

    for (int i = 0; i < 10; i++) {
        if (lb.push()) std::cout << "Request " << i+1 << " accepted\n";
        else std::cout << "Request " << i+1 << " rejected\n";
    }
    std::this_thread::sleep_for(std::chrono::seconds(2));
    processor.detach();
    return 0;
}
  • 实现原理:用固定容量的队列模拟桶,以恒定速率从桶中取出请求处理。请求到达时入桶,桶满则溢出(拒绝)。
  • 优点:强制固定速率处理,流量绝对平滑。
  • 缺点:面对突发流量不够灵活,突发请求会被直接堆积或丢弃。

2.4 令牌桶算法

令牌桶允许一定程度的突发流量,同时保持长期的平均速率。

// 令牌桶算法 C++ 演示代码
#include <iostream>
#include <mutex>
#include <chrono>
#include <algorithm>

class TokenBucket {
    std::mutex mtx;
    int capacity;
    double tokens;
    double refillRate; // 每秒填充速率
    std::chrono::steady_clock::time_point lastRefill;

public:
    TokenBucket(int cap, double rate) 
        : capacity(cap), tokens(cap), refillRate(rate), lastRefill(std::chrono::steady_clock::now()) {}

    bool allow() {
        std::lock_guard<std::mutex> lock(mtx);
        auto now = std::chrono::steady_clock::now();
        std::chrono::duration<double> elapsed = now - lastRefill;

        // 按速率计算新增令牌
        double tokensToAdd = elapsed.count() * refillRate;
        if (tokensToAdd > 0) {
            tokens = std::min((double)capacity, tokens + tokensToAdd);
            lastRefill = now;
        }

        // 判断是否有充足令牌
        if (tokens >= 1.0) {
            tokens -= 1.0;
            return true;
        }
        return false;
    }
};

int main() {
    TokenBucket limiter(10, 2.0); // 容量10,每秒加2个
    for (int i = 0; i < 15; i++) {
        std::cout << "Request " << i+1 << (limiter.allow() ? " allowed\n" : " rejected\n");
    }
    return 0;
}
  • 实现原理:以固定速率向桶中添加令牌,直到达到容量上限。请求到来时需消耗令牌,令牌不足则拒绝。由于桶内能预存令牌,因此能处理瞬间突发流量。
  • 优点:允许突发流量,灵活度高。
  • 缺点:实现需维护时间戳和浮点计算,有一定同步开销。

03 限流的实现方式

3.1 应用层限流

在应用代码中通过中间件实现限流。拦截请求,判断是否放行。

// 应用层限流伪代码 (结合上述 TokenBucket)
#include <iostream>
#include <string>

// 模拟 HTTP 请求上下文
struct Context {
    bool aborted = false;
    void JSON(int code, const std::string& msg) { std::cout << "HTTP " << code << " : " << msg << "\n"; }
    void Abort() { aborted = true; }
    void Next() { if (!aborted) std::cout << "Processing handler logic...\n"; }
};

// 模拟限流中间件
auto Middleware(TokenBucket* tb) {
    return [tb](Context& c) {
        if (!tb->allow()) {
            c.JSON(429, "too many requests");
            c.Abort();
            return;
        }
        c.Next();
    };
}

int main() {
    TokenBucket tb(10, 1.0);
    auto limitMiddleware = Middleware(&tb);
    
    // 模拟请求流转
    Context ctx;
    limitMiddleware(ctx); 
    return 0;
}
  • 优点:易于集成,细粒度控制(按路由/用户限制)。
  • 缺点:增加应用侧同步开销,可能影响并发吞吐。

3.2 代理层限流

在 Nginx/HAProxy 等代理服务器层拦截,保护后端。 Nginx 配置示例

http {
    limit_req_zone $binary_remote_addr zone=mylimit:10m rate=1r/s;
    server {
        listen 80;
        location /api/ {
            limit_req zone=mylimit burst=5 nodelay;
            proxy_pass http://backend/;
        }
    }
}
  • 优点:全局保护,减轻后端应用负担。
  • 缺点:依赖中间件功能,分布式下需额外状态同步机制。

3.3 硬件层限流

在物理或云厂商的负载均衡器(如 F5、云 SLB)网关节点进行拦截。

04 限流策略

4.1 阈值设置

决定单位时间内的最大承载量。

// 带有明确阈值配置的扩展
class RateLimiterV2 {
    std::mutex mtx;
    int tokens;
    int capacity;
    double refillRate;
    int limit; // 强制熔断阈值

public:
    RateLimiterV2(int cap, double rate, int lim) : capacity(cap), tokens(cap), refillRate(rate), limit(lim) {}
    bool allow() {
        std::lock_guard<std::mutex> lock(mtx);
        if (tokens >= limit) return false; // 触发阈值熔断
        // ... 继续令牌计算 ...
        return true;
    }
};

4.2 请求分类

针对不同 API 赋予不同限流器。

#include <unordered_map>
#include <string>

std::unordered_map<std::string, RateLimiterV2*> RouteLimiterMap;

void SetRateLimiterForRoute(const std::string& route, int cap, double rate, int limit) {
    RouteLimiterMap[route] = new RateLimiterV2(cap, rate, limit);
}

// 模拟带路由的中间件
auto MiddlewareWithRoute(const std::string& route) {
    return [route](Context& c) {
        if (RouteLimiterMap.count(route) && !RouteLimiterMap[route]->allow()) {
            c.JSON(429, "too many requests");
            c.Abort();
            return;
        }
        c.Next();
    };
}

4.3 反馈机制

返回明确原因给调用方。

#include <tuple>

class FeedbackLimiter {
public:
    std::tuple<bool, std::string> allowWithFeedback() {
        // ... 业务判断逻辑 ...
        bool isAllowed = false; // 模拟被拒
        if (!isAllowed) {
            return {false, "Too many requests. Please try again later."};
        }
        return {true, ""};
    }
};

05 限流的考虑因素

5.1 公平性

依据 UserID 或 IP 分发限流器。

class FairLimiter {
    std::mutex mtx;
    std::unordered_map<std::string, RateLimiterV2*> limits;

public:
    std::tuple<bool, std::string> allow(const std::string& userID) {
        std::lock_guard<std::mutex> lock(mtx);
        if (limits.find(userID) == limits.end()) {
            limits[userID] = new RateLimiterV2(100, 10.0, 100);
        }
        return {true, "Allowed"}; // 调内部器 allowWithFeedback
    }
};

5.2 灵活性

支持运行时热更新参数。

class FlexibleLimiter {
    std::mutex mtx;
    int capacity;
    double refillRate;
    int limit;

public:
    void setParams(int cap, double rate, int lim) {
        std::lock_guard<std::mutex> lock(mtx);
        capacity = cap;
        refillRate = rate;
        limit = lim;
    }
    // 动态应用新参数去执行判断...
};

5.3 透明性

响应头注入当前剩余额度,供客户端削峰。

class TransparentLimiter {
    int currentTokens = 50; // 模拟额度
public:
    std::tuple<bool, std::string, int> allowWithStatus() {
        return {false, "Limit exceeded", currentTokens}; 
    }
};

// 中间件设置 Header (伪代码)
void MiddlewareWithTransparency(Context& w) {
    TransparentLimiter tl;
    auto [allowed, msg, tokens] = tl.allowWithStatus();
    if (!allowed) {
        // w.SetHeader("X-RateLimit-Remaining", std::to_string(tokens));
        w.JSON(429, msg);
        w.Abort();
        return;
    }
    w.Next();
}

posted @ 2026-09-04 18:25  光風霽月  阅读(7)  评论(0)    收藏  举报