常用的限流算法

限流算法用于限制系统的请求流量,防止系统因超载而崩溃。它在高并发场景下广泛应用于微服务、API 网关、缓存等场景。以下是常见的几种限流算法及其详解:

1. 计数器法(Fixed Window)

原理

计数器法是最简单的限流算法。它基于一个固定时间窗口,在窗口内统计请求的数量。如果请求次数超过预设的上限,后续请求将在窗口结束前被拒绝。到达下一个时间窗口时,计数器会重置为 0,重新开始统计。

流程
  1. 在系统中为某个请求路径设置一个计数器和时间窗口(如 1 分钟)。
  2. 每次请求到达时,检查当前时间窗口内的请求数是否已达到限制。
  3. 如果未达到限制,计数器增加 1,请求继续处理。
  4. 如果已达到限制,拒绝请求并返回错误。
  5. 时间窗口结束后,重置计数器为 0。
优点
  • 实现简单,适合不需要复杂限流的场景。
  • 对于稳定且均匀的请求流量效果较好。
缺点
  • 存在“突发效应”,即时间窗口的边界处可能会出现流量激增的现象。例如,在一个窗口快结束时请求突然增多,下一个窗口又可以立即接受新请求。
场景
  • 适用于请求流量较为均匀且系统可承受突发流量的简单场景,如某些 API 请求限流。
示例

假设设定一个时间窗口为 1 分钟,允许每分钟最多处理 100 个请求。如果在 1 分钟内收到 101 个请求,第 101 个请求将被拒绝或等待到下一个时间窗口。

java代码实现:
import java.util.concurrent.atomic.AtomicInteger;

public class FixedWindowRateLimiter {
    private final int limit;
    private final long windowSize;
    private long windowStart;
    private final AtomicInteger requestCount;

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

    public synchronized boolean allowRequest() {
        long currentTime = System.currentTimeMillis();
        if (currentTime - windowStart > windowSize) {
            windowStart = currentTime;
            requestCount.set(0);  // Reset count
        }
        if (requestCount.get() < limit) {
            requestCount.incrementAndGet();  // Increment count
            return true;  // Allow request
        }
        return false;  // Reject request
    }
}
go代码实现:
package main

import (
	"sync"
	"time"
)

type FixedWindowRateLimiter struct {
	limit       int
	windowSize  time.Duration
	windowStart time.Time
	requestCount int
	mu          sync.Mutex
}

func NewFixedWindowRateLimiter(limit int, windowSize time.Duration) *FixedWindowRateLimiter {
	return &FixedWindowRateLimiter{
		limit:      limit,
		windowSize: windowSize,
		windowStart: time.Now(),
	}
}

func (rl *FixedWindowRateLimiter) AllowRequest() bool {
	rl.mu.Lock()
	defer rl.mu.Unlock()
	if time.Since(rl.windowStart) > rl.windowSize {
		rl.windowStart = time.Now()
		rl.requestCount = 0 // Reset count
	}
	if rl.requestCount < rl.limit {
		rl.requestCount++ // Increment count
		return true // Allow request
	}
	return false // Reject request
}

2. 滑动窗口法(Sliding Window)

原理

滑动窗口法通过不断移动时间窗口来限制请求,避免了固定窗口带来的突发效应。它将一个大时间窗口分成多个小的时间片,并实时计算多个时间片内的请求总和。

流程
  1. 将整个时间窗口划分为多个较小的时间段(如 1 分钟划分为 6 个 10 秒)。
  2. 每个时间段内记录请求数。
  3. 每次有新请求到达时,计算当前时间段和前几个时间段内的总请求数。
  4. 如果总请求数未达到限制,请求通过;否则请求被拒绝。
  5. 随着时间推移,旧的时间段不断被丢弃,新的时间段被创建,窗口在“滑动”。
优点
  • 能更精确地限制流量,避免了固定窗口中的突发流量问题。
  • 对突发流量响应更平滑,不会出现固定窗口的边界问题。
