限流与降级策略:单机限流(令牌桶/漏桶),分布式限流(Redis+Lua),服务降级与熔断(Hystrix/Sentinel)

这一章咱们聊聊限流和降级。说实话,这两个东西是系统稳定性的最后一道防线。我见过太多系统,平时跑得欢,流量一上来就崩了。嗯,说白了就是没做好限流。

单机限流:令牌桶 vs 漏桶

先说说单机限流。你想想看,一个服务实例能扛多少请求?总有个上限吧。令牌桶和漏桶就是两种最常见的限流算法。

令牌桶算法

令牌桶的思路很简单:系统以固定速率往桶里放令牌,请求来了就拿令牌,拿不到就等或者直接拒绝。我习惯用这个算法,因为它能应对突发流量——桶里攒的令牌可以一次性用完。

// 伪代码示例
public class TokenBucket {
    private long capacity;    // 桶容量
    private long tokens;      // 当前令牌数
    private long rate;        // 令牌放入速率
    private long lastRefill;  // 上次补充时间

    public boolean tryAcquire() {
        refill(); // 先补充令牌
        if (tokens > 0) {
            tokens--;
            return true;
        }
        return false;
    }

    private void refill() {
        long now = System.currentTimeMillis();
        long elapsed = now - lastRefill;
        tokens = Math.min(capacity, tokens + elapsed * rate);
        lastRefill = now;
    }
}

我的经验:令牌桶适合大部分业务场景。我在项目中遇到过秒杀系统,用令牌桶控制瞬间流量,效果不错。但要注意桶容量别设太大,否则突发流量可能把下游打垮。

漏桶算法

漏桶就完全不一样了。它像一个漏斗,水(请求)进来,以固定速率流出去。流量太猛?直接溢出丢弃。说白了,它强制平滑流量,不允许任何突发。

// 伪代码示例
public class LeakyBucket {
    private long capacity;    // 桶容量
    private long water;       // 当前水量
    private long rate;        // 漏水速率
    private long lastLeak;    // 上次漏水时间

    public boolean tryAcquire() {
        leak(); // 先漏水
        if (water < capacity) {
            water++;
            return true;
        }
        return false;
    }

    private void leak() {
        long now = System.currentTimeMillis();
        long elapsed = now - lastLeak;
        water = Math.max(0, water - elapsed * rate);
        lastLeak = now;
    }
}

避坑指南:我曾经在一个消息推送服务里用了漏桶,结果发现用户投诉推送延迟。为什么呢?因为漏桶强制匀速,高峰期请求排队太久。后来换成令牌桶,允许一定突发,问题就解决了。

算法 特点 适用场景
令牌桶 允许突发,平滑整体速率 秒杀、API网关
漏桶 强制匀速,不允许突发 消息队列、数据库写入

分布式限流:Redis + Lua

单机限流好理解,但微服务架构下,你得考虑全局限流。比如一个用户调用多个服务,每个服务单独限流,加起来可能还是把系统打崩了。这时候就需要分布式限流。

我推荐用 Redis + Lua 脚本。为什么?因为 Lua 脚本在 Redis 里是原子执行的,不会出现并发问题。你想想看,多个服务实例同时读写同一个 key,不加锁怎么行?

-- Lua 脚本:令牌桶分布式限流
local key = KEYS[1]           -- 限流 key
local capacity = tonumber(ARGV[1])  -- 桶容量
local rate = tonumber(ARGV[2])      -- 令牌速率
local now = tonumber(ARGV[3])       -- 当前时间戳

-- 获取当前令牌数
local tokens = redis.call('get', key)
if not tokens then
    tokens = capacity
else
    tokens = tonumber(tokens)
end

-- 计算需要补充的令牌
local lastRefill = redis.call('get', key .. ':last')
if not lastRefill then
    lastRefill = now
else
    lastRefill = tonumber(lastRefill)
end

local elapsed = now - lastRefill
tokens = math.min(capacity, tokens + elapsed * rate)

-- 判断是否允许请求
if tokens >= 1 then
    tokens = tokens - 1
    redis.call('set', key, tokens)
    redis.call('set', key .. ':last', now)
    return 1  -- 允许
else
    return 0  -- 拒绝
end

核心要点:Redis 限流的关键是原子性。Lua 脚本保证了「读-计算-写」三步操作不会被其他请求打断。我在项目中用这个方案扛过双十一流量,单机 QPS 上万,Redis 延迟不到 1ms。

服务降级与熔断:Hystrix / Sentinel

限流是主动防御,降级和熔断是被动应对。说白了,系统扛不住了怎么办?你得有预案。

服务降级

降级就是主动放弃一些非核心功能。比如电商大促时,商品详情页的「你可能喜欢」推荐模块可以降级,只展示静态数据。我习惯把降级策略分为三级:

  • 一级降级:关闭非核心功能(如日志、监控上报)
  • 二级降级:简化核心功能(如只返回缓存数据)
  • 三级降级:返回默认值或错误提示(如「系统繁忙,请稍后再试」)

服务熔断

熔断是降级的升级版。当某个下游服务连续出错,直接切断调用,避免雪崩。Hystrix 和 Sentinel 都实现了熔断器模式,有三个状态:

状态 说明 触发条件
CLOSED(关闭) 正常调用 错误率低于阈值
OPEN(打开) 直接拒绝调用 错误率超过阈值
HALF_OPEN(半开) 尝试放行一个请求 熔断时间到,试探恢复
// Sentinel 熔断配置示例
FlowRule rule = new FlowRule();
rule.setResource("orderService");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setCount(100);  // 单机 QPS 上限 100

DegradeRule degradeRule = new DegradeRule();
degradeRule.setResource("orderService");
degradeRule.setGrade(RuleConstant.DEGRADE_GRADE_RT);
degradeRule.setCount(500);  // 响应时间超过 500ms 触发熔断
degradeRule.setTimeWindow(10);  // 熔断持续 10 秒

我曾经踩过的坑:有一次熔断时间窗口设得太短(3秒),结果下游服务还没恢复,熔断器就反复打开关闭,导致系统抖动更严重。后来我改成 30 秒,配合健康检查,问题才解决。记住:熔断时间窗口要大于下游服务的恢复时间。

总结一下

限流、降级、熔断这三板斧,是系统稳定性的基石。我个人习惯这样组合使用:

  • 入口网关用令牌桶限流,防止流量洪峰
  • 核心服务用 Redis + Lua 做分布式限流
  • 非核心功能配置降级策略,压力大时自动降级
  • 下游依赖配置熔断,防止雪崩

你想想看,如果每个服务都做好这几点,系统想崩都难。嗯,这一章就到这里,下一章咱们聊聊更进阶的稳定性策略。