HTTP服务实现用户级窗口限流

核心需求

对每个用户,在【任意连续的 100 秒】时间范围内,累计请求次数不得超过 60 次

补充:区别于「固定窗口限流」(比如每 100 秒一个区间),固定窗口有致命缺陷:比如用户在第 99 秒发 60 次、第 101 秒再发 60 次,这 2 秒内实际请求 120 次,但固定窗口会判定合规,而滑动窗口能精准规避这个问题 ,完全匹配「任意 100 秒」要求,是该场景的唯一正确选型

结构分析

1. 「用户」的唯一标识怎么定?

HTTP 服务做用户限流,必须有用户唯一标识 key,根据业务场景二选一即可(优先级从上到下):

  • 优先方案:客户端 IP 地址X-Real-IP/remote_addr),无需用户登录,无侵入性,99% 的 HTTP 限流场景都用这个,最实用。
  • 精准方案:用户 UID/Token(登录后从 Header / 参数中获取),适合有用户体系的服务,能精准区分「同一 IP 下的不同用户」。
2. 实现的核心技术选型(C++)

首选std::unordered_map 做核心存储,原因:

  • 单用户的查询是「根据用户 key 精准查询」,不需要有序,unordered_mapO(1) 查询性能远优于map的 O (logN);
  • 数据量极小:哪怕 1 万个用户,每个用户仅存储最多 60 个时间戳,内存占用可以忽略不计。
  • 线程安全必须加:HTTP 服务器都是多线程 / 多协程 模型,必须加互斥锁std::mutex保证并发安全,否则会出现计数错乱。

方案一:基于「请求时间戳队列」的滑动窗口限流

核心实现思路
  • 每个用户 维护一个「请求时间戳队列」(只存该用户最近的请求时间,最多存 60 个);
  • 当该用户新发起 1 次请求 时,执行 3 个核心操作:
    • 第一步:清理过期的时间戳 → 从队列头部删除「当前时间 - 时间戳 > 100 秒」的所有记录(这些请求已经不在 100 秒窗口内了,无需计数);
    • 第二步:判断是否超限 → 此时队列的剩余元素个数,就是「该用户在最近 100 秒内的请求次数」;如果个数 ≥60 → 拒绝请求;如果 <60 → 允许请求;
    • 第三步:记录本次请求 → 允许请求的话,把「当前请求的时间戳」追加到队列尾部。
  • 过期机制:天然内存友好,每个用户的队列最多只存 60 个时间戳,不会无限膨胀;用户长时间无请求,队列里的时间戳会被逐步清理为空。

优点

  • 绝对精准,完美匹配「任意 100 秒最多 60 次」的需求;
  • 逻辑极简,代码量少,易实现、易维护、无 bug;
  • 内存占用极低:每个用户最多存 60 个时间戳(double 类型,8 字节 / 个,仅 480 字节 / 用户);
  • 性能极高:单次请求的操作就是「队列头尾删除 + 追加」,都是 O (1) 操作,查询用户是 unordered_map 的 O (1)。
实现代码
cpp 复制代码
#include <iostream>
#include <unordered_map>
#include <deque>
#include <mutex>
#include <chrono>
#include <string>

// 限流核心配置:按需修改,完美匹配你的需求
const int TIME_WINDOW_SEC = 100;    // 时间窗口:100秒
const int MAX_REQUEST_COUNT = 60;   // 窗口内最大请求次数:60次

// 滑动窗口限流类(单例/全局均可,这里封装成独立类)
class RateLimiter {
private:
    // 核心存储:key=用户唯一标识(IP/UID),value=该用户的请求时间戳队列(毫秒级)
    std::unordered_map<std::string, std::deque<double>> userRequestTimestamps;
    // 互斥锁:保证多线程并发安全,HTTP服务必须加!
    std::mutex mtx;

    // 获取当前系统时间戳(毫秒级,返回double是为了精度,也可以用long long)
    double getCurrentTimestampMs() const {
        auto now = std::chrono::system_clock::now();
        auto duration = now.time_since_epoch();
        return std::chrono::duration_cast<std::chrono::milliseconds>(duration).count() / 1000.0;
    }

public:
    // 核心限流方法:传入用户标识,返回true=允许请求,false=拒绝请求
    bool allowRequest(const std::string& userId) {
        std::lock_guard<std::mutex> lock(mtx); // 加锁,自动解锁,线程安全
        double now = getCurrentTimestampMs();
        double expireTime = now - TIME_WINDOW_SEC; // 过期时间:当前时间-100秒

        // 1. 清理该用户的过期请求时间戳(核心步骤)
        auto& timestamps = userRequestTimestamps[userId];
        while (!timestamps.empty() && timestamps.front() < expireTime) {
            timestamps.pop_front(); // 移除窗口外的请求记录
        }

        // 2. 判断是否超过限流阈值
        if (timestamps.size() >= MAX_REQUEST_COUNT) {
            return false; // 超限,拒绝请求
        }

        // 3. 未超限,记录本次请求的时间戳
        timestamps.push_back(now);
        return true;
    }