缺点
  • 实现复杂度较高,计算开销相对较大。
  • 需要维护更多的数据结构来存储各个时间段的请求数。
场景
  • 适合需要精准限流的场景,尤其是在突发流量较大的环境中,例如支付系统或电商抢购等。
示例

在过去 1 分钟内最多允许 100 个请求,将 1 分钟划分为 6 个 10 秒的小窗口。如果在过去的 6 个窗口内总请求数未超过 100 个,新的请求将通过,否则拒绝。

java代码实现:
import java.util.LinkedList;
import java.util.Queue;

public class SlidingWindowRateLimiter {
    private final int limit;
    private final long windowSize;
    private final Queue<Long> requestTimes;

    public SlidingWindowRateLimiter(int limit, long windowSize) {
        this.limit = limit;
        this.windowSize = windowSize;
        this.requestTimes = new LinkedList<>();
    }

    public synchronized boolean allowRequest() {
        long currentTime = System.currentTimeMillis();
        while (!requestTimes.isEmpty() && currentTime - requestTimes.peek() > windowSize) {
            requestTimes.poll();  // Remove expired requests
        }
        if (requestTimes.size() < limit) {
            requestTimes.offer(currentTime);  // Add new request
            return true;  // Allow request
        }
        return false;  // Reject request
    }
}

go代码实现:
package main

import (
	"container/list"
	"sync"
	"time"
)

type SlidingWindowRateLimiter struct {
	limit       int
	windowSize  time.Duration
	requestTimes *list.List
	mu          sync.Mutex
}

func NewSlidingWindowRateLimiter(limit int, windowSize time.Duration) *SlidingWindowRateLimiter {
	return &SlidingWindowRateLimiter{
		limit:       limit,
		windowSize:  windowSize,
		requestTimes: list.New(),
	}
}

func (rl *SlidingWindowRateLimiter) AllowRequest() bool {
	rl.mu.Lock()
	defer rl.mu.Unlock()
	currentTime := time.Now()
	for rl.requestTimes.Len() > 0 && currentTime.Sub(rl.requestTimes.Front().Value.(time.Time)) > rl.windowSize {
		rl.requestTimes.Remove(rl.requestTimes.Front()) // Remove expired requests
	}
	if rl.requestTimes.Len() < rl.limit {
		rl.requestTimes.PushBack(currentTime) // Add new request
		return true // Allow request
	}
	return false // Reject request
}


3. 漏桶算法(Leaky Bucket)

原理

漏桶算法将请求流量视为向桶中注水,桶以固定速率“漏水”(处理请求)。如果水(请求)进入的速度超过漏水的速度,桶会溢出,多余的水(请求)将被丢弃。因此,它通过固定的处理速率来平滑流量。

流程
  1. 当有请求到达时,将请求放入漏桶中。
  2. 桶中的请求以固定速率被处理(类似于恒定的漏水速率)。
  3. 如果桶满了,新请求会被丢弃。
  4. 桶永远保持一个固定大小,无法无限制容纳请求。
优点
  • 可以有效地平滑突发流量,保持系统处理请求的稳定性。
  • 保证系统不会被突发流量压垮,能够防止超载。
缺点
  • 对突发流量的处理能力较差,如果请求到达速率远超系统的处理速率,超出部分会直接丢弃。
场景
  • 适用于处理必须保持恒定速率的请求,比如银行系统中的转账或交易请求。
示例

假设漏桶的容量为 10,处理速率为每秒 2 个请求。如果每秒收到 5 个请求,多余的请求将被丢弃,只能处理 2 个请求。

java代码实现:
import java.util.concurrent.TimeUnit;

public class LeakyBucketRateLimiter {
    private final int capacity;
    private final int leakRate;
    private int currentWater;
    private long lastLeakTime;

    public LeakyBucketRateLimiter(int capacity, int leakRate) {
        this.capacity = capacity;
        this.leakRate = leakRate;
        this.currentWater = 0;
        this.lastLeakTime = System.currentTimeMillis();
    }

