文章目录
- 使用场景
- 使用固定窗口计数算法管理调用频率
- 使用测试
- 一秒钟执行五次
- 适用场景
使用场景
在调用第三方 API 时,我们通常会在 API 文档中看到对接口访问频率的限制。为了确保不超出这些限制,使用限流算法来控制 API 调用频率是至关重要的。
在作为 API 的使用者时,我们通常需要参考并使用限流算法来控制 API 调用的频率,确保不超过提供方设定的访问限制。例如,我们可以使用固定窗口计数算法来有效管理调用频率。
如果你是 API 的提供者,常用的限流算法则会有所不同。令牌桶算法及其变体通常被广泛应用,它们不仅能有效控制请求速率,还允许一定程度的突发流量,从而提升系统的灵活性和可靠性。
使用固定窗口计数算法管理调用频率
LimitUtil
实现的限流机制属于 固定窗口计数算法(Fixed Window Counter) 的一种变体。
代码中下面两个参数至关重要,根据自己的需求修改。
- N:时间窗口内最多允许的请求次数。这个参数直接决定了在指定时间窗口内可以处理的最大请求量。如果超过这个数量,多余的请求将被延迟或拒绝。
- bucket:时间窗口的大小。这个参数定义了时间窗口的长度(通常以毫秒为单位)。它决定了在多长时间内会统计和限制请求次数。
通过合理设置这两个参数,可以有效控制 API 的调用频率,避免超出限制。
代码如下:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class LimitUtil {
// 用于循环访问 requestTimeList 的索引,volatile 保证多线程之间的可见性
private static volatile int seq = 0;
// 时间窗口大小,单位为毫秒,这里设置为1000毫秒(即1秒)
private static final int bucket = 1000;
// 用于存储最近 N 次请求的时间戳列表,volatile 保证线程安全
private static volatile List<Long> requestTimeList;
// 每个 bucket 时间窗口内最多允许的请求次数,这里按需求自定义
private static final int N = 1;
static {
// 初始化 requestTimeList,为长度为 N 的列表,所有值均为 0L
requestTimeList = new ArrayList<>(Collections.nCopies(N, 0L));
}
// 私有构造函数,防止实例化该工具类
private LimitUtil() {
}
/**
* 限流实现,使用 synchronized 修饰,表示对整个类上锁,保证线程安全
*
* @throws InterruptedException 如果线程被中断,则抛出该异常
*/
public static synchronized void tryBeforeRun() throws InterruptedException {
// 获取当前系统时间(毫秒)
long now = System.currentTimeMillis();
// 计算当前请求时间与 seq 索引处的上一次请求时间的间隔
long interval = now - requestTimeList.get(seq);
if (interval < 0) {
// 如果当前时间早于上次请求时间(即 interval < 0),需要等待到上次请求的下一个时间点再执行
// 计算等待时间,并让线程等待
Thread.sleep(bucket - interval);
// 递归调用自己,确保限流逻辑执行
tryBeforeRun();
}
if (interval < bucket) {
// 如果时间间隔小于 bucket 时间窗口,说明请求太快,需要延迟执行
requestTimeList.set(seq, requestTimeList.get(seq) + bucket);
Thread.sleep(bucket - interval);
} else {
// 否则,更新当前索引处的请求时间为当前时间
requestTimeList.set(seq, now);
}
// 更新 seq,指向下一个索引位置,采用取模操作实现循环
seq = (seq + 1) % requestTimeList.size();
}
}
使用测试
我下面使用 100 个线程并发调用 API,并在调用 API 之前引入限流方法。通过打印每次 API 调用的时间戳,可以观察并验证限流算法的效果以及 API 调用频率的变化。
@Test
public void testLimitUtil() {
// 100个线程并发测试
ThreadUtil.concurrencyTest(100, () -> {
try {
LimitUtil.tryBeforeRun();
} catch (InterruptedException e) {
e.printStackTrace();
}
this.api();
});
}
private void api() {
long now = System.currentTimeMillis();
System.out.println("API 调用时间: " + now / 1000 + "秒,由线程 " + Thread.currentThread().getName() + " 执行");
// 模拟业务逻辑的处理
}
设定的 N = 1 和 bucket = 1000 的参数配置。
API 调用时间: 1724903993秒,由线程 hutool-4 执行
API 调用时间: 1724903994秒,由线程 hutool-9 执行
API 调用时间: 1724903995秒,由线程 hutool-7 执行
API 调用时间: 1724903996秒,由线程 hutool-6 执行
API 调用时间: 1724903997秒,由线程 hutool-8 执行
API 调用时间: 1724903998秒,由线程 hutool-2 执行
API 调用时间: 1724903999秒,由线程 hutool-5 执行
API 调用时间: 1724904000秒,由线程 hutool-100 执行
更多的就不展示了。
通过测试结果,可以确认:
限流机制有效:API 调用频率严格控制在每秒一次,符合设定的参数。
无并发问题:每个 API 调用的时间戳显示在不同的秒数,说明限流机制成功避免了多个线程在同一时间窗口内同时调用 API。
一秒钟执行五次
一秒钟执行五次只需要把N调整为5即可。
// 每个 bucket 时间窗口内最多允许的请求次数,这里按需求自定义
private static final int N = 5;
测试部分结果如下,符合预期,一秒钟执行最多执行5次api。
API 调用时间: 1724910227秒,由线程 hutool-25 执行
API 调用时间: 1724910227秒,由线程 hutool-23 执行
API 调用时间: 1724910227秒,由线程 hutool-10 执行
API 调用时间: 1724910227秒,由线程 hutool-24 执行
API 调用时间: 1724910227秒,由线程 hutool-5 执行
API 调用时间: 1724910228秒,由线程 hutool-8 执行
API 调用时间: 1724910228秒,由线程 hutool-38 执行
API 调用时间: 1724910228秒,由线程 hutool-3 执行
API 调用时间: 1724910228秒,由线程 hutool-9 执行
API 调用时间: 1724910228秒,由线程 hutool-4 执行
代码逻辑演示如下。
适用场景
这种算法适用于简单的场景,比如在1秒内只允许某个操作执行一次。它的实现简单,适合于限制在某段时间内的请求次数,但可能会在窗口边界处出现短时间内的突发流量问题(即"临界点问题"),这也是固定窗口计数算法的一般特点。
临界点问题验证
public static void main(String[] args) throws InterruptedException {
testCriticalPoint();
}
public static void testCriticalPoint() throws InterruptedException {
// 第一个请求,在接近时间窗口的末尾发起
Thread.sleep(950); // 等待950毫秒,使其接近1秒窗口的结束
System.out.println("第一次请求: " + System.currentTimeMillis() / 1000 + " 秒");
LimitUtil.tryBeforeRun();
// 第二个请求,在下一个时间窗口的开始发起
Thread.sleep(50); // 等待50毫秒,跨越到下一个时间窗口
System.out.println("第二次请求: " + System.currentTimeMillis() / 1000 + " 秒");
LimitUtil.tryBeforeRun();
}
运行结果:
第一次请求: 1724909724 秒
第二次请求: 1724909724 秒
在上述测试中,LimitUtil 在这两个请求之间没有进行限流,这说明在临界点时 LimitUtil 允许超出预期的请求数量,临界点问题确实存在。
分析:
临界点问题发生的原因在于,固定窗口计数算法只关注窗口内的请求数量,而没有考虑跨越窗口边界时的请求情况。
在这个例子中,虽然两个请求是在不同的时间窗口发起的,但由于时间窗口的计算是以固定的时间单位为基础(例如以秒为单位),因此可能会在窗口边界附近发生短时间内处理多个请求的情况。
解决方案:
为了避免临界点问题,可以考虑使用以下方法:
**滑动窗口计数算法(**Sliding Window Counter):
通过对请求的时间戳进行更精细的记录,并在窗口内的每个时刻统计请求数量,避免在窗口边界处出现突发流量。
令牌桶算法(Token Bucket Algorithm):
通过生成和消耗令牌来控制请求速率,允许一定的突发流量,同时控制平均请求速率,平滑处理流量。
滑动窗口平均算法(Sliding Window Rate Limiting):
将时间窗口进一步细分为更小的子窗口,计算多个子窗口的平均请求数量,避免在窗口边界处出现流量峰值。