    // 可选:手动清理指定用户的限流记录(比如用户登出)
    void clearUser(const std::string& userId) {
        std::lock_guard<std::mutex> lock(mtx);
        userRequestTimestamps.erase(userId);
    }


}
};

// 全局限流实例,整个服务共享一个即可
static RateLimiter g_rateLimiter;

// ===================== 测试调用示例 =====================
int main() {
    // 模拟用户标识:这里用IP地址作为示例
    std::string userIp = "192.168.1.100";

    // 模拟该用户连续发起65次请求
    for (int i = 1; i <= 65; ++i) {
        bool allowed = g_rateLimiter.allowRequest(userIp);
        if (allowed) {
            std::cout << "第" << i << "次请求:允许,正常处理HTTP请求" << std::endl;
        } else {
            std::cout << "第" << i << "次请求:拒绝,触发限流(100秒内最多60次)" << std::endl;
        }
    }

    return 0;
}
测试运行结果
cpp 复制代码
第1次请求:允许,正常处理HTTP请求
...
第60次请求:允许,正常处理HTTP请求
第61次请求:拒绝,触发限流(100秒内最多60次)
第62次请求:拒绝,触发限流(100秒内最多60次)
第63次请求:拒绝,触发限流(100秒内最多60次)
第64次请求:拒绝,触发限流(100秒内最多60次)
第65次请求:拒绝,触发限流(100秒内最多60次)
  • 纯 C++ 标准库实现,无任何第三方依赖,可无缝集成到任何 C++ HTTP 服务器(muduo/asio/cpprestsdk/nginx C++ 模块等);
  • 线程安全:全局互斥锁保证多线程并发安全,HTTP 服务必加;
  • 封装成独立的限流类RateLimiter,调用一行代码搞定,极低侵入性;
  • 时间戳用毫秒级精度,避免同一毫秒多次请求的重复计数问题;
  • 自动清理过期数据,无内存泄漏风险;
  • 核心参数可配置:窗口时长100秒最大请求数60次,改常量即可。
集成到HTTP服务
cpp 复制代码
// 你的HTTP请求处理函数
void handleHttpRequest(const HttpRequest& req, HttpResponse& resp) {
    // 步骤1:获取用户唯一标识(二选一)
    std::string userId = req.getClientIp(); // 方案1:用客户端IP(推荐)
    // std::string userId = req.getHeader("token"); // 方案2:用登录用户的token/UID

    // 步骤2:调用限流判断,一行代码搞定!
    if (!g_rateLimiter.allowRequest(userId)) {
        // 步骤3:触发限流,返回标准HTTP响应码 + 提示信息
        resp.setStatusCode(429); // HTTP 429 Too Many Requests 标准限流响应码
        resp.setBody("请求过于频繁,请稍后再试(100秒内最多60次请求)");
        return;
    }

    // 步骤4:未触发限流,正常处理业务逻辑
    resp.setStatusCode(200);
    resp.setBody("请求成功,业务处理完成");
}

方案二:计数器分片式近似滑动窗口限流

这个方案的核心是 "空间换时间" ,通过拆分大时间窗口为小时间片 ,用数组计数器 替代方案一的时间戳队列 ,彻底避免「队列头部删除」的操作开销,在超高并发场景 下性能优势明显,是方案一的性能优化版

它的 trade-off 是 "精准度换性能" ,存在极小的限流误差(误差范围 = 单个时间片的时长),但对于绝大多数 HTTP 服务场景,这个误差完全可接受。

核心原理

这个我们采用数格子的例子直接讲解:100 秒最多 60 次请求,100 秒 = 10 个 10 秒分片

阶段 1:用户第 0 秒~第 9 秒,发起 20 次请求
  • 第 1 步:时间在 0-9 秒 → 定位到「格子 0」
  • 第 2 步:用户第一次请求,无过期格子
  • 第 3 步:总和 = 0 <60 → 允许请求
  • 第 4 步:格子 0 的数字从 0→1→2→......→20
  • 此时格子状态:[20,0,0,0,0,0,0,0,0,0] 总和 = 20