    public synchronized boolean allowRequest() {
        long currentTime = System.currentTimeMillis();
        long deltaTime = currentTime - lastLeakTime;
        int leakedWater = (int) (deltaTime / 1000) * leakRate;
        currentWater = Math.max(0, currentWater - leakedWater);
        lastLeakTime = currentTime;

        if (currentWater < capacity) {
            currentWater++; // Add request to bucket
            return true; // Allow request
        }
        return false; // Reject request
    }
}

go代码实现:
package main

import (
	"sync"
	"time"
)

type LeakyBucketRateLimiter struct {
	capacity    int
	leakRate    int
	currentWater int
	lastLeakTime time.Time
	mu          sync.Mutex
}

func NewLeakyBucketRateLimiter(capacity int, leakRate int) *LeakyBucketRateLimiter {
	return &LeakyBucketRateLimiter{
		capacity: capacity,
		leakRate: leakRate,
		currentWater: 0,
		lastLeakTime: time.Now(),
	}
}

func (rl *LeakyBucketRateLimiter) AllowRequest() bool {
	rl.mu.Lock()
	defer rl.mu.Unlock()
	currentTime := time.Now()
	deltaTime := currentTime.Sub(rl.lastLeakTime).Seconds()
	leakedWater := int(deltaTime) * rl.leakRate
	rl.currentWater -= leakedWater
	if rl.currentWater < 0 {
		rl.currentWater = 0
	}
	rl.lastLeakTime = currentTime

	if rl.currentWater < rl.capacity {
		rl.currentWater++ // Add request to bucket
		return true // Allow request
	}
	return false // Reject request
}


4. 令牌桶算法(Token Bucket)

原理

令牌桶算法与漏桶算法类似,但它允许突发流量。系统以固定速率生成令牌,放入桶中。每个请求必须消耗一个令牌才能被处理,如果没有令牌,请求将被拒绝或等待。桶的大小决定了系统可以承受的突发流量上限。

流程
  1. 系统以固定速率生成令牌,并将令牌放入令牌桶中。
  2. 每个请求到达时,必须先从桶中获取一个令牌,才能被处理。
  3. 如果桶中没有令牌,请求将被拒绝或等待。
  4. 桶中可以积累令牌,从而支持一定的流量突发。
优点
  • 允许一定的突发流量,在突发流量到来时仍能保证部分请求被处理。
  • 适合需要应对偶尔流量高峰的系统。
缺点
  • 实现较为复杂,需要处理令牌生成速率和桶容量的动态调整。
场景
  • 适合那些偶尔有突发流量、但需要大多数时间保持恒定处理速率的场景,比如带宽控制或视频流媒体系统。
示例

假设每秒生成 5 个令牌,最多可以存储 10 个令牌。如果一秒内收到 8 个请求,5 个请求被处理,剩下的 3 个请求会等待令牌。

java代码实现:
import java.util.concurrent.atomic.AtomicInteger;

public class TokenBucketRateLimiter {
    private final int capacity;
    //每秒生成的令牌数量,通常以“令牌/秒”为单位
    private final int rate;
    //生成一个令牌所需的时间间隔,通常以秒为单位
    private final long interval;
    private int tokens;
    private long lastRefillTime;

    public TokenBucketRateLimiter(int capacity, int rate, long interval) {
        this.capacity = capacity;
        this.rate = rate;
        this.interval = interval;
        this.tokens = 0;
        this.lastRefillTime = System.currentTimeMillis();
    }

    public synchronized boolean allowRequest() {
        long currentTime = System.currentTimeMillis();
        refillTokens(currentTime);
        if (tokens > 0) {
            tokens--; // Consume a token
            return true; // Allow request
        }
        return false; // Reject request
    }

    private void refillTokens(long currentTime) {
        long timeElapsed = currentTime - lastRefillTime;
        int tokensToAdd = (int) (timeElapsed / interval) * rate;
        tokens = Math.min(capacity, tokens + tokensToAdd);
        lastRefillTime = currentTime;
    }
}

