后端限流:保护系统的 “保险丝” 设计

88 阅读4分钟

当流量突然激增(如秒杀活动、恶意攻击),后端系统可能因资源耗尽而崩溃 —— 数据库连接池占满、内存溢出、线程池耗尽,这些都是 “流量洪灾” 的典型症状。限流作为系统的 “保险丝”,能在流量超过阈值时 “熔断”,保障核心功能可用。

限流的核心目标与常用算法

限流的核心是控制单位时间内的请求量,确保系统在承载范围内稳定运行。常用算法有:

1. 令牌桶算法:平滑限流,支持突发流量

原理:系统以固定速率向桶中放入令牌(如 100 个 / 秒),请求来了需先获取令牌,无令牌则拒绝。

  • 优势:允许一定程度的突发流量(桶中有积累的令牌时)

  • 适用场景:API 网关、需要处理突发请求的服务

Guava 实现示例


// 创建令牌桶,每秒生成100个令牌,桶最大容量200(允许突发200请求)
RateLimiter limiter = RateLimiter.create(100.0, 200, TimeUnit.SECONDS);

// 尝试获取令牌,无令牌则返回false(非阻塞)
public boolean tryAcquire() {
    return limiter.tryAcquire();
}

// 接口中使用
@RequestMapping("/seckill")
public Result seckill() {
    if (!rateLimiter.tryAcquire()) {
        return Result.fail("当前人数过多,请稍后再试");
    }
    // 执行秒杀逻辑
    return seckillService.process();
}

2. 漏桶算法:严格控制流出速率

原理:请求先进入漏桶,桶以固定速率流出请求,溢出的请求被丢弃。

  • 优势:输出速率绝对平稳,适合对下游服务有严格速率要求的场景(如调用第三方 API,避免触发对方限流)
  • 劣势:无法应对突发流量

3. 计数器算法:简单粗暴,有临界问题

原理:单位时间内(如 1 秒)维护一个计数器,请求来了加 1,超过阈值则拒绝。

  • 问题:临界时刻可能允许 2 倍流量(如 59 秒和 0 秒各处理 100 请求,实际 1 秒内 200 请求)
  • 改进:滑动窗口算法(将 1 秒分成 10 个 100ms 窗口,更精细控制)

实战中的限流策略

1. 分级限流:核心接口优先保障

对不同接口设置不同阈值,核心接口(如支付)限流阈值高于非核心接口(如商品浏览):


// 基于Redis的分布式限流(伪代码)
public boolean limitByApi(String apiKey) {
    String key = "rate_limit:" + apiKey;
    Long count = redisTemplate.opsForValue().increment(key, 1);
    if (count == 1) {
        redisTemplate.expire(key, 1, TimeUnit.SECONDS); // 1秒过期
    }
    // 不同接口不同阈值,从配置中心获取
    int threshold = configService.getThreshold(apiKey);
    return count <= threshold;
}

2. 分布式限流:集群环境下的统一控制

单机限流无法应对集群场景(如 10 台机器,每台限 100QPS,实际总限流 1000QPS),需用 Redis+Lua 脚本实现分布式限流:


-- Redis Lua脚本实现令牌桶(简化版)
local key = KEYS[1]
local capacity = tonumber(ARGV[1]) -- 桶容量
local rate = tonumber(ARGV[2]) -- 每秒生成令牌数
local now = tonumber(ARGV[3]) -- 当前时间戳

-- 初始化桶
local bucket = redis.call('hgetall', key)
if #bucket == 0 then
    redis.call('hset', key, 'tokens', capacity, 'last_time', now)
    bucket = { 'tokens', capacity, 'last_time', now }
end

-- 计算当前令牌数
local tokens = tonumber(bucket[2])
local last_time = tonumber(bucket[4])
local elapsed = now - last_time
local new_tokens = math.min(capacity, tokens + elapsed * rate)

-- 尝试获取令牌
if new_tokens >= 1 then
    redis.call('hset', key, 'tokens', new_tokens - 1, 'last_time', now)
    return 1 -- 允许访问
else
    return 0 -- 拒绝访问
end

3. 限流后的优雅降级

限流不是简单返回 “系统繁忙”,而是提供降级方案:

  • 返回缓存数据(如商品详情返回 5 分钟前的缓存)
  • 提示 “非核心功能暂时不可用”
  • 引导用户稍后重试(附预计可用时间)

注意事项

  • 限流阈值需动态调整:通过压测确定基准值,结合监控实时调整(如促销时临时提高阈值)
  • 避免 “级联限流”:上游服务限流不当导致下游服务空闲,需协调上下游阈值
  • 监控告警:当限流次数突增时,及时告警排查是否有异常流量或系统瓶颈

限流的本质不是 “限制用户”,而是在保护系统稳定的前提下,最大化服务可用性 —— 就像保险丝,平时不起眼,关键时刻能救命。