大纲
1.关于限流的概述
2.高并发下的四大限流算法原理及实现
3.Sentinel使用的设计模式总结
1.关于限流的概述
保护高并发系统的三把利器:缓存、降级和限流。限流就是通过限制请求的流量以达到保护系统的目的,比如秒杀抢购。具体就是对并发请求进行限速,或对一个时间窗口内的的请求进行限速,一旦达到限制速率就会拒绝服务或进行流量整形。
常用的限流方式:
一.限制总请求数
如数据库连接池、线程池。
二.限制瞬时并发数
如Nginx的LimitConn模块,可以用来限制瞬时并发连接数。如Java的Semaphore也可以用来限制瞬时并发数。如果需要限制方法被调用的并发数不能超过100(同一时间并发数),则可以使用信号量Semaphore来实现。
三.限制时间窗口内的平均速率
如Guava的RateLimiter、Nginx的LimitReq模块,就可以用来限制每秒的请求速率。如果需要限制方法在一段时间内平均被调用次数不超过100,则可以使用RateLimiter来实现。Guava的RateLimiter只能用于单机的限流,如果想要集群限流,则需要引入Redis或者阿里开源的Sentinel中间件。
四.限制远程接口的调用速率
五.限制MQ的消费速率
六.根据网络、CPU或内存负载等来限流
2.高并发下的四大限流算法原理及实现
(1)固定窗口计数法
(2)滑动窗口计数法
(3)漏桶算法
(4)令牌桶算法
(5)四种限流算法的对比总结
(1)固定窗口计数法
一.实现原理
在一个固定长度的时间窗口内限制请求数量。每来一个请求,请求数加一,如果请求数超过最大限制,就拒绝该请求。
二.算法流程