go代码实现:
package main

import (
	"sync"
	"time"
)

type TokenBucketRateLimiter struct {
	capacity     int
	rate         int
	interval     time.Duration
	tokens       int
	lastRefillTime time.Time
	mu           sync.Mutex
}

func NewTokenBucketRateLimiter(capacity int, rate int, interval time.Duration) *TokenBucketRateLimiter {
	return &TokenBucketRateLimiter{
		capacity:     capacity,
		rate:         rate,
		interval:     interval,
		tokens:       0,
		lastRefillTime: time.Now(),
	}
}

func (rl *TokenBucketRateLimiter) AllowRequest() bool {
	rl.mu.Lock()
	defer rl.mu.Unlock()
	currentTime := time.Now()
	rl.refillTokens(currentTime)
	if rl.tokens > 0 {
		rl.tokens-- // Consume a token
		return true // Allow request
	}
	return false // Reject request
}

func (rl *TokenBucketRateLimiter) refillTokens(currentTime time.Time) {
	timeElapsed := currentTime.Sub(rl.lastRefillTime)
	tokensToAdd := int(timeElapsed / rl.interval) * rl.rate
	rl.tokens += tokensToAdd
	if rl.tokens > rl.capacity {
		rl.tokens = rl.capacity // Limit to capacity
	}
	rl.lastRefillTime = currentTime
}


5. 熔断器(Circuit Breaker)

原理

熔断器并不是严格意义上的限流算法,它是用于保护系统的机制。当系统检测到自身的负载过大或某些功能出现问题时,熔断器会切断请求的继续处理,以防止系统崩溃或过载。

流程
  1. 系统监控自身的状态(如响应时间、错误率等)。
  2. 当某个阈值(如错误率过高)被触发时,熔断器“打开”,所有请求被直接拒绝。
  3. 系统在一段时间后尝试恢复,熔断器进入“半开”状态,允许部分请求通过。
  4. 如果系统恢复正常,熔断器完全关闭,系统恢复正常流量处理。
优点
  • 有效防止系统在压力过大时崩溃,保护系统稳定运行。
  • 可与其他限流算法结合使用,提供额外的保护。
缺点
  • 不能直接控制请求的流量,更多用于故障处理场景。
场景
  • 适合需要应对系统高负载或突发错误的场景,如微服务架构中的服务熔断保护。
示例

如果系统的错误率超过 50%,熔断器开启,所有新请求被直接拒绝,避免进一步的压力导致系统崩溃。

java代码实现:
public class CircuitBreaker {
    private final int failureThreshold;
    private final long timeout;
    private int failureCount;
    private long lastFailureTime;
    private boolean isOpen;

    public CircuitBreaker(int failureThreshold, long timeout) {
        this.failureThreshold = failureThreshold;
        this.timeout = timeout;
        this.failureCount = 0;
        this.lastFailureTime = 0;
        this.isOpen = false;
    }

    public synchronized boolean allowRequest() {
        if (isOpen){
            if (System.currentTimeMillis() - lastFailureTime > timeout) {
                    isOpen = false;  // Reset the circuit breaker after timeout
                    failureCount = 0; // Reset failure count
            }
            return !isOpen; // Allow request if the circuit is closed
        }
    	return true // Allow request if circuit is closed
	}

    public synchronized void recordFailure() {
        failureCount++;
        lastFailureTime = System.currentTimeMillis();
        if (failureCount >= failureThreshold) {
            isOpen = true; // Open the circuit if failure threshold is reached
        }
    }

    public synchronized void recordSuccess() {
        failureCount = 0; // Reset on success
    }
}
go代码实现:
package main

import (
	"sync"
	"time"
)

type CircuitBreaker struct {
	failureThreshold int
	timeout          time.Duration
	failureCount     int
	lastFailureTime  time.Time
	isOpen           bool
	mu               sync.Mutex
}