阶段 2:用户第 10 秒~第 19 秒,发起 25 次请求
  • 第 1 步:时间在 10-19 秒 → 定位到「格子 1」
  • 第 2 步:间隔 10 秒,无过期格子
  • 第 3 步:总和 = 20 <60 → 允许请求
  • 第 4 步:格子 1 的数字从 0→1→......→25
  • 此时格子状态:[20,25,0,0,0,0,0,0,0,0] 总和 = 45
阶段 3:用户第 20 秒~第 29 秒,发起 15 次请求
  • 第 1 步:时间在 20-29 秒 → 定位到「格子 2」
  • 第 2 步:间隔 10 秒,无过期格子
  • 第 3 步:总和 = 45 <60 → 允许请求
  • 第 4 步:格子 2 的数字从 0→1→......→15
  • 此时格子状态:[20,25,15,0,0,0,0,0,0,0] 总和 = 60 → 刚好到阈值!
阶段 4:用户第 30 秒,发起第 61 次请求
  • 第 1 步:时间在 30-39 秒 → 定位到「格子 3」
  • 第 2 步:间隔 10 秒,无过期格子
  • 第 3 步:总和 = 60 ≥60 → 拒绝请求
  • 此时格子状态不变,用户收到「请求频繁」的提示。
阶段 5:用户第 105 秒,再次发起请求(暂停了 75 秒)
  • 第 1 步:时间在 105 秒 → 定位到「格子 0」(105/10=10,余 5 → 格子 0)
  • 第 2 步:当前时间 105 秒 - 上一次请求时间 30 秒 =75 秒 <100 秒 → 计算过期分片数 = 75/10=7 个 → 把 7 个过期的格子清零
  • 第 3 步:清零后总和 = 格子 7+8+9+0 = 0+0+0+0 =0 <60 → 允许请求
  • 第 4 步:格子 0 的数字从 0→1
  • 此时格子状态:[1,0,0,0,0,0,0,0,0,0] 总和 = 1 → 窗口完成了「滑动」,重新计数!
实现代码
cpp 复制代码
#include <iostream>
#include <unordered_map>
#include <vector>
#include <mutex>
#include <chrono>
#include <string>

// ==================== 核心配置参数 ====================
const int TIME_WINDOW_SEC = 100;       // 总限流窗口:100秒
const int MAX_REQUEST_COUNT = 60;      // 窗口内最大请求数:60次
const int SLOT_COUNT = 10;             // 分片数量:10个
const int SLOT_DURATION_SEC = TIME_WINDOW_SEC / SLOT_COUNT; // 单片时长:10秒

// ==================== 用户限流数据结构 ====================
struct UserLimitData {
    std::vector<int> slots;            // 分片计数器数组
    long long last_update_time;        // 最后请求时间戳(秒级)

    // 构造函数:初始化计数器数组为全0
    UserLimitData() : slots(SLOT_COUNT, 0), last_update_time(0) {}
};

// ==================== 限流核心类 ====================
class ShardedRateLimiter {
private:
    // 核心存储:key=用户标识,value=用户限流数据
    std::unordered_map<std::string, UserLimitData> user_limit_map;
    // 并发安全锁
    std::mutex mtx;

    // 获取当前时间戳(秒级,简化计算)
    long long getCurrentTimestampSec() const {
        auto now = std::chrono::system_clock::now();
        return std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
    }

public:
    ShardedRateLimiter() {
        // 提前预留容量,避免高频扩容
        user_limit_map.reserve(10000);
    }

