限流器(Rate Limiter)是一种用于控制系统资源利用率和质量的重要机制。它通过限制单位时间内可以执行的操作数量,从而防止系统过载和保护服务的可靠性。在程序设计中,可以使用多种方式来实现限流器,下面是几个常见方案的介绍:
- 令牌桶算法
- 漏桶算法
- 划窗算法
- 固定窗口算法(缺点很大!)
- 基于计数器的流量控制算法
- ...
令牌桶算法(Token Bucket)
令牌桶算法是一种常见的限流实现方式。它维护一个存放令牌的桶,以固定的速率向桶中添加令牌。每次请求到来时,需要从桶中获取一个令牌,只有当桶中有足够的令牌时,请求才能被处理。否则,请求将被拒绝或阻塞。
实现思路如下:
- 维护一个固定大小的令牌桶和一个记录上一次令牌被添加到桶中的时间戳。
- 以固定的速率(每秒生成的令牌数)向桶中添加令牌。
- 当请求到来时,尝试从桶中获取一个令牌。如果桶中有令牌,则处理请求并消耗一个令牌;否则,拒绝或阻塞请求。
- 使用锁或其他同步机制来保证线程安全。
code:
package RateLimiter;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
// 令牌桶算法 (TokenBucketRateLimiter)
public class TokenBucketRateLimiter {
/**
* REFILL_PERIOD 表示令牌桶的refill周期,即每隔多长时间(秒)向桶中添加令牌。
* MAX_TOKENS 表示令牌桶的最大容量,即桶中最多可以存放多少个令牌。
* REFILL_TOKENS 表示每个refill周期向桶中添加的令牌数量。
* lastRefillTimestamp 记录上一次refill的时间戳。
* tokenBucket 是一个Semaphore实例,用于模拟令牌桶的行为。
*/
private static final long REFILL_PERIOD = 1; // 1秒
private static final long MAX_TOKENS = 5; // 桶容量
private static final long REFILL_TOKENS = 2; // 每次添加令牌数
/**
* AtomicLong是Java中用于表示一个原子性的长整型值的类。它提供了一些原子操作方法,用于在多线程环境下安全地更新和访问长整型值。
* - 在这些限流器实现中,AtomicLong主要用于记录上一次令牌/请求刷新的时间戳。
* - 由于多个线程可能同时尝试获取令牌或请求,因此需要确保对时间戳的读写操作是原子性的,以避免竞态条件。
*/
private AtomicLong lastRefillTimestamp = new AtomicLong(System.nanoTime());
/**
* Semaphore(信号量)是Java中一个并发控制工具,用于控制对共享资源的访问。
* - 它基于计数器的原理,可以限制同时访问某个资源的线程数量。用于模拟令牌桶的行为。
* - Semaphore使用acquire()和release()方法来获取和释放信号量:
*/
private Semaphore tokenBucket = new Semaphore((int) MAX_TOKENS);
/**
* tryAcquire() 方法是获取令牌的入口:
*
* 1-获取当前时间戳 now。
* 2-根据当前时间戳和上一次refill时间戳,计算出这段时间内应该添加多少个令牌 newTokens。
* 3-更新上一次refill时间戳为当前时间戳。
* 4-将新的令牌数量 newTokens 释放到 tokenBucket 中。
* 5-尝试从 tokenBucket 中获取一个令牌,如果成功则返回 true,否则返回 false。
*/
public boolean tryAcquire() {
long now = System.nanoTime();
long lastRefillTime = lastRefillTimestamp.get();
long newTokens = calculateNewTokens(lastRefillTime, now);
lastRefillTimestamp.set(now);
tokenBucket.release((int) newTokens);
return tokenBucket.tryAcquire();
}
/**
* calculateNewTokens() 方法根据时间差计算出应该添加的令牌数量,但不会超过桶的最大容量。
*/
private long calculateNewTokens(long lastRefillTime, long now) {
long nanosElapsed = now - lastRefillTime;
long refillPeriodCount = nanosElapsed / TimeUnit.SECONDS.toNanos(REFILL_PERIOD);
return Math.min(refillPeriodCount * REFILL_TOKENS, MAX_TOKENS);
}
}
漏桶算法(Leaky Bucket)
漏桶算法类似于令牌桶算法,不同之处在于它维护一个存放请求的队列,而不是令牌桶。当请求到来时,它们会被添加到队列中。队列以固定的速率漏水,即以固定的速率处理请求。
实现思路如下:
- 维护一个固定大小的请求队列和一个上次处理请求的时间戳。
- 当请求到来时,将其添加到队列中。如果队列已满,则拒绝或阻塞请求。
- 以固定的速率(每秒处理的请求数)从队列中取出请求并处理。
- 使用锁或其他同步机制来保证线程安全。
code:
package RateLimiter;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
// 漏桶算法 (LeakyBucketRateLimiter)
public class LeakyBucketRateLimiter {
/**
* REFILL_PERIOD 表示漏桶的refill周期,即每隔多长时间(秒)处理请求。
* MAX_REQUESTS 表示漏桶的最大容量,即桶中最多可以存放多少个请求。
* REFILL_REQUESTS 表示每个refill周期处理的请求数量。
* requestQueue 是一个队列,用于存放待处理的请求。
* lastRefillTimestamp 记录上一次refill的时间戳。
*/
private static final long REFILL_PERIOD = 1; // 1秒
private static final long MAX_REQUESTS = 5; // 桶容量
private static final long REFILL_REQUESTS = 2; // 每次处理请求数
private Queue<Long> requestQueue = new LinkedList<>();
private AtomicLong lastRefillTimestamp = new AtomicLong(System.nanoTime());
/**
* tryAcquire() 方法是获取请求的入口:
* - 获取当前时间戳 now。
* - 根据当前时间戳和上一次refill时间戳,计算出这段时间内应该处理多少个请求 newRequests。
* - 更新上一次refill时间戳为当前时间戳。
* - 将新的请求数量 newRequests 添加到 requestQueue 中,如果队列已满则移除最早的请求。
* - 如果队列大小不超过最大容量 MAX_REQUESTS,则返回 true,否则返回 false。
*/
public boolean tryAcquire() {
long now = System.nanoTime();
long lastRefillTime = lastRefillTimestamp.get();
long newRequests = calculateNewRequests(lastRefillTime, now);
lastRefillTimestamp.set(now);
for (long i = 0; i < newRequests; i++) {
if (requestQueue.size() >= MAX_REQUESTS) {
requestQueue.poll();
}
requestQueue.offer(now);
}
return requestQueue.size() <= MAX_REQUESTS;
}
/**
* calculateNewRequests() 方法根据时间差计算出应该处理的请求数量。
*/
private long calculateNewRequests(long lastRefillTime, long now) {
long nanosElapsed = now - lastRefillTime;
long refillPeriodCount = nanosElapsed / TimeUnit.SECONDS.toNanos(REFILL_PERIOD);
return refillPeriodCount * REFILL_REQUESTS;
}
}
滑动窗口(Sliding Window)
滑动窗口算法通过维护一个固定大小的窗口来限制单位时间内的请求数。当请求到来时,它会检查窗口内的请求数是否已达到限制。如果没有,则允许请求;否则,拒绝或阻塞请求。窗口会随着时间推移而滑动,移除较早的请求记录。
冷知识:
TCP协议中数据包的传输,同样也是采用滑动窗口来进行流量控制。
实现思路如下:
- 维护一个队列或其他数据结构来存储请求的时间戳。
- 当请求到来时,将其时间戳添加到队列中。
- 检查队列中最近的一段时间内(窗口大小)的请求数是否超过限制。如果没有,则允许请求;否则,拒绝或阻塞请求。
- 定期(或在每次请求到来时)移除队列中较早的请求记录,以维护窗口大小。
- 使用锁或其他同步机制来保证线程安全。
Reference:
https://blog.csdn.net/legend050709/article/details/114917637
原理:
需要先看看固定窗口算法的原理和缺点,
动图:
code:
package RateLimiter;
import java.util.LinkedList;
import java.util.Queue;
/**
* 滑动窗口算法 (SlidingWindowRateLimiter)
*/
public class SlidingWindowRateLimiter {
/**
* WINDOW_SIZE 表示滑动窗口的大小(秒)。
* MAX_REQUESTS 表示窗口内允许的最大请求数量。
* requestTimestamps 是一个队列,用于存放请求的时间戳。
*/
private static final long WINDOW_SIZE = 5; // 窗口大小(秒)
private static final long MAX_REQUESTS = 10; // 最大请求数
private Queue<Long> requestTimestamps = new LinkedList<>();
/**
* tryAcquire() 方法是获取请求的入口:
* - 获取当前时间戳 now。
* - 将当前时间戳添加到 requestTimestamps 队列中。
* - 计算窗口的开始时间 windowStartTime。
* - 移除队列中早于 windowStartTime 的时间戳,即移除窗口之外的请求记录。
* - 如果队列大小不超过 MAX_REQUESTS,则返回 true,否则返回 false。
*/
public boolean tryAcquire() {
long now = System.currentTimeMillis();
requestTimestamps.add(now);
long windowStartTime = now - WINDOW_SIZE * 1000;
while (!requestTimestamps.isEmpty() && requestTimestamps.peek() < windowStartTime) {
requestTimestamps.poll();
}
return requestTimestamps.size() <= MAX_REQUESTS;
}
}
演示code:
package RateLimiter;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class RateLimiterDemo {
public static void main(String[] args) {
// 创建限流器实例
TokenBucketRateLimiter tokenBucketRateLimiter = new TokenBucketRateLimiter();
LeakyBucketRateLimiter leakyBucketRateLimiter = new LeakyBucketRateLimiter();
SlidingWindowRateLimiter slidingWindowRateLimiter = new SlidingWindowRateLimiter();
// 创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
// 提交任务
for (int i = 0; i < 20; i++) {
executorService.submit(() -> {
boolean tokenBucketAllowed = tokenBucketRateLimiter.tryAcquire();
boolean leakyBucketAllowed = leakyBucketRateLimiter.tryAcquire();
boolean slidingWindowAllowed = slidingWindowRateLimiter.tryAcquire();
System.out.println("Token Bucket: " + tokenBucketAllowed +
", Leaky Bucket: " + leakyBucketAllowed +
", Sliding Window: " + slidingWindowAllowed);
try {
TimeUnit.MILLISECONDS.sleep(500); // 模拟处理请求
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 关闭线程池
executorService.shutdown();
}
}
out:
Token Bucket: true, Leaky Bucket: true, Sliding Window: true
Token Bucket: false, Leaky Bucket: true, Sliding Window: true
Token Bucket: false, Leaky Bucket: true, Sliding Window: true
Token Bucket: false, Leaky Bucket: true, Sliding Window: true
Token Bucket: true, Leaky Bucket: true, Sliding Window: true
Token Bucket: false, Leaky Bucket: true, Sliding Window: true
Token Bucket: false, Leaky Bucket: true, Sliding Window: true
Token Bucket: true, Leaky Bucket: true, Sliding Window: true
Token Bucket: true, Leaky Bucket: true, Sliding Window: true
Token Bucket: true, Leaky Bucket: true, Sliding Window: true
Token Bucket: false, Leaky Bucket: true, Sliding Window: false
Token Bucket: false, Leaky Bucket: true, Sliding Window: false
Token Bucket: false, Leaky Bucket: true, Sliding Window: false
Token Bucket: false, Leaky Bucket: true, Sliding Window: false
Token Bucket: false, Leaky Bucket: true, Sliding Window: false
Token Bucket: false, Leaky Bucket: true, Sliding Window: false
Token Bucket: false, Leaky Bucket: true, Sliding Window: false
Token Bucket: false, Leaky Bucket: true, Sliding Window: false
Token Bucket: false, Leaky Bucket: true, Sliding Window: false
Token Bucket: false, Leaky Bucket: true, Sliding Window: false
总结
这三种算法都是通过控制请求的速率或数量来实现限流,但具体的实现方式有所不同。令牌桶算法和漏桶算法都依赖于时间来控制速率,而滑动窗口算法则是基于请求数量来控制。它们各有优缺点,适合不同的场景。具体选择哪种需要自己根据应用场景进行选择和调整。
同时,也可以考虑使用现有的限流器库或框架,如Guava RateLimiter、Netflix Hystrix等,以简化开发过程。
练习1
请给出答案:
todo