在分布式系统和高并发应用中,限流(Rate Limiting)是一项关键技术,用于防止过多的请求淹没服务器,从而保证服务的稳定性与性能。本文介绍几种常见的限流算法,包括固定窗口限流、滑动窗口限流、令牌桶算法、漏桶算法。

固定窗口限流

原理

固定窗口限流在一个固定的时间窗口内(如1秒、1分钟),限制请求数量。例如,一个用户在1分钟内最多只能发送100个请求。如果请求数超过阈值,则拒绝请求。时间窗口结束后,计数器重置。

优点

  • 实现简单:使用简单的计数器即可实现。

  • 低资源占用:不需要大量额外的计算和存储资源。

缺点

  • 临界问题:在时间窗口的边界可能出现大量突发请求。例如,一个用户在窗口结束前发出多个请求,并在新窗口开始时再次发出多个请求,导致短时间内服务器受到过多请求的压力。

代码示例

public class RateLimiterDemo
{
    private readonly int _maxRequests;
    private readonly TimeSpan _windowTime;
    private int _requestCount;
    private DateTime _windowStart;

    public RateLimiterDemo(int maxRequests, TimeSpan windowTime)
    {
        _maxRequests = maxRequests;
        _windowTime = windowTime;
        _windowStart = DateTime.UtcNow;
        _requestCount = 0;
    }

    public bool IsRequestAllowed()
    {
        var now = DateTime.UtcNow;

        if (now - _windowStart > _windowTime)
        {
            _windowStart = now;
            _requestCount = 0;
        }

        if (_requestCount < _maxRequests)
        {
            _requestCount++;
            return true;
        }

        return false;
    }
}

滑动窗口限流

原理

滑动窗口限流通过将时间窗口细分为多个小窗口,并对每个小窗口的请求进行统计来平滑流量。它在每个时间点都会计算过去固定时间内的请求总数,避免固定窗口的临界问题。

优点

  • 解决临界问题:滑动窗口避免了固定窗口临界点的突发流量问题。

  • 限流更加平滑:更加精准地控制请求频率。

缺点

  • 实现复杂:需要存储每个小窗口的请求计数,并动态更新窗口。

  • 较高的资源开销:需要更多的计算和存储空间。

代码示例

public class RateLimiterDemo
{
    private readonly int _maxRequests;
    private readonly TimeSpan _windowTime;
    private readonly LinkedList<DateTime> _requestTimestamps = new LinkedList<DateTime>();

    public RateLimiterDemo(int maxRequests, TimeSpan windowTime)
    {
        _maxRequests = maxRequests;
        _windowTime = windowTime;
    }

    public bool IsRequestAllowed()
    {
        var now = DateTime.UtcNow;
        var windowStart = now - _windowTime;

        while (_requestTimestamps.Count > 0 && _requestTimestamps.First.Value < windowStart)
        {
            _requestTimestamps.RemoveFirst();
        }

        if (_requestTimestamps.Count < _maxRequests)
        {
            _requestTimestamps.AddLast(now);
            return true;
        }

        return false;
    }
}

令牌桶算法

原理

令牌桶算法通过生成和消耗"令牌"来控制请求速率。系统以固定速率生成令牌并放入桶中,请求到达时从桶中取走一个令牌,如果桶中没有令牌,则拒绝请求。未使用的令牌可以累积,因此允许一定的突发流量。

优点

  • 支持突发流量:未使用的令牌可以累积,从而允许短期的流量高峰。

  • 灵活性高:可以通过调整令牌生成速率和桶的大小来调整限流策略。

缺点

  • 精确控制较弱:突发流量过大时仍可能导致系统压力增加。

代码示例

public class RateLimiterDemo
{
    private readonly int _capacity;
    private readonly int _tokensPerSecond;
    private int _tokens;
    private DateTime _lastRefillTime;

    public RateLimiterDemo(int capacity, int tokensPerSecond)
    {
        _capacity = capacity;
        _tokensPerSecond = tokensPerSecond;
        _tokens = capacity;
        _lastRefillTime = DateTime.UtcNow;
    }

    public bool IsRequestAllowed()
    {
        var now = DateTime.UtcNow;
        var timeElapsed = (now - _lastRefillTime).TotalSeconds;

        // Refill tokens
        var tokensToAdd = (int)(timeElapsed * _tokensPerSecond);
        _tokens = Math.Min(_capacity, _tokens + tokensToAdd);
        _lastRefillTime = now;

        if (_tokens > 0)
        {
            _tokens--;
            return true;
        }

        return false;
    }
}

漏桶算法

原理

漏桶算法将请求流量控制为一个固定速率。它模拟了一个装水的桶,水(请求)可以以任意速率流入桶中,但只能以固定速率流出(处理请求)。如果桶满了,超出的请求将被丢弃。该算法强制将请求处理速率保持稳定。

优点

  • 严格控制流量:漏桶算法能严格限制请求的处理速率,流量平滑。

  • 适用于带宽控制:特别适用于需要严格限速的场景。

缺点

  • 不支持突发流量:多余的请求直接丢弃,无法处理突发流量。

代码示例

public class RateLimiterDemo
{
    private readonly int _capacity;
    private readonly int _dripRate; // Requests per second
    private int _currentLoad;
    private DateTime _lastLeakTime;

    public RateLimiterDemo(int capacity, int dripRate)
    {
        _capacity = capacity;
        _dripRate = dripRate;
        _currentLoad = 0;
        _lastLeakTime = DateTime.UtcNow;
    }

    public bool IsRequestAllowed()
    {
        var now = DateTime.UtcNow;
        var timeElapsed = (now - _lastLeakTime).TotalSeconds;

        // Leak the bucket
        var leaks = (int)(timeElapsed * _dripRate);
        _currentLoad = Math.Max(0, _currentLoad - leaks);
        _lastLeakTime = now;

        if (_currentLoad < _capacity)
        {
            _currentLoad++;
            return true;
        }

        return false;
    }
}

总结

算法

优点

缺点

适用场景

固定窗口限流

实现简单,计算资源开销小

存在临界问题,无法很好地控制短期内的突发流量

适合简单的API限流场景

滑动窗口限流

解决临界问题,限流平滑

实现复杂,存储和计算开销较大

高并发场景、需要精确控制的限流场景

令牌桶算法

支持突发流量,灵活调整

短期突发请求过大时可能无法很好地控制流量

实时通信、支付系统、API限流等场景

漏桶算法

流量控制严格,平稳处理

不支持突发流量,突发请求可能被丢弃

带宽控制、流量整形等严格限速的场景