API接口限流实战:令牌桶算法实现与分布式限流方案落地

限流是后端开发中保护服务的最后一道防线。接口被爬虫扫、营销活动流量洪峰、下游服务变慢导致线程池打满,这些场景下没有限流的接口会直接拖垮整个应用。API接口限流的核心是把流量控制在系统容量之内,本文给出从单机令牌桶到分布式限流的完整落地方案。

限流算法选型:固定窗口、滑动窗口与令牌桶

三种主流算法各有适用场景。固定窗口实现最简单,按分钟计数,临界点有突刺问题——窗口结束前1秒和下个窗口开始后1秒可以打进2倍流量;滑动窗口把大窗口切成小格子滚动统计,精度高但内存开销大;令牌桶以恒定速率往桶里放令牌,请求拿到令牌才放行,允许一定突发流量,是业务接口最均衡的选择。

选型结论:网关层全局防护用滑动窗口(配合Nginx limit_req);业务接口用令牌桶(Guava RateLimiter或Redis实现);削峰场景如秒杀下单用漏桶思想排队。单机令牌桶用Guava最快落地:

import com.google.common.util.concurrent.RateLimiter;

public class OrderService {
    // 每秒放行200个请求,突发容量约1秒量
    private final RateLimiter limiter = RateLimiter.create(200.0);

    public Result createOrder(OrderRequest req) {
        // tryTimeout超时直接拒绝,不让线程排队堆积
        if (!limiter.tryAcquire(50, TimeUnit.MILLISECONDS)) {
            return Result.fail(429, "请求过于频繁,请稍后重试");
        }
        return doCreate(req);
    }
}

单机限流的问题:集群N台机器,流量按权重分摊后,单机限流阈值无法保证集群总量。实例扩缩容时限流阈值要跟着改,配置漂移极易出错。

分布式限流:Redis+Lua保证原子性

集群级限流把计数器放到Redis,用Lua脚本保证”读取-判断-扣减”原子执行,避免并发超卖。令牌桶的Redis实现核心是惰性补偿:不依赖定时任务真实放令牌,而是在每次请求时按时间差补算应发令牌数。

-- token_bucket.lua:KEYS[1]=限流键 ARGV=rate,capacity,now,requested
local key = KEYS[1]
local rate = tonumber(ARGV[1])        -- 每秒生成令牌数
local capacity = tonumber(ARGV[2])   -- 桶容量(突发上限)
local now = tonumber(ARGV[3])         -- 当前毫秒时间戳
local requested = tonumber(ARGV[4])   -- 本次申请令牌数

local bucket = redis.call('HMGET', key, 'tokens', 'last_time')
local tokens = tonumber(bucket[1]) or capacity
local last_time = tonumber(bucket[2]) or now

-- 按时间差补偿令牌(惰性计算,无需定时任务)
local delta = math.max(0, (now - last_time) / 1000.0)
tokens = math.min(capacity, tokens + delta * rate)

local allowed = 0
if tokens >= requested then
    tokens = tokens - requested
    allowed = 1
end

redis.call('HMSET', key, 'tokens', tokens, 'last_time', now)
redis.call('PEXPIRE', key, math.ceil(capacity / rate * 1000) + 1000)
return allowed

Java侧封装调用,注意Lua脚本的KEYS/ARGV顺序不可颠倒:

@Component
public class RedisRateLimiter {
    private static final DefaultRedisScript<Long> SCRIPT;
    static {
        SCRIPT = new DefaultRedisScript<>();
        SCRIPT.setLocation(new ClassPathResource("lua/token_bucket.lua"));
        SCRIPT.setResultType(Long.class);
    }

    @Resource private StringRedisTemplate redisTemplate;

    public boolean tryAcquire(String key, int rate, int capacity, int requested) {
        Long r = redisTemplate.execute(SCRIPT,
                Collections.singletonList(key),
                String.valueOf(rate), String.valueOf(capacity),
                String.valueOf(System.currentTimeMillis()),
                String.valueOf(requested));
        return r != null && r == 1L;
    }
}

限流层级设计与阈值计算方法

生产环境限流不是单点配置,而是分层防护体系:

1. 接入层(Nginx/网关):按IP限流,挡住单源异常流量,limit_req_zone配置每秒10个请求的突发桶;

2. 服务层(应用内+Redis):按接口+用户维度限流,阈值按压测容量除以安全系数0.7计算,压测单接口承载800QPS则限流阈值设560;

3. 依赖层(下游保护):调用第三方接口按对方配额限流,防止把下游打挂后连锁故障。

阈值计算的常见误区是拿CPU使用率当容量指标,正确的容量基线是压测出的”错误率拐点”——QPS上升过程中错误率从0.01%跳到1%的位置就是系统真实容量,留30%余量作为限流线。

被限流请求的处理策略

拒绝不是唯一选项。按业务重要性分级处理:

1. 直接拒绝返回429,响应头带Retry-After告知重试时间,适用普通查询接口;

2. 排队等待(漏桶),适用写操作,队列长度要有限防止内存溢出;

3. 降级兜底,读接口返回缓存快照或默认数据,牺牲新鲜度保可用性。

@GetMapping("/hot/products")
public Result listHotProducts() {
    if (!userLimiter.tryAcquire("hot:list:" + currentUserId(), 5, 10, 1)) {
        // 限流后走本地缓存兜底,不返回错误页
        return Result.ok(localCache.getSnapshot("hot_products"));
    }
    return Result.ok(productService.listHot());
}

监控上把限流触发次数接入Prometheus,counter按接口维度打标,触发速率突增说明要么有异常流量、要么容量需要扩容,两个方向都要有人跟进。

常见踩坑与验证清单

1. Redis限流的热键问题:全部流量打到一个分片,用key加哈希槽分散或客户端本地预扣减+异步同步;

2. 时钟回拨导致令牌补偿异常,Lua内用Redis的TIME命令取时间而非业务服务器传参;

3. 限流粒度遗漏:只按用户限流,攻击者换一批账号照样打穿,关键接口叠加IP维度。

上线验证三条:单机压测确认阈值生效、Redis故障时限流器fail-open或fail-close的演练、限流返回的错误码与前端重试逻辑对齐。限流不是配置完就结束的功能,容量随业务变化,每季度复查一次阈值是必要的运维动作。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/api-jie-kou-xian-liu-shi-zhan-ling-pai-tong-suan-fa-shi/

(0)
小编小编
上一篇 3小时前
下一篇 2026年8月19日

相关推荐