限流算法深度解析:令牌桶、漏桶与滑动窗口计数

深入理解令牌桶、漏桶、滑动窗口等限流算法的原理与实现,掌握 Guava RateLimiter、Sentinel 与自建限流组件的选型与调优

在高并发系统中,限流是保护服务稳定性的核心手段。当请求量超过系统承载能力时,通过拒绝或延迟部分请求,可以防止级联故障,保障核心功能的可用性。理解不同限流算法的原理与适用场景,是设计高可用系统的必备技能。

一、限流的核心目标

目标说明
保护服务防止突发流量压垮后端
资源公平防止单一用户/租户耗尽资源
降级预案非核心请求让路给核心请求
成本可控防止异常调用产生高额账单

二、计数器限流(固定窗口)

2.1 原理

时间窗口:每秒允许 100 个请求

  0s-1s          1s-2s          2s-3s
┌──────┐       ┌──────┐       ┌──────┐
│ ████ │       │ ████ │       │      │
│ 80/100│       │ 100/100│      │ 0/100 │
└──────┘       └──────┘       └──────┘

问题:窗口切换瞬间可能允许 2 倍流量(边界突发)

2.2 Java 实现

public class FixedWindowRateLimiter {
    
    private final long windowSizeMs;
    private final int maxRequests;
    private final AtomicInteger counter = new AtomicInteger(0);
    private volatile long windowStart = System.currentTimeMillis();
    
    public FixedWindowRateLimiter(int maxRequests, long windowSizeMs) {
        this.maxRequests = maxRequests;
        this.windowSizeMs = windowSizeMs;
    }
    
    public synchronized boolean tryAcquire() {
        long now = System.currentTimeMillis();
        
        // 检查是否需要重置窗口
        if (now - windowStart >= windowSizeMs) {
            counter.set(0);
            windowStart = now;
        }
        
        int current = counter.incrementAndGet();
        if (current <= maxRequests) {
            return true;
        }
        
        // 超限,回滚计数
        counter.decrementAndGet();
        return false;
    }
}

// 使用:每秒限制 100 次
FixedWindowRateLimiter limiter = new FixedWindowRateLimiter(100, 1000);

// 在 Filter/Interceptor 中使用
if (!limiter.tryAcquire()) {
    response.setStatus(429);
    return;
}

2.3 Redis 分布式实现

@Service
public class RedisFixedWindowRateLimiter {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    public boolean isAllowed(String key, int maxRequests, int windowSeconds) {
        String redisKey = "ratelimit:" + key;
        long now = System.currentTimeMillis();
        long windowStart = now / (windowSeconds * 1000) * (windowSeconds * 1000);
        String windowKey = redisKey + ":" + windowStart;
        
        Long count = redisTemplate.opsForValue().increment(windowKey);
        if (count == 1) {
            redisTemplate.expire(windowKey, windowSeconds, TimeUnit.SECONDS);
        }
        
        return count <= maxRequests;
    }
}

2.4 优缺点

优点缺点
实现简单临界突发问题(2 倍流量)
内存开销小不平滑,窗口切换瞬时不均匀
不支持突发流量后快速恢复

三、滑动窗口限流

3.1 原理

滑动窗口将时间划分为更小的格子:

  0    200  400  600  800  1000ms
  |----|----|----|----|----|
  ▓▓   ▓    ▓▓   ░    ░     ▓ = 有请求,░ = 无请求
  
当前时间 = 850ms,窗口大小 = 1000ms
统计 50ms~850ms 区间的请求数,与前 200ms、400ms 等格子按时间比例加权

3.2 Java 实现

public class SlidingWindowRateLimiter {
    
    private final int maxRequests;
    private final long windowSizeMs;
    private final int gridNum;           // 窗口细分格子数
    private final long gridSizeMs;
    private final AtomicInteger[] grids; // 每个格子的计数
    private volatile long windowStart;
    
    public SlidingWindowRateLimiter(int maxRequests, long windowSizeMs, int gridNum) {
        this.maxRequests = maxRequests;
        this.windowSizeMs = windowSizeMs;
        this.gridNum = gridNum;
        this.gridSizeMs = windowSizeMs / gridNum;
        this.grids = new AtomicInteger[gridNum];
        for (int i = 0; i < gridNum; i++) {
            grids[i] = new AtomicInteger(0);
        }
        this.windowStart = System.currentTimeMillis();
    }
    
