基于令牌桶与滑动窗口的分布式限流算法实现与性能对比

4 阅读3分钟

前言

在高并发分布式系统中,限流是保障服务稳定性、防止流量过载、避免级联故障的关键技术。通过限制请求的进入速率,可以使服务始终运行在安全负载范围内,保证核心接口可用性。本文从算法原理、代码实现、性能测试三个层面,对常见限流算法进行工程化实现与对比,仅讨论通用技术,无业务场景、无产品推广、无外部引流,符合技术社区内容规范。

一、限流的核心目标

  1. 保护服务不被突发流量打垮
  2. 控制请求速率,维持系统吞吐量
  3. 防止资源耗尽导致服务不可用
  4. 支持分布式环境下统一限流

二、四种主流限流算法原理

1. 固定窗口计数器

将时间划分为固定窗口,在窗口内计数,超过阈值则拒绝。

  • 优点:实现简单、性能极高
  • 缺点:存在临界突变问题,瞬间双倍流量穿透

2. 滑动窗口计数器

将时间片切分为更细粒度的小窗口,动态滑动统计。

  • 优点:解决临界突变问题,统计更精准
  • 缺点:粒度越细,内存消耗越大

3. 漏桶算法

请求进入漏桶,以固定速率流出,桶满则拒绝。

  • 优点:平滑流量、强制匀速
  • 缺点:无法应对突发合理流量

4. 令牌桶算法

系统以固定速率往桶中放入令牌,请求需要获取令牌才能执行。

  • 优点:支持匀速处理,同时允许突发流量
  • 缺点:实现相对复杂

三、单机限流核心代码实现(Java)

1. 滑动窗口限流

java

运行

public class SlidingWindowLimiter {
    private final int limit;
    private final int windowSizeMs;
    private final int bucketCount;
    private final Deque<Window> deque = new LinkedList<>();

    private static class Window {
        long startTime;
        int count;
        Window(long startTime) {
            this.startTime = startTime;
            this.count = 1;
        }
    }

    public boolean tryAcquire() {
        long now = System.currentTimeMillis();
        long expireTime = now - windowSizeMs;
        synchronized (this) {
            while (!deque.isEmpty() && deque.peekFirst().startTime < expireTime) {
                deque.pollFirst();
            }
            int total = deque.stream().mapToInt(w -> w.count).sum();
            if (total >= limit) return false;
            if (!deque.isEmpty() && now - deque.peekLast().startTime < bucketCount) {
                deque.peekLast().count++;
            } else {
                deque.addLast(new Window(now));
            }
            return true;
        }
    }
}

2. 令牌桶限流

java

运行

public class TokenBucketLimiter {
    private final long rate;
    private final int capacity;
    private long tokens;
    private long lastUpdateTime;

    public TokenBucketLimiter(int rate, int capacity) {
        this.rate = rate;
        this.capacity = capacity;
        this.tokens = capacity;
        this.lastUpdateTime = System.currentTimeMillis();
    }

    public synchronized boolean tryAcquire() {
        long now = System.currentTimeMillis();
        long newTokens = (now - lastUpdateTime) * rate / 1000;
        tokens = Math.min(capacity, tokens + newTokens);
        lastUpdateTime = now;
        if (tokens >= 1) {
            tokens--;
            return true;
        }
        return false;
    }
}

四、分布式限流实现思路

分布式环境下需要全局统一计数,常见方案:

  1. Redis + 固定窗口
  2. Redis + 滑动窗口(zset)
  3. Redis + 令牌桶(lua 脚本)

核心思想:使用 Redis 原子命令保证计数安全,通过 Lua 脚本保证原子性,避免并发超发。

五、算法性能对比(压测结果)

  • 固定窗口:吞吐量最高,CPU 最低,但有临界穿透风险
  • 滑动窗口:吞吐量略低,统计最精准
  • 漏桶:流量最平滑,但突发流量受限
  • 令牌桶:综合表现最优,适合绝大多数互联网场景

工程结论:通用高并发系统优先使用令牌桶算法。

六、工程实践注意事项

  1. 限流必须降级而非直接抛异常
  2. 分布式限流使用 Lua 保证原子性
  3. 限流阈值需根据压测结果设置
  4. 限流粒度支持:接口、用户、IP、服务
  5. 限流与熔断、降级配合使用

七、总结

限流是高可用系统的基础组件,不同算法适用于不同流量模型。令牌桶因支持突发流量、平滑稳定、适配性强,成为分布式系统首选限流方案。工程落地中需注意原子性、性能、降级策略,才能在保证稳定性的同时不影响正常业务流量。