惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Stack Overflow Blog
Stack Overflow Blog
J
Java Code Geeks
Last Week in AI
Last Week in AI
人人都是产品经理
人人都是产品经理
博客园 - 【当耐特】
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
C
Check Point Blog
月光博客
月光博客
腾讯CDC
Engineering at Meta
Engineering at Meta
博客园 - Franky
Vercel News
Vercel News
D
Docker
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
F
Fortinet All Blogs
Microsoft Security Blog
Microsoft Security Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
雷峰网
雷峰网
Google DeepMind News
Google DeepMind News
Martin Fowler
Martin Fowler
GbyAI
GbyAI
B
Blog
Hugging Face - Blog
Hugging Face - Blog
T
Tailwind CSS Blog

Aimee's Blog

秒杀怎么防机器人:验证码之外的几道防线 防薅羊毛怎么做:号码风控、设备指纹与分层策略 批量打包下载怎么设计:流式 ZIP 与异步任务 图片处理怎么做:异步处理与 CDN 实时参数 大文件上传怎么设计:分片、断点续传、秒传 报表可重跑的设计:快照、水位线与幂等 UV 统计为什么有误差:精确计数与近似计数怎么选 大数据量导出怎么做:流式写入与异步任务 排行榜的两类需求,难度差了一个数量级 内容审核怎么选:先审后发,还是先发后审 实时热榜怎么设计:Redis ZSet 与热度分值 互关状态怎么保持同步:双向关系的维护与缓存失效 并发点赞的计数设计:原子操作与最终对账 删了父评论,子评论怎么处理——先想清楚用户看到的是什么 定时消息任务触发了两次 消息量大,写扩散还是读扩散——接到这个需求先问清楚规模 同一条消息推送了三次 活动结束了,用户还在收短信 接到注销需求,先问两个问题 微服务与服务拆分:何时拆、怎么拆 异步与事件驱动架构:把协作从「打电话」改成「发消息」 高可用设计:怎么让系统尽量不宕机 可扩展性设计:怎么让系统加机器就能扛更多 缓存架构:多级缓存怎么搭 高并发三板斧:限流、熔断、降级 架构设计到底在设计什么 —— 从单体到微服务的演进 服务成本账:一个服务一个月烧多少钱 可观测性:线上出问题怎么查 API 设计:好接口长什么样 消息队列:为什么要 MQ,以及丢失、重复、顺序怎么破
接口限流怎么选:固定窗口、滑动窗口、令牌桶
Aimee · 2026-08-17 · via Aimee's Blog

接口限流怎么选:固定窗口、滑动窗口、令牌桶

接口被脚本刷是迟早会遇到的事:同一个 user_id 高频密集请求同一个接口,间隔均匀,典型的自动化特征。

没有限流的接口,这些请求会全部打到数据库上,慢查询堆积、正常请求跟着遭殃。


固定窗口的边界漏洞

最简单的限流:每分钟最多 100 次,超了拒绝。

String key = "limit:" + userId + ":" + (System.currentTimeMillis() / 60_000);
long count = redis.incr(key);
redis.expire(key, 60, TimeUnit.SECONDS);
if (count > 100) throw new TooManyRequestsException();

这有个边界问题:59:59 发了 100 次,60:00 计数器归零,60:01 又能发 100 次。在分钟切换的 2 秒内可以发 200 次请求,是设定上限的两倍。


令牌桶:允许适度突发

令牌桶以固定速率往桶里放令牌,请求消耗令牌,桶满后新令牌溢出不累积。

特点:平时积累令牌,短时突发可以消耗积累,但长期速率不超过补充速率。

单机场景用 Guava:

// 每秒允许 10 个请求(令牌补充速率)
RateLimiter limiter = RateLimiter.create(10.0);

public void handleRequest() {
    if (!limiter.tryAcquire()) {
        throw new TooManyRequestsException();
    }
    // 处理请求
}

多实例部署时,Guava 的令牌桶是单机的,各实例独立计数。100 台机器,每台限 10 QPS,实际能放过 1000 QPS——需要分布式限流。


分布式滑动窗口:Redis + Sorted Set

精确的分布式限流,用 Redis Sorted Set 实现滑动窗口:

public boolean isAllowed(String userId, int maxRequests, long windowMs) {
    String key = "ratelimit:" + userId;
    long now = System.currentTimeMillis();
    long windowStart = now - windowMs;

    // Lua 脚本保证原子性
    String script = """
        local key = KEYS[1]
        local now = tonumber(ARGV[1])
        local windowStart = tonumber(ARGV[2])
        local maxRequests = tonumber(ARGV[3])
        
        redis.call('ZREMRANGEBYSCORE', key, 0, windowStart)
        local count = redis.call('ZCARD', key)
        if count < maxRequests then
            redis.call('ZADD', key, now, now)
            redis.call('EXPIRE', key, math.ceil((now - windowStart) / 1000) + 1)
            return 1
        end
        return 0
        """;

    Long result = redis.execute(script, List.of(key),
        String.valueOf(now), String.valueOf(windowStart), String.valueOf(maxRequests));
    return Long.valueOf(1).equals(result);
}

Lua 脚本保证"查计数 + 写入"的原子性,避免并发时多个请求同时判断"未超限"都通过。


限流后的响应

不要只返回 500 或让请求超时挂着,正确做法:

// HTTP 429 Too Many Requests
response.setStatus(429);
response.setHeader("Retry-After", "60");  // 告诉客户端 60 秒后重试
response.getWriter().write("{\"error\":\"too_many_requests\",\"retryAfter\":60}");

Retry-After 头让调用方知道等多久,合法的自动化客户端会尊重这个响应,减少无效重试。


限流的维度

维度key 格式适合拦截
用户limit:user:{userId}单用户滥用
IPlimit:ip:{ip}未登录刷接口
接口 + 用户limit:api:{path}:{userId}针对特定接口保护
全局接口limit:api:{path}保护后端总吞吐

多维度叠加使用,先过用户级,再过 IP 级,再过接口总量——任何一层超限就拒绝。