    public synchronized boolean tryAcquire() {
        long now = System.currentTimeMillis();
        long elapsed = now - windowStart;
        
        // 计算需要滑动的格子数
        int shiftGrids = (int) (elapsed / gridSizeMs);
        
        if (shiftGrids >= gridNum) {
            // 整个窗口已过期,全部清零
            for (AtomicInteger grid : grids) {
                grid.set(0);
            }
            windowStart = now;
            shiftGrids = 0;
        } else if (shiftGrids > 0) {
            // 滑动:清零过期的格子
            for (int i = 0; i < shiftGrids; i++) {
                grids[(i + getCurrentGridIndex()) % gridNum].set(0);
            }
            windowStart += shiftGrids * gridSizeMs;
        }
        
        // 统计当前窗口内请求总数
        int total = Arrays.stream(grids).mapToInt(AtomicInteger::get).sum();
        
        if (total < maxRequests) {
            grids[getCurrentGridIndex()].incrementAndGet();
            return true;
        }
        return false;
    }
    
    private int getCurrentGridIndex() {
        return (int) ((System.currentTimeMillis() / gridSizeMs) % gridNum);
    }
}

四、令牌桶算法

4.1 原理

令牌以固定速率放入桶中,请求需要消耗令牌才能通过:

  ┌─────────┐
  │  Token  │  ← 以 rate 速率放入令牌
  │  Bucket │
  │  [●●●]  │  ← 桶容量 capacity
  └────┬────┘
       │ 请求来时取令牌
       ▼
     Service

优点:允许一定突发(桶中有积累),平滑限流

4.2 Guava RateLimiter

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

@Service
public class GuavaRateLimitService {
    
    // 每秒 1000 个许可(平滑突发)
    private final RateLimiter rateLimiter = RateLimiter.create(1000.0);
    
    // 每秒 1000 个许可(平滑预热,启动时逐渐加速)
    private final RateLimiter warmingLimiter = RateLimiter.create(
        1000.0,           // QPS
        5,                // 预热时间(秒)
        TimeUnit.SECONDS  // 时间单位
    );
    
    public void processRequest() {
        // 阻塞获取(直到获得许可)
        rateLimiter.acquire();
        doWork();
    }
    
    public boolean tryProcess() {
        // 非阻塞尝试(立即返回)
        if (rateLimiter.tryAcquire()) {
            doWork();
            return true;
        }
        return false;  // 被限流
    }
    
    public boolean tryProcessWithTimeout() {
        // 带超时的尝试
        if (rateLimiter.tryAcquire(100, TimeUnit.MILLISECONDS)) {
            doWork();
            return true;
        }
        return false;
    }
}

4.3 自建令牌桶

public class TokenBucketRateLimiter {
    
    private final long capacity;        // 桶容量
    private final double ratePerMs;     // 令牌产生速率(每毫秒)
    private final AtomicLong tokens;    // 当前令牌数(放大 1000 倍避免浮点)
    private volatile long lastRefillTime;
    
    public TokenBucketRateLimiter(long capacity, double permitsPerSecond) {
        this.capacity = capacity * 1000;
        this.ratePerMs = permitsPerSecond;
        this.tokens = new AtomicLong(this.capacity);
        this.lastRefillTime = System.currentTimeMillis();
    }
    
    public synchronized boolean tryAcquire(int permits) {
        refill();
        
        long required = permits * 1000L;
        long current = tokens.get();
        
        if (current >= required) {
            tokens.addAndGet(-required);
            return true;
        }
        return false;
    }
    
    private void refill() {
        long now = System.currentTimeMillis();
        long elapsed = now - lastRefillTime;
        
        if (elapsed > 0) {
            long newTokens = (long) (elapsed * ratePerMs * 1000);
            long current = tokens.get();
            long updated = Math.min(capacity, current + newTokens);
            tokens.set(updated);
            lastRefillTime = now;
        }
    }
}

五、漏桶算法

5.1 原理

请求像水一样流入桶,以固定速率流出:

       请求流入(任意速率)
            │
            ▼
      ┌───────────┐
      │    ██     │  ← 桶(有容量限制,满了溢出)
      │    ██     │
      └────┬──────┘
           │ 固定速率流出
           ▼
         处理

特点:强行限制流出速率,绝对平滑,但不灵活

5.2 Java 实现

public class LeakyBucketRateLimiter {
    
    private final long capacity;       // 桶容量
    private final long leakRateMs;     // 漏出速率(每毫秒)
    private final BlockingQueue<Long> bucket;  // 用队列模拟桶
    private volatile long lastLeakTime;
    
    public LeakyBucketRateLimiter(long capacity, long permitsPerSecond) {
        this.capacity = capacity;
        this.leakRateMs = permitsPerSecond / 1000;
        this.bucket = new LinkedBlockingQueue<>((int) capacity);
        this.lastLeakTime = System.currentTimeMillis();
        
        // 启动漏出线程
        startLeaking();
    }
    
    private void startLeaking() {
        Thread leakThread = new Thread(() -> {
            while (!Thread.interrupted()) {
                try {
                    Long request = bucket.poll(leakRateMs, TimeUnit.MILLISECONDS);
                    if (request != null) {
                        processRequest(request);
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }
        });
        leakThread.setDaemon(true);
        leakThread.start();
    }
    
    public boolean tryAcquire() {
        return bucket.offer(System.currentTimeMillis());
    }
    
    private void processRequest(long timestamp) {
        // 处理请求
    }
}

六、算法对比与选型

算法突发处理平滑性实现复杂度内存占用适用场景
固定窗口简单简单计数场景
滑动窗口中等较为精确的限流
令牌桶中等允许突发,平滑限流
漏桶极好中等严格匀速处理

6.1 应用场景

// 场景 1:API 网关限流(令牌桶,允许突发)
RateLimiter apiLimiter = RateLimiter.create(10000);  // 10k QPS

// 场景 2:短信发送(漏桶,严格匀速)
// 每秒最多发 10 条,避免触发运营商限制

// 场景 3:用户级限流(Redis 滑动窗口)
// 每个用户每分钟最多操作 60 次

// 场景 4:系统保护(固定窗口,简单有效)
// 全局每秒最多 50,000 请求

七、Sentinel 中的限流实现

7.1 滑动窗口统计

Sentinel 使用高性能的滑动窗口算法:

// 核心数据结构(LeapArray)
public abstract class LeapArray<T> {
    protected int windowLengthInMs;    // 窗口长度(默认 500ms)
    protected int sampleCount;          // 采样数(默认 2)
    protected int intervalInMs;         // 统计间隔(默认 1000ms)
    protected final AtomicReferenceArray<WindowWrap<T>> array;
    
    // 获取当前窗口(无锁设计,CAS 操作)
    public WindowWrap<T> currentWindow(long timeMillis) {
        // 计算时间戳对应的窗口索引
        int idx = calculateTimeIdx(timeMillis);
        // 计算窗口开始时间
        long windowStart = calculateWindowStart(timeMillis);
        
        while (true) {
            WindowWrap<T> old = array.get(idx);
            if (old == null) {
                // 窗口未初始化,CAS 创建
                WindowWrap<T> window = new WindowWrap<>(windowLengthInMs, windowStart, newEmptyBucket());
                if (array.compareAndSet(idx, null, window)) {
                    return window;
                }
            } else if (windowStart == old.windowStart()) {
                // 窗口命中
                return old;
            } else if (windowStart > old.windowStart()) {
                // 窗口已过期,重置
                if (updateLock.tryLock()) {
                    try {
                        return resetWindowTo(old, windowStart);
                    } finally {
                        updateLock.unlock();
                    }
                }
            }
            // else: 时钟回拨,等待
        }
    }
}

7.2 热点参数限流

@GetMapping("/api/goods/{goodsId}")
@SentinelResource(value = "getGoods", blockHandler = "blockHandler")
public Goods getGoods(@PathVariable Long goodsId) {
    return goodsService.getById(goodsId);
}

// 规则配置:对 goodsId 维度限流
ParamFlowRule rule = new ParamFlowRule("getGoods")
    .setParamIdx(0)                          // 第 0 个参数
    .setGrade(RuleConstant.FLOW_GRADE_QPS)
    .setCount(100)                           // 每个 goodsId 100 QPS
    .setDurationInSec(1);                    // 统计周期 1 秒

八、分布式限流

8.1 Redis + Lua 实现

-- redis-ratelimiter.lua
local key = KEYS[1]
local window = tonumber(ARGV[1])
local threshold = tonumber(ARGV[2])
local now = tonumber(ARGV[3])

-- 清理过期数据
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)

-- 统计当前窗口请求数
local count = redis.call('ZCARD', key)

if count < threshold then
    -- 允许,记录请求
    redis.call('ZADD', key, now, now .. ':' .. redis.call('INCR', key .. ':seq'))
    redis.call('EXPIRE', key, math.ceil(window / 1000))
    return 1
else
    return 0
end
@Service
public class DistributedRateLimiter {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    private static final String LUA_SCRIPT = 
        "local key = KEYS[1] " +
        "local window = tonumber(ARGV[1]) " +
        "local threshold = tonumber(ARGV[2]) " +
        "local now = tonumber(ARGV[3]) " +
        "redis.call('ZREMRANGEBYSCORE', key, 0, now - window) " +
        "local count = redis.call('ZCARD', key) " +
        "if count < threshold then " +
        "  redis.call('ZADD', key, now, now) " +
        "  redis.call('EXPIRE', key, math.ceil(window / 1000)) " +
        "  return 1 " +
        "else " +
        "  return 0 " +
        "end";
    
    private final RedisScript<Long> script = new DefaultRedisScript<>(LUA_SCRIPT, Long.class);
    
    public boolean isAllowed(String key, int maxRequests, long windowMs) {
        Long result = redisTemplate.execute(
            script,
            Collections.singletonList("ratelimit:" + key),
            String.valueOf(windowMs),
            String.valueOf(maxRequests),
            String.valueOf(System.currentTimeMillis())
        );
        return result != null && result == 1;
    }
}

8.2 Redisson 限流器

@Service
public class RedissonRateLimitService {
    
    @Autowired
    private RedissonClient redisson;
    
    public boolean tryAcquire(String key, long rate, long rateInterval, RateIntervalUnit unit) {
        RRateLimiter limiter = redisson.getRateLimiter("rl:" + key);
        // 初始化限流器(首次调用)
        limiter.trySetRate(RateType.OVERALL, rate, rateInterval, unit);
        return limiter.tryAcquire();
    }
}

// 使用
boolean allowed = redissonRateLimitService.tryAcquire(
    "user:" + userId,    // key
    10,                   // 10 个许可
    1,                    // 时间间隔
    RateIntervalUnit.MINUTES  // 每分钟
);

九、最佳实践

9.1 分层限流策略

流量入口层级:
    
    第一层:Nginx / CDN
    └── 基于 IP / User-Agent 的粗粒度限流
    
    第二层:API Gateway(Spring Cloud Gateway + Sentinel)
    └── 路由级、API 分组级限流
    
    第三层:服务层(Sentinel / Guava)
    └── 接口级、用户级、热点参数级限流
    
    第四层:数据库连接池 / 线程池
    └── 资源级硬限制

9.2 限流响应设计

// 统一的限流响应
public class RateLimitResponse {
    public static final ErrorResponse TOO_MANY_REQUESTS = new ErrorResponse(
        429,
        "Too Many Requests",
        "请求过于频繁,请稍后重试",
        Map.of(
            "Retry-After", "60",           // 建议客户端等待秒数
            "X-RateLimit-Limit", "100",     // 限制次数
            "X-RateLimit-Remaining", "0",   // 剩余次数
            "X-RateLimit-Reset", "1234567890"  // 重置时间戳
        )
    );
}

9.3 监控与告警

@Component
public class RateLimitMetrics {
    
    private final MeterRegistry meterRegistry;
    private final Counter blockedCounter;
    private final Counter allowedCounter;
    
    public RateLimitMetrics(MeterRegistry registry) {
        this.meterRegistry = registry;
        this.blockedCounter = Counter.builder("ratelimit.blocked")
            .description("被限流的请求数")
            .register(registry);
        this.allowedCounter = Counter.builder("ratelimit.allowed")
            .description("通过的请求数")
            .register(registry);
    }
    
    public void record(boolean allowed) {
        if (allowed) {
            allowedCounter.increment();
        } else {
            blockedCounter.increment();
        }
    }
}

十、总结

算法核心思想选择建议
固定窗口计数 + 定时重置简单场景,容忍临界突发
滑动窗口细分格子滑动统计需要较精确限流
令牌桶定速生产 + 消耗允许突发,平滑输出
漏桶定速消费 + 缓存严格限速,强制匀速

限流是高并发系统的"保险丝"。在实际工程中,通常采用多层限流策略:网关层做粗粒度防护,服务层做精细控制,配合监控告警和降级预案,构建完整的流量治理能力。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java-enterprise」更多文章

  1. Java 代码质量:SonarQube、Checkstyle 与 SpotBugs 工程化实践
  2. Spring IoC 容器与依赖注入原理深度剖析
  3. 分布式文件存储:MinIO、阿里云 OSS 与 Spring 集成实战