写在前面
遥想当年,面对P8,意气风发,问我意向,开口微服务,闭眼高并发,上到集群,下到秒杀
P8一乐:来个基础,说说限流算法?
me :wow ......啊吧啊吧
固定窗口
概括:10秒内,只允许请求5次
维护一个固定大小的时间窗口,如 (00:00,00:10) 这个窗口只允许5次流量
(00:10,00:20) 这个窗口只允许5次流量
依次记录每个窗口请求的req次数,超出阈值时,拒绝请求
public class LimitMethodOne {
static long lastReqTime = System.currentTimeMillis();
static int counter = 0;
static int threshold = 3;
static long windowUnit = 10 * 1000l;
static synchronized boolean fixedWindowsTryAcquire() {
long currentTime = System.currentTimeMillis();
if (currentTime - lastReqTime > windowUnit) {
counter = 0;
lastReqTime = currentTime;
}
if (counter < threshold) {
counter++;
return true;
}
return false;
}
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newFixedThreadPool(20);
List<Callable<String>> runnables = new ArrayList<>();
for (int i = 0; i < 20; i++) {
runnables.add(new Callable() {
@Override
public String call() throws Exception {
if (fixedWindowsTryAcquire()) {
System.out.println("流量安全:" + Thread.currentThread());
System.out.println();
} else {
System.out.println("流量不安全:" + Thread.currentThread());
}
return "1";
}
});
}
executorService.invokeAll(runnables);
System.in.close();
}
}
缺陷: (00:00,00:10) 如果(00:08,00:10) 发生了5次req请求
(00:10,00:20) 如果(00:10,00:12) 发生了5次req请求
那么对于窗口(00:08,00:18) 10秒内发生了10次req请求,也就是临界问题,则不符合规则10秒内5次req请求
也就是滑动窗口解决
滑动窗口
滑动窗口的思想背景:可见基本Leetcode算法
leetCode滑动窗口
概括:10秒内,只允许请求5次
固定窗口的切分是 (00:00,00:10),(00:10,00:20),(00:20,00:30)
那么滑动窗口的思想则是更加细微的切分,也就是将大窗口,逐渐切分成小窗口,例如以1秒的格式递进
(00:00,00:10)
(00:01,00:11)
(00:02,00:12)
(00:03,00:13)
public class LimitMethodTwo {
private static Map<Long, Integer> counters = new HashMap<>();
static private int SUB_CYCLE = 1;
static private int threshold = 5;
static Long windowUnit = 10 * 1000l;
synchronized static boolean slidingWindowsTryAcquire() {
Long currentTime = System.currentTimeMillis() / 1000;
int currentWindowNum = countCurrentWindow(currentTime);
if (currentWindowNum > threshold) {
return false;
}
counters.put(currentTime, (Objects.isNull(counters.get(currentTime)) ? 0 : counters.get(currentTime)) + 1);
return true;
}
static int countCurrentWindow(Long currentWindowTime) {
Long startTime = currentWindowTime - windowUnit;
int count = 0;
Iterator iterator = counters.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Long, Integer> entry = (Map.Entry<Long, Integer>) iterator.next();
if (entry.getKey() < startTime) {
iterator.remove();
} else {
count = count + entry.getValue();
}
}
return count;
}
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newFixedThreadPool(20);
List<Callable<String>> runnables = new ArrayList<>();
for (int i = 0; i < 20; i++) {
runnables.add(new Callable() {
@Override
public String call() throws Exception {
if (slidingWindowsTryAcquire()) {
System.out.println("流量安全:" + Thread.currentThread());
System.out.println();
} else {
System.out.println("流量不安全:" + Thread.currentThread());
}
return "1";
}
});
}
executorService.invokeAll(runnables);
System.in.close();
}
}
缺陷:滑动窗口算法虽然解决了固定窗口的临界问题,但是一旦到达限流后,请求都会直接暴力被拒绝,也就是会丟是部分请求
漏桶算法
概括:假设你有一个容量为10l的桶,下面有个洞,这个桶以1l/s的恒定速度出水,只要桶未满,即可入桶
public class LimitMethodThree {
static long rate = 1l;
static long refreshTime = System.currentTimeMillis();
static long currentWater = 0;
static long capacity = 5l;
static synchronized boolean leakybucketLimitTryAcquire() {
long currentTime = System.currentTimeMillis();
long outWater = (currentTime - refreshTime) / 1000 * rate;
currentWater = Math.max(0, currentWater - outWater);
if (currentWater < capacity) {
currentWater++;
return true;
}
return false;
}
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newFixedThreadPool(200);
List<Callable<String>> runnables = new ArrayList<>();
for (int i = 0; i < 200; i++) {
runnables.add(new Callable() {
@Override
public String call() throws Exception {
if (leakybucketLimitTryAcquire()) {
System.out.println("流量安全:" + Thread.currentThread());
System.out.println();
} else {
System.out.println("流量不安全:" + Thread.currentThread());
}
return "1";
}
});
}
executorService.invokeAll(runnables);
System.in.close();
}
}
缺陷: 天突下暴雨⛈️,桶迅速装满,下面洞流出率水小, 无用
也就是突发流量下,无法应对
如何应对?见令牌桶
令牌桶算法
概括:楼主买劳斯莱斯的故事
楼主想买劳斯莱斯(req请求),前期是必须要有购车资格卡(令牌)
杭州政府每个月30天,放出共500张购车资格卡(令牌发放速率),在官网(桶)上
楼主抢到了购车资格卡(令牌),则可以买劳斯莱斯(允许请求)
未抢到,无购车资格(拒绝请求)

public class LimitMethodFour {
static long rate = 10l;
static long refreshTime = System.currentTimeMillis();
static long currentCount = 0;
static long capacity = 10;
synchronized static boolean tokenBucketTryAcquire() {
long currentTime = System.currentTimeMillis();
long generationCount = ((currentTime - refreshTime) / 1000) * rate;
currentCount = Math.min(capacity, generationCount + currentCount);
refreshTime = currentTime;
if (currentCount > 0) {
currentCount--;
return true;
}
return false;
}
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newFixedThreadPool(200);
List<Callable<String>> runnables = new ArrayList<>();
for (int i = 0; i < 200; i++) {
runnables.add(new Callable() {
@Override
public String call() throws Exception {
Thread.sleep(1000l);
if (tokenBucketTryAcquire()) {
System.out.println("流量安全:" + Thread.currentThread());
System.out.println();
} else {
System.out.println("流量不安全:" + Thread.currentThread());
}
return "1";
}
});
}
executorService.invokeAll(runnables);
System.in.close();
}
}
思考:对比于漏桶算法的缺陷,突降大雨,桶爆的缺陷
令牌桶则可以通过增大令牌发送速率来解决 (如杭州每个月发30000000000000张购车资格卡)
参考资料
面试必备:4种经典限流算法讲解