    // 核心限流方法:返回true=允许请求,false=拒绝请求
    bool allowRequest(const std::string& userId) {
        std::lock_guard<std::mutex> lock(mtx);
        long long now = getCurrentTimestampSec();
        // 获取或初始化该用户的限流数据(unordered_map的[]运算符自动初始化)
        UserLimitData& user_data = user_limit_map[userId];

        // ========== 步骤1:计算当前分片索引 ==========
        // 分片索引 = (当前时间 / 单片时长) % 分片数 → 循环复用数组
        int current_slot_idx = (now / SLOT_DURATION_SEC) % SLOT_COUNT;

        // ========== 步骤2:清理过期分片 ==========
        long long time_diff = now - user_data.last_update_time;
        if (time_diff >= TIME_WINDOW_SEC) {
            // 情况1:所有分片都已过期 → 重置整个计数器数组
            std::fill(user_data.slots.begin(), user_data.slots.end(), 0);
        } else if (time_diff >= SLOT_DURATION_SEC) {
            // 情况2:部分分片过期 → 只清空过期的分片(避免全量重置)
            // 计算需要清空的分片数量
            int expired_slot_count = time_diff / SLOT_DURATION_SEC;
            // 从last_slot_idx的下一个分片开始,清空expired_slot_count个分片
            int last_slot_idx = (user_data.last_update_time / SLOT_DURATION_SEC) % SLOT_COUNT;
            for (int i = 1; i <= expired_slot_count; ++i) {
                int expired_idx = (last_slot_idx + i) % SLOT_COUNT;
                user_data.slots[expired_idx] = 0;
            }
        }

        // ========== 步骤3:求和未过期分片,判断是否超限 ==========
        int total_requests = 0;
        for (int count : user_data.slots) {
            total_requests += count;
        }
        if (total_requests >= MAX_REQUEST_COUNT) {
            return false; // 超限,拒绝请求
        }

        // ========== 步骤4:未超限,累加当前分片计数 ==========
        user_data.slots[current_slot_idx]++;
        user_data.last_update_time = now;
        return true;
    }

    // 可选:清理指定用户的限流记录
    void clearUser(const std::string& userId) {
        std::lock_guard<std::mutex> lock(mtx);
        user_limit_map.erase(userId);
    }

    // 可选:定时清理僵尸用户(长时间无请求的用户)
    void cleanExpiredUsers() {
        std::lock_guard<std::mutex> lock(mtx);
        long long now = getCurrentTimestampSec();
        for (auto it = user_limit_map.begin(); it != user_limit_map.end();) {
            if (now - it->second.last_update_time >= TIME_WINDOW_SEC) {
                it = user_limit_map.erase(it);
            } else {
                ++it;
            }
        }
    }
};

// 全局限流实例
static ShardedRateLimiter g_sharded_limiter;

// ==================== 测试调用示例 ====================
int main() {
    std::string user_ip = "192.168.1.100";
    // 模拟用户连续发起65次请求
    for (int i = 1; i <= 65; ++i) {
        bool allowed = g_sharded_limiter.allowRequest(user_ip);
        if (allowed) {
            std::cout << "第" << i << "次请求:允许(当前累计:" << i << ")" << std::endl;
        } else {
            std::cout << "第" << i << "次请求:拒绝(触发限流,100秒内最多60次)" << std::endl;
        }
    }
    return 0;
}

测试运行结果

cpp 复制代码
第1次请求:允许(当前累计:1)
...
第60次请求:允许(当前累计:60)
第61次请求:拒绝(触发限流,100秒内最多60次)
第62次请求:拒绝(触发限流,100秒内最多60次)
第63次请求:拒绝(触发限流,100秒内最多60次)
第64次请求:拒绝(触发限流,100秒内最多60次)
第65次请求:拒绝(触发限流,100秒内最多60次)
HTTP 服务集成
cpp 复制代码
void handleHttpRequest(const HttpRequest& req, HttpResponse& resp) {
    // 步骤1:获取用户标识(IP/Token)
    std::string userId = req.getHeader("X-Real-IP");
    if (userId.empty()) userId = req.getClientIp();

    // 步骤2:调用分片式限流(直接替换方案一的g_rateLimiter)
    if (!g_sharded_limiter.allowRequest(userId)) {
        resp.setStatusCode(429); // HTTP 429 标准响应码
        resp.setBody("请求过于频繁,请10秒后重试(100秒内最多60次)");
        return;
    }

    // 步骤3:正常处理业务
    resp.setStatusCode(200);
    resp.setBody("请求成功");
}
相关推荐
代码村新手3 小时前
C++-类和对象(上)
开发语言·c++
全栈小精灵4 小时前
Winform入门
开发语言·机器学习·c#
独自破碎E4 小时前
RabbitMQ中的Prefetch参数
分布式·rabbitmq
心静财富之门4 小时前
退出 for 循环,break和continue 语句
开发语言·python
txinyu的博客4 小时前
map和unordered_map的性能对比
开发语言·数据结构·c++·算法·哈希算法·散列表
Mr -老鬼4 小时前
Rust适合干什么?为什么需要Rust?
开发语言·后端·rust
予枫的编程笔记4 小时前
【Java集合】深入浅出 Java HashMap:从链表到红黑树的“进化”之路
java·开发语言·数据结构·人工智能·链表·哈希算法
ohoy4 小时前
RedisTemplate 使用之Set
java·开发语言·redis
mjhcsp4 小时前
C++ 后缀数组(SA):原理、实现与应用全解析
java·开发语言·c++·后缀数组sa