func NewCircuitBreaker(failureThreshold int, timeout time.Duration) *CircuitBreaker {
	return &CircuitBreaker{
		failureThreshold: failureThreshold,
		timeout:          timeout,
	}
}

func (cb *CircuitBreaker) AllowRequest() bool {
	cb.mu.Lock()
	defer cb.mu.Unlock()
	if cb.isOpen {
		if time.Since(cb.lastFailureTime) > cb.timeout {
			cb.isOpen = false // Reset the circuit after timeout
			cb.failureCount = 0 // Reset failure count
		} else {
			return false // Reject request if circuit is open
		}
	}
	return true // Allow request if circuit is closed
}

func (cb *CircuitBreaker) RecordFailure() {
	cb.mu.Lock()
	defer cb.mu.Unlock()
	cb.failureCount++
	cb.lastFailureTime = time.Now()
	if cb.failureCount >= cb.failureThreshold {
		cb.isOpen = true // Open the circuit if failure threshold is reached
	}
}

func (cb *CircuitBreaker) RecordSuccess() {
	cb.mu.Lock()
	defer cb.mu.Unlock()
	cb.failureCount = 0 // Reset on success
}


6. 随机丢弃(Random Early Drop,RED)

原理

随机丢弃是一种队列管理策略,当系统的请求队列接近满负荷时,随机丢弃部分请求,以防止系统超载。这种机制会在队列未满时就开始丢弃请求,从而在系统达到高负载前缓解压力。

流程
  1. 监控系统的请求队列。
  2. 当队列长度达到某个阈值时,开始随机丢弃部分请求。
  3. 队列越满,丢弃请求的概率越大。
  4. 在队列完全满之前,系统已经通过丢弃部分请求减轻负载。
优点
  • 减轻压力:在系统负载接近饱和时主动减轻压力,避免系统完全崩溃。
  • 公平性:通过随机丢弃,可以保证所有请求都有相同的机会被处理,减少某些请求长期被丢弃的情况。
缺点
  • 不确定性:由于请求被随机丢弃,可能导致某些重要请求被拒绝,不适合对请求有严格时限的场景。
  • 实现复杂度:需要监控队列长度,并设定丢弃概率的阈值,增加了系统的复杂性。
场景
  • 适合需要处理大量突发请求的场景,比如网络设备中的流量管理、视频流的实时传输等。
示例

当请求队列的长度达到 80%,系统开始随机丢弃请求,丢弃的概率随着队列长度的增加而增加。在队列满的时候,所有新的请求都将被丢弃。

java代码实现:
import java.util.Random;

public class RandomEarlyDropRateLimiter {
    private final int limit;
    private final Random random;

    public RandomEarlyDropRateLimiter(int limit) {
        this.limit = limit;
        this.random = new Random();
    }

    public synchronized boolean allowRequest(int currentCount) {
        if (currentCount < limit) {
            return true; // Allow request
        }
        // Randomly drop requests
        return random.nextInt(currentCount + 1) > limit; // Drop with a probability
    }
}

go代码实现:
package main

import (
	"math/rand"
	"sync"
)

type RandomEarlyDropRateLimiter struct {
	limit int
	mu    sync.Mutex
}

func NewRandomEarlyDropRateLimiter(limit int) *RandomEarlyDropRateLimiter {
	return &RandomEarlyDropRateLimiter{limit: limit}
}

func (rl *RandomEarlyDropRateLimiter) AllowRequest(currentCount int) bool {
	rl.mu.Lock()
	defer rl.mu.Unlock()
	if currentCount < rl.limit {
		return true // Allow request
	}
	// Randomly drop requests
	return rand.Intn(currentCount+1) > rl.limit // Drop with a probability
}


7. 基于优先级的限流

原理

基于优先级的限流算法考虑请求的优先级,根据优先级决定请求的处理顺序。高优先级的请求可以优先处理,而低优先级的请求在系统负载高时可能会被延迟处理或拒绝。

流程
  1. 将请求分为不同的优先级(如高、中、低)。
  2. 高优先级的请求被放在队列的前面,优先处理。
  3. 在处理请求时,检查当前系统负载,根据优先级决定处理哪个请求。
  4. 如果系统负载过高,低优先级的请求可能会被延迟或拒绝。
优点
  • 能根据业务需求灵活地处理不同重要性的请求,提供更好的用户体验。
  • 在高负载情况下,能够保证重要请求不被影响。
缺点
  • 实现复杂度较高,需要定义清晰的优先级策略。
  • 可能导致低优先级请求长期得不到处理。
场景
  • 适用于对请求有不同重要性的场景,比如电商抢购中 VIP 用户和普通用户请求的处理。
示例

在一场促销活动中,VIP 用户的请求被设定为高优先级,系统优先处理 VIP 请求,而普通用户请求可能需要等待或在高负载时被拒绝。

java代码实现:
import java.util.PriorityQueue;

public class PriorityRateLimiter {
    private final PriorityQueue<PriorityRequest> requestQueue;
    private final int limit;

    public PriorityRateLimiter(int limit) {
        this.limit = limit;
        this.requestQueue = new PriorityQueue<>((a, b) -> b.priority - a.priority);
    }

    public synchronized boolean allowRequest(PriorityRequest request) {
        if (requestQueue.size() < limit) {
            requestQueue.offer(request);
            return true; // Allow request
        }
        return false; // Reject request
    }

    public synchronized PriorityRequest processRequest() {
        if (!requestQueue.isEmpty()) {
            return requestQueue.poll(); // Process highest priority request
        }
        return null; // No requests to process
    }
}

class PriorityRequest {
    public final int priority;

    public PriorityRequest(int priority) {
        this.priority = priority;
    }
}

go代码实现:
package main

import (
	"container/heap"
	"sync"
)

type PriorityRequest struct {
	Priority int
}

type PriorityQueue []*PriorityRequest

func (pq PriorityQueue) Len() int { return len(pq) }
func (pq PriorityQueue) Less(i, j int) bool {
	return pq[i].Priority > pq[j].Priority // Higher priority first
}
func (pq PriorityQueue) Swap(i, j int) {
	pq[i], pq[j] = pq[j], pq[i]
}

func (pq *PriorityQueue) Push(x interface{}) {
	*pq = append(*pq, x.(*PriorityRequest))
}

func (pq *PriorityQueue) Pop() interface{} {
	old := *pq
	n := len(old)
	x := old[n-1]
	*pq = old[0 : n-1]
	return x
}

type PriorityRateLimiter struct {
	Queue *PriorityQueue
	Limit int
	mu    sync.Mutex
}

func NewPriorityRateLimiter(limit int) *PriorityRateLimiter {
	pq := make(PriorityQueue, 0)
	heap.Init(&pq)
	return &PriorityRateLimiter{
		Queue: &pq,
		Limit: limit,
	}
}

func (rl *PriorityRateLimiter) AllowRequest(request *PriorityRequest) bool {
	rl.mu.Lock()
	defer rl.mu.Unlock()
	if len(*rl.Queue) < rl.Limit {
		heap.Push(rl.Queue, request)
		return true // Allow request
	}
	return false // Reject request
}

func (rl *PriorityRateLimiter) ProcessRequest() *PriorityRequest {
	rl.mu.Lock()
	defer rl.mu.Unlock()
	if len(*rl.Queue) > 0 {
		return heap.Pop(rl.Queue).(*PriorityRequest) // Process highest priority request
	}
	return nil // No requests to process
}


总结

限流算法的选择需要根据业务需求和场景来进行权衡:

  • 如果需要简单的实现,且对突发流量不敏感,可以选择计数器法
  • 如果希望平滑流量、避免突发效应,可以选择滑动窗口法
  • 如果需要限制流量处理速率并允许少量突发流量,可以选择令牌桶算法
  • 如果系统压力较大,且需要稳定处理流量,可以使用漏桶算法
  • 如果系统错误率较高,可能需要通过熔断器保护系统。

你可能感兴趣的:(Java,go,限流算法)