源码分布式限流实现逻辑?

访客 源码剖析 1

本文目录导读:

  1. 目录导读
  2. 为什么需要分布式限流?
  3. 分布式限流的三种核心算法
  4. 源码级解析:基于Redis+Lua的令牌桶实现
  5. 分布式限流常见陷阱与优化方案
  6. FAQ问答

分布式限流实现逻辑与实战指南

目录导读

  1. 为什么需要分布式限流?——从单机到集群的痛点
  2. 分布式限流的三种核心算法(令牌桶、漏桶、滑动窗口)
  3. 源码级解析:基于Redis+Lua的令牌桶实现
  4. 分布式限流常见陷阱与优化方案
  5. FAQ问答:高频面试与实战避坑

为什么需要分布式限流?

在微服务架构中,单机限流(如Guava RateLimiter)无法应对集群流量,假设你有3个节点,每个节点允许100QPS,但全局流量可能瞬间突破300QPS,导致下游服务雪崩。分布式限流的核心目标是在多个服务实例间维护一个全局计数器或令牌池,确保整体流量不超过阈值。

核心挑战

  • 原子性:多实例并发读写需要保证计数准确
  • 性能:不能因限流增加明显延迟
  • 一致性:极端情况下允许少量偏差(最终一致性)

分布式限流的三种核心算法

1 固定窗口(计数器)

原理:以1s为窗口,统计请求数,超过阈值则拒绝。
问题:窗口切换时存在“突刺”——假设限制100 QPS,在0.9s-1.1s内可能出现200个请求。

2 滑动窗口

原理:将窗口细分为更小的时间片(如1s分为10个100ms片),滑动统计。
优点:平滑突刺
缺点:需要存储多个时间片的计数,内存占用稍高

3 令牌桶(推荐)

原理:以固定速率生成令牌,请求需获取令牌才能处理。
特点:允许突发流量(只要令牌桶有存量),适合大多数业务场景。
核心参数rate(生成速率)、capacity(桶容量)


源码级解析:基于Redis+Lua的令牌桶实现

为什么选择Redis+Lua?

Redis单线程保证原子性;Lua脚本避免网络往返,一次原子执行。

核心Lua脚本(伪代码+注释)

-- KEYS[1] = 令牌桶key, ARGV[1] = 当前时间戳
-- ARGV[2] = 每秒生成速率, ARGV[3] = 桶容量
local token_key = KEYS[1]  
local now = tonumber(ARGV[1])  
local rate = tonumber(ARGV[2])  
local capacity = tonumber(ARGV[3])  
-- 获取上次补充时间
local last_refresh = redis.call('GET', token_key .. ':time')
if not last_refresh then  
    last_refresh = 0  
end  
-- 计算距离上次补充的时间差
local delta = math.max(0, now - tonumber(last_refresh))  
-- 补充令牌数
local current_tokens = tonumber(redis.call('GET', token_key) or 0)  
local new_tokens = math.min(capacity, current_tokens + delta * rate)  
-- 判断是否允许通过
if new_tokens < 1 then  
    return 0  -- 拒绝
else  
    -- 更新令牌数和时间
    redis.call('SET', token_key, new_tokens - 1)  
    redis.call('SET', token_key .. ':time', now)  
    return 1  -- 允许
end

执行流程

  1. 每次请求调用脚本
  2. 计算自上次取令牌后“应该生成多少令牌”
  3. 若剩余令牌≥1,则消耗一个令牌返回成功;否则拒绝

Java客户端调用示例(Spring Boot集成Redis)

public boolean tryAcquire(String key, int rate, int capacity) {
    List<String> keys = Arrays.asList(key);
    List<String> args = Arrays.asList(
        String.valueOf(System.currentTimeMillis()),
        String.valueOf(rate),
        String.valueOf(capacity)
    );
    Long result = redisTemplate.execute(redisScript, keys, args);
    return result == 1L;
}

性能说明:单次Lua调用约0.5-1ms(局域网Redis),支持千QPS以上。


分布式限流常见陷阱与优化方案

陷阱1:时间同步问题

现象:若各服务实例系统时间不一致,导致令牌生成速率偏差。
方案:改用Redis的TIME命令获取统一时间,或使用NTP同步。

陷阱2:高并发下Lua脚本阻塞

场景:限流逻辑与业务逻辑共用一个Redis连接,限流慢导致业务堆积。
方案:使用独立线程池 + 独立Redis连接(或Lettuce异步方案)。

陷阱3:突发流量保护不足

改进:结合“预热模式”(如令牌桶启动时不满桶),避免突发流量瞬间消耗完所有令牌。

陷阱4:内存占用过高

场景:为每个用户、每个API都创建独立令牌桶。
方案:采用“分桶+共享池”策略,例如根据路由前缀分享令牌池。

优化技巧:

  • 本地缓存+异步同步:本地缓存令牌,定期批量同步Redis,降低Redis压力(牺牲一定精确性)
  • 降级熔断:当Redis不可用时,自动切换为单机限流

FAQ问答

Q1:分布式限流和熔断有什么区别?
A:限流主动控制流入流量(如每秒100请求);熔断是被动保护(如超时比例达50%后拒绝所有请求),两者常配合使用(如Sentinel的FlowRule + DegradeRule)。

Q2:为什么不用ZooKeeper实现限流?
A:ZK是CP系统(强一致),写性能低(约几百QPS),不适合高并发读写,Redis多为AP系统(最终一致),单机10W+ QPS,更适合限流场景。

Q3:令牌桶容量设置多大合适?
A:一般设为rate * 1-5,例如rate=100 QPS,桶容量设100-500,既能吸收突发,又不会过度堆积。

Q4:多级限流如何实现?
A:常见为“网关层限流(全局)+ 服务层限流(细粒度)”,例如Nginx限流1000 QPS,同时服务内部对各API限流100 QPS。

Q5:如果Redis挂了怎么办?
A:建议降级策略:

  1. 单机Guava RateLimiter(牺牲全局性)
  2. 异步上报失败统计,用于事后调整
  3. 用etcd或本地共享内存作为降级方案

延伸思考:更先进的分布式限流方案如Sentinel内置滑动窗口(基于内存+网络传输)、云原生网关Kong的限流插件(支持集群模式),实际落地时,建议先压测Redis性能,再结合业务容忍度选择算法。

标签: 分布式限流

抱歉,评论功能暂时关闭!