- 漏桶
- 实现
- 功能性测试
- 滑动窗口
- 实现
- 功能性测试
- 令牌桶
- 单机
- 分布式
- 功能性测试
- 参考
本文的限流工具都只有功能性测试(见正文),未进行过高并发和大流量下的性能测试,生产环境下的性能未知,仅供参考。完整源码详见 github。
漏桶
漏桶是最简单的限流工具,设计思路为:如果时间间隔达到规定的时间间隔,则允许通过,否则返回失败。
实现
LeakyLimiter 类中有四个属性,最核心的是 intervalNanos,表示时间间隔。如下所示:
private final RedisService redisService;
// 漏桶唯一标识
private final String name;
// 分布式互斥锁
private final RLock lock;
// 每两滴水之间的时间间隔
private final long intervalNanos;
redisService 用于操作缓存,lock 表示分布式锁。
上锁和解锁的方法如下所示:
/**
* 尝试获取锁
* @return 获取成功返回 true
*/
private boolean lock() {
try {
// 等待 100 秒,获得锁 100 秒后自动解锁
return this.lock.tryLock(100, 100, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
}
/**
* 释放锁
*/
private void unlock() {
this.lock.unlock();
}
在 acquire 方法中检查是否已达到时间间隔,如下所示:
/**
* 尝试通过漏桶
*
* @return 获取成功返回 true,失败返回 false
*/
@Override
public boolean acquire() {
while (true) {
if (lock()) {
try {
return tryAcquire();
} finally {
unlock();
}
}
}
}
private boolean tryAcquire() {
long recent = getRecent();
long now = System.nanoTime();
if (now - recent >= this.intervalNanos) {
resync(now);
return true;
} else {
log.info("Acquire LeakyLimiter[" + this.name + "] failed.");
return false;
}
}
getRecent 的作用是获取当前时间,resync 是同步缓存中的最新时间戳。
private long getRecent() {
Long recent = redisService.get(LeakyBucketKey.leakyBucket, this.name, Long.class);
if (recent == null) {
recent = System.nanoTime();
resync(recent);
return recent - intervalNanos;
}
return recent;
}
private void resync(long now) {
redisService.setwe(LeakyBucketKey.leakyBucket, this.name, now, LeakyBucketKey.leakyBucket.expireSeconds());
}
功能性测试
测试漏桶功能,多线程同时请求通过漏桶,只有一个线程能通过。代码如下所示:
@Test
public void getLeakyLimiter() {
LeakyLimiterFactory factory = new LeakyLimiterFactory();
LeakyLimiterConfig config = new LeakyLimiterConfig("testLeakyLimiter", 1, redissonService.getRLock("testLeakylock"), redisService);
final LeakyLimiter leakyLimiter = factory.getLeakyLimiter(config);
final int N = 3;
Runnable task = new Runnable() {
@Override
public void run() {
if (leakyLimiter.acquire()) {
System.out.println(Thread.currentThread().getName() + " passed.");
} else {
System.out.println(Thread.currentThread().getName() + " failed.");
}
}
};
Executor executor = Executors.newFixedThreadPool(N);
for (int i = 0; i < N; i++) {
executor.execute(task);
}
try {
Thread.sleep(2 * 1000);
} catch (Exception e) {
e.printStackTrace();
}
Executor executor2 = Executors.newFixedThreadPool(N);
for (int i = 0; i < N; i++) {
executor2.execute(task);
}
}
滑动窗口
计数器限流是统计一段时间间隔内的请求数,如果达到了阈值,则拒绝后面的请求。滑动窗口在此基础上将时间间隔进行细分,让请求更平滑地执行。
如果计数器的时间间隔为 1s,限制请求数为 1000,考虑如下情况:在前一秒的最后 100ms 通过请求数 1000,下一秒的前 100ms 通过请求数 1000,实际上在 200ms 的时间内通过了 2000 个请求,远远超过了限流器的原始设计。
滑动窗口把 1s 的时间段分成更小的部分,例如 10 份,当时间到达后一秒的前 100ms 时,滑动窗口的范围是前一秒的后 900ms 和后一面的前 100ms,这时候窗口范围内已经达到了限制请求数,不会允许此时的 1000 个请求通过。无论何时,窗口范围内都只允许最大 1000 个请求。
实现
滑动窗口使用链表实现,链表的每一个节点是 Node 类的实例。Node 表示一小段时间间隔,类中有三个属性,分别代表“起始时间”、“终止时间”、“时间段内计数”。如果滑动窗口已经完全经过该时间段,可以把该段删除。
Node 节点如下所示:
public class Node {
private long startTime;
private long endTime;
private long count;
// getter and setter
// ...
}
Window 是在缓存中传递的载体,包括以下属性:
// 唯一标识
private String name;
// 滑动窗口
private LinkedList<Node> slots;
// 时间间隔