🚀 为什么需要限流?

在微服务、高并发或网关等场景下,如果不加控制地接收请求:

  • 数据库、缓存或中间件可能被压垮;
  • 导致服务不可用或响应变慢;
  • 引发雪崩效应,影响整个系统。

因此,限流成为「高可用架构」中不可或缺的一环。

常见限流场景

  • 秒杀系统:控制并发下单量,防止库存超卖。
  • API 接口:保护后端服务不被恶意刷流。
  • 爬虫防护:限制访问频率,防止恶意抓取。
  • 服务降级:当系统达到阈值时,主动限流保护自己。

🔄 限流核心算法概览

限流算法原理控制维度应用场景
计数器法固定窗口内统计请求数时间 + 次数简单接口
滑动窗口更精确地统计请求数时间滑动窗口秒级限流
漏桶算法请求以固定速率流出平滑流量确保系统稳定性
令牌桶算法请求必须拿到令牌才能处理控制速率 + 支持突发推荐使用

💧 漏桶算法(Leaky Bucket)

1. 算法原理

  • 漏桶类似一个固定容量的桶,水(请求)可随时进入,但出水速率恒定。
  • 如果请求量大于处理速率,多余的请求会被丢弃或排队。
  • 达到限流效果的本质是:以固定速率处理请求,消除突发流量对系统的冲击。

2. 应用特点

  • 控制出水速率,达到平滑请求流量的目的。
  • 如果流入速率持续大于出水速率,会造成请求被抛弃。
  • 适合对实时性要求不高、需要稳定输出的系统。

3. Java 实现代码

public class LeakyBucketLimiter {

    private final int capacity; // 漏桶容量
    private final int rate;     // 每秒处理速率
    private int water;          // 当前水量
    private long lastTime;

    public LeakyBucketLimiter(int capacity, int rate) {
        this.capacity = capacity;
        this.rate = rate;
        this.water = 0;
        this.lastTime = System.currentTimeMillis();
    }

    public synchronized boolean tryAcquire() {
        long now = System.currentTimeMillis();
        long timePassed = now - lastTime;
        int leaked = (int) ((timePassed / 1000.0) * rate);

        water = Math.max(0, water - leaked);
        lastTime = now;

        if (water < capacity) {
            water++;
            return true;
        } else {
            return false; // 拒绝请求
        }
    }
}

🪙 令牌桶算法(Token Bucket)

1. 算法原理

  • 系统以固定速率往桶中放入令牌(如每秒10个),每个请求需获取1个令牌才能被处理。
  • 如果桶满了,多余令牌会被丢弃;如果请求来时桶为空,则被拒绝或等待。
  • 支持突发流量——如果系统一段时间未被调用,桶中令牌将积累,可以应对短时间突发流量。

2. 应用特点

  • 能平衡系统整体负载,且容忍短时高峰。
  • 可通过调整速率和容量灵活应对不同业务场景。
  • 适用于接口限流、用户行为控制、广告请求频控等。

3. Java 实现代码

public class TokenBucketLimiter {

    private final int capacity;         
    private final int tokensPerSecond;  
    private double currentTokens;
    private long lastRefillTime;

    public TokenBucketLimiter(int capacity, int tokensPerSecond) {
        this.capacity = capacity;
        this.tokensPerSecond = tokensPerSecond;
        this.currentTokens = capacity;
        this.lastRefillTime = System.nanoTime();
    }

    public synchronized boolean tryAcquire() {
        refill();
        if (currentTokens >= 1) {
            currentTokens -= 1;
            return true;
        }
        return false;
    }

    private void refill() {
        long now = System.nanoTime();
        double seconds = (now - lastRefillTime) / 1_000_000_000.0;
        double newTokens = seconds * tokensPerSecond;
        currentTokens = Math.min(capacity, currentTokens + newTokens);
        lastRefillTime = now;
    }
}

🧮 计数器法(Fixed Window Counter)

1. 算法原理

  • 将时间划分为固定时间窗口(如1秒),统计该时间段内请求次数。
  • 若请求次数超过阈值,则拒绝本窗口后续请求。

2. 存在问题

  • 窗口边界突变时,可能造成瞬时流量激增(突刺现象)。
    如:999ms内100个请求,1001ms又来100个请求 —— 实际短时间接收200请求。

3. Java 实现

import java.util.concurrent.atomic.AtomicInteger;

public class FixedWindowLimiter {

    private final int limit;          // 每个窗口允许的请求数
    private final long windowSize;    // 窗口大小(毫秒)
    private long windowStart;         // 当前窗口起始时间
    private AtomicInteger counter;    // 请求计数器

    public FixedWindowLimiter(int limit, long windowSize) {
        this.limit = limit;
        this.windowSize = windowSize;
        this.windowStart = System.currentTimeMillis();
        this.counter = new AtomicInteger(0);
    }

    public synchronized boolean tryAcquire() {
        long now = System.currentTimeMillis();
        if (now - windowStart >= windowSize) {
            windowStart = now;
            counter.set(0);
        }
        if (counter.incrementAndGet() <= limit) {
            return true;
        }
        return false;
    }
}

⏳ 滑动窗口算法(Sliding Window Log)

1. 算法原理

  • 使用时间戳记录每次请求,按时间滚动删除过期记录。
  • 在任意时刻统计最近一段时间内的请求次数,判断是否限流。

2. 优点

  • 精度高,无突刺问题。
  • 但记录每次请求时间,内存消耗较大,适合请求频率较低场景。

3. Java 实现

import java.util.LinkedList;
import java.util.Queue;

public class SlidingWindowLogLimiter {

    private final int maxRequests;
    private final long intervalMillis;
    private final Queue<Long> timestamps = new LinkedList<>();

    public SlidingWindowLogLimiter(int maxRequests, long intervalMillis) {
        this.maxRequests = maxRequests;
        this.intervalMillis = intervalMillis;
    }

    public synchronized boolean tryAcquire() {
        long now = System.currentTimeMillis();
        while (!timestamps.isEmpty() && now - timestamps.peek() > intervalMillis) {
            timestamps.poll(); // 移除过期请求
        }
        if (timestamps.size() < maxRequests) {
            timestamps.offer(now);
            return true;
        }
        return false;
    }
}

🧩 限流算法选型建议

算法精度内存突发支持实现复杂度典型应用
计数器法★★☆☆☆★☆☆☆☆否简单基础接口限流
滑动窗口★★★★☆★★★☆☆否中等高精度限流
漏桶算法★★★★☆★☆☆☆☆否简单消息队列控制
令牌桶算法★★★★★★☆☆☆☆是较复杂接口、微服务

✅ 实战建议:令牌桶支持突发请求,更加灵活,适合大多数 API 限流场景。


🔐 进阶增强建议

异常策略建议

  • 返回 429 Too Many Requests HTTP 状态码
  • 返回 JSON 提示信息 { code: 429, message: "请求频繁,请稍后重试" }
  • 加入重试机制、回退策略

分布式限流方案

  • Redis + Lua 脚本实现全局令牌桶
  • 使用开源组件如:Sentinel、Bucket4j、Resilience4j

应用层封装建议

  • 使用注解方式配置限流参数
  • 利用 AOP 拦截进行限流处理

🎯 总结

  • 漏桶适合稳定处理流量,确保后端不会被压垮。
  • 令牌桶适合突发流量,控制速率的同时提升用户体验。
  • 高并发系统中合理选型限流策略,是保障服务稳定性的重要手段。

如果你觉得这篇文章有帮助,欢迎点赞、收藏、评论支持!

更多推荐