三.算法存在的问题
问题一:限流不够平滑
例如设置的是每秒限流3个请求,第一毫秒就发送3个请求,达到限流。那么窗口剩余时间的请求都将会被拒绝,这样用户体验就不好。
问题二:存在突发流量的问题
由于在进行窗口切换时,当前窗口的访问总数会立即置为0,所以可能会导致流量突发的问题。
四.算法的代码实现
//注意:下面的实现并没有考虑并发的情况
public class FixWindowLimiter {
//窗口的大小,1000ms
public static long windowUnit = 1000;
//窗口的最大请求数
public static long threshold = 10;
//当前窗口内的请求数
public static long count = 0;
//当前窗口的开始时间
public static long lastTime = 0;
//限流方法,返回true表示通过
public boolean canPass() {
//获取当前时间
long currentTime = System.currentTimeMillis();
//判断当前时间与窗口的开始时间的时间差,是否大于窗口的大小
if (currentTime - lastTime > windowUnit) {
//当前窗口的请求数设置为0
count = 0;
//重置当前窗口的开始时间为当前时间
lastTime = currentTime;
}
//判断当前窗口的请求数是否超过窗口的最大请求数
if (count < threshold) {
count++;
return true;
}
return false;
}
}
(2)滑动窗口计数法
为解决固定窗口计数法潜在的流量突发问题,可使用滑动窗口计数法。
一.实现原理
在滑动窗口算法中,窗口的开始时间是动态的,窗口大小是固定的。每来一个请求,就向后推一个时间窗口,计算该窗口内的请求数量。如果请求数超过限制就拒绝请求,否则处理请求 + 记录请求的时间戳。另外还需要一个任务清理过期的时间戳,滑动窗口没有划分固定的时间窗起点与终点。
二.算法存在的问题
虽然解决了流量突发的问题,但限流依然不够平滑。例如设置的是每秒限流3个请求,第一毫秒就发送3个请求,达到限流。那么窗口剩余时间的请求都将会被拒绝,这样用户体验就不好。
三.算法的代码实现
public class SlidingWindowLimiter {
//每个窗口的最大请求数量
public static long threshold = 10;
//窗口大小,1000ms
public static long windowUnit = 1000;
//请求集合,用来存储窗口内的请求数量
public static List<Long> requestList = new ArrayList<>();
//限流方法,返回true表示通过
public boolean canPass() {
//获取当前时间
long currentTime = System.currentTimeMillis();
//统计当前时间对应的窗口,收到的请求的数量
int sizeOfValid = this.sizeOfValid(currentTime);
//判断请求数是否超过窗口的最大请求数量
if (sizeOfValid < threshold) {
//把当前请求添加到请求集合里
requestList.add(currentTime);
return true;
}
return false;
}
//统计当前时间对应的窗口的请求数
private int sizeOfValid(long currentTime) {
int sizeOfValid = 0;
for (Long requestTime : requestList) {
//判断是否在当前时间窗口内
if (currentTime - requestTime <= windowUnit) {
sizeOfValid++;
}
}
return sizeOfValid;
}
//清理过期的请求:单独启动一个线程处理
private void clean() {
//判断是否超出当前时间窗口
requestList.removeIf(requestTime -> System.currentTimeMillis() - requestTime > windowUnit);
}
}
(3)漏桶算法
它是一种流量整形(Traffic Shaping)和流量控制(Traffic Policing)的算法,它可以有效地控制流量的处理速率以及防止网络拥塞。
一.实现原理
首先,一个固定容量的漏桶,按照固定速率流出水(处理请求)。然后,当流入水的速度过大会直接溢出(请求数量超过限制则直接拒绝)。最后,漏桶里的水不够则无法流出水(漏桶内没有请求则不处理)。
当请求流量正常或者较小时,请求能够得到正常的处理。当请求流量过大时,漏桶算法可通过丢弃部分请求来防止系统过载。
这种算法的一个重要特性是:无论请求的接收速率如何变化,请求的处理速率始终是稳定的,这就确保了系统的负载不会超过预设的阈值。但是由于请求的处理速率是固定的,所以无法处理突发流量。此外如果入口流量过大,漏桶可能会溢出,导致请求丢失。
二.算法的优缺点
优点一:平滑流量
由于以固定的速率处理请求,所以可以有效地平滑和整形流量,避免流量的突发和波动,类似于消息队列的削峰填谷的作用。
优点二:防止过载
当流入的请求超过桶的容量时,可以直接丢弃请求,防止系统过载。
缺点一:无法处理突发流量
由于漏桶的出口速度是固定的,无法处理突发流量。例如,即使在流量较小的时候,也无法以更快的速度处理请求。
缺点二:可能会丢失数据
如果入口流量过大,超过了桶的容量,那么就需要丢弃部分请求。在一些不能接受丢失请求的场景中,这可能是一个问题。
缺点三:不适合处理速率变化大的场景
如果处理速率变化大,或需要动态调整处理速率,则无法满足。
三.算法的代码实现
public class LeakyBucketLimiter {
//桶的最大容量
public static long threshold = 10;
//当前桶内的水量
public static long count = 0;
//漏水速率(每秒5次)
public static long leakRate = 5;
//上次漏水时间
public static long lastLeakTime = System.currentTimeMillis();
//限流方法,返回true表示通过
public boolean canPass() {
//调用漏水方法
this.leak();
//判断是否超过最大请求数量
if (count < threshold) {
count++;
return true;
}
return false;
}
//漏水方法,计算并更新这段时间内漏水量
private void leak() {
//获取系统当前时间
long currentTime = System.currentTimeMillis();
//计算这段时间内,需要流出的水量
long leakWater = (currentTime - lastLeakTime) * leakRate / 1000;
count = Math.max(count - leakWater, 0);
//更新最近一次的漏水时间
lastLeakTime = currentTime;
}
}
(4)令牌桶算法
令牌桶限流算法也是一种常用的流量整形和限制请求处理速率的算法。
一.实现原理
首先,系统会以固定的速率向桶中添加令牌。然后,当有请求到来时,会尝试从桶中移除一个令牌。如果桶中有足够的令牌,则请求可以被处理。如果桶中没有令牌,那么请求将被拒绝。此外,桶中的令牌数不能超过桶的容量。如果新生成的令牌超过了桶的容量,那么新的令牌会被丢弃。
令牌桶算法的一个重要特性是,它能够处理突发流量。当桶中有足够的令牌时,可以一次性处理多个请求,这对于需要处理突发流量的应用场景非常有用。但是又不会无限制的增加处理速率导致压垮服务器,因为桶内令牌数量是有限制的。
二.算法的优缺点
优点一:可以处理突发流量
令牌桶算法可以处理突发流量。当桶满时,能够以最大速度处理请求。这对于需要处理突发流量的应用场景非常有用。
优点二:限制请求处理的平均速率
在长期运行中,请求的处理速率会被限制在预定义的平均速率下,也就是生成令牌的速率。
优点三:灵活性
与漏桶算法相比,令牌桶算法提供了更大的灵活性。例如,可以动态地调整生成令牌的速率。
缺点一:可能导致过载
如果令牌产生速度过快,可能会导致大量突发流量,使网络或服务过载。
缺点二:需要存储空间
令牌桶需要一定的存储空间来保存令牌,可能会导致内存资源的浪费。
三.算法的代码实现
public class TokenBucketLimiter {
//桶的最大容量
public static long threshold = 10;
//当前桶内的令牌数
public static long count = 0;
//令牌生成速率(每秒5次)
public static long tokenRate = 5;
//上次生成令牌的时间
public static long lastRefillTime = System.currentTimeMillis();
//限流方法,返回true表示通过
public boolean canPass() {
//调用生成令牌方法
this.refillTokens();
//判断桶内是否还有令牌
if (count > 0) {
count--;
return true;
}
return false;
}
//生成令牌方法,计算并更新这段时间内生成的令牌数量
private void refillTokens() {
long currentTime = System.currentTimeMillis();
//计算这段时间内,需要生成的令牌数量
long refillTokens = (currentTime - lastRefillTime) * tokenRate / 1000;
//更新桶内的令牌数
count = Math.min(count + refillTokens, threshold);
//更新令牌生成时间
lastRefillTime = currentTime;
}
}
(5)四种限流算法的对比总结
一.四种算法的优缺点
固定窗口算法实现简单,但是限流不够平滑,存在突发流量的问题,适用于需要简单实现限流的场景。
滑动窗口算法虽然解决了突发流量的问题,但是还是存在限流不够平滑的问题,所以它适用于需要控制平均请求速率的场景。
漏桶算法的优点是流量处理更平滑,但是无法应对突发流量,适用于需要平滑流量的场景。
令牌桶算法既能平滑流量,又能处理突发流量,适用于需要处理突发流量的场景。
二.令牌桶算法和漏桶算法的对比总结
令牌桶算法就是以固定速率生成令牌放入桶中。每个请求都需要从桶中获取令牌,没有获取到令牌的请求会被阻塞限流。当令牌消耗速度小于生成速度时,令牌桶内就会预存这些未消耗的令牌。当有突发流量进来时,可以直接从桶中取出令牌,而不会被限流。
漏桶算法就是将请求放入桶中,然后以固定的速率从桶中取出请求来处理。当桶中等待的请求数超过桶的容量后,后续的请求就不再加入桶中。
漏桶算法适用于需要以固定速率处理请求的场景。在多数业务场景中,其实并不需要按照严格的速率进行请求处理。而且多数业务场景都需要应对突发流量的能力,所以会使用令牌桶。
但不管是令牌桶算法还是漏桶算法,都可以通过延迟计算的方式来实现。延迟计算指的是不需要单独的线程来定时生成令牌或从漏桶中定时取请求,而是由调用限流器的线程自己来计算是否有足够的令牌以及需要sleep的时间。使用延迟计算的方式,可以节省一个线程资源。