限流与降级策略:单机限流(令牌桶/漏桶),分布式限流(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 做分布式限流
- 非核心功能配置降级策略,压力大时自动降级
- 下游依赖配置熔断,防止雪崩
你想想看,如果每个服务都做好这几点,系统想崩都难。嗯,这一章就到这里,下一章咱们聊聊更进阶的稳定性策略。