赞
踩
限流是对某一时间窗口内的请求数进行限制,保持系统的可用性和稳定性,防止因流量暴增而导致的系统运行缓慢或宕机。
在高并发系统中,出于系统保护角度考虑,通常会对流量进行限流。
在分布式系统中,高并发场景下,为了防止系统因突然的流量激增而导致的崩溃,同时保证服务的高可用性和稳定性,限流是最常用的手段。
常见的四种限流算法,分别是:固定窗口算法、滑动窗口算法、漏桶算法、令牌桶算法。
1. 实现原理
固定窗口又称固定窗口(又称计数器算法,Fixed Window)限流算法,是最简单的限流算法。
实现原理:在指定周期内累加访问次数,当访问次数达到设定的阈值时,触发限流策略,当进入下一个时间周期时进行访问次数的清零。如图所示,我们要求 3 秒内的请求不要超过 150 次:
2. 简易代码实现
public class FixedWindow { private long time = new Date().getTime(); private Integer count = 0; // 计数器 private final Integer max = 100; // 请求阈值 private final Integer interval = 1000; // 窗口大小 public boolean trafficMonitoring() { long nowTime = new Date().getTime(); if (nowTime < time + interval) { // 在时间窗口内 count++; return max > count; } else { time = nowTime; // 开启新的窗口 count = 1; // 初始化计数器,由于这个请求属于当前新开的窗口,所以记录这个请求 return true; } } }
3. 优缺点
限流不够平滑。例如:限流是每秒 3 个,在第一毫秒发送了 3 个请求,达到限流,窗口剩余时间的请求都将会被拒绝,体验不好。
无法处理窗口边界问题。因为是在某个时间窗口内进行流量控制,所以可能会出现窗口边界效应,即在时间窗口的边界处可能会有大量的请求被允许通过,从而导致突发流量。
即:如果第 2 到 3 秒内产生了 150 次请求,而第 3 到 4 秒内产生了 150 次请求,那么其实在第 2 秒到第 4
秒这两秒内,就已经发生了 300 次请求了,远远大于我们要求的 3 秒内的请求不要超过 150 次这个限制,如下图所示:
1. 实现原理
滑动窗口为固定窗口的改良版,解决了固定窗口在窗口切换时会受到两倍于阈值数量的请求。在滑动窗口算法中,窗口的起止时间是动态的,窗口的大小固定。这种算法能够较好地处理窗口边界问题,但是实现相对复杂,需要记录每个请求的时间戳。
实现原理:滑动窗口在固定窗口的基础上,将时间窗口进行了更精细的分片,将一个窗口分为若干个等份的小窗口,每次仅滑动一小块的时间。每个小窗口对应不同的时间点,拥有独立的计数器,当请求的时间点大于当前窗口的最大时间点时,则将窗口向前平
移一个小窗口(将第一个小窗口的数据舍弃,第二个小窗口变成第一个小窗口,当前请求放在最后一个小窗口),整个窗口的所有请求数相加不能大于阈值。其中,Sentinel 就是采用滑动窗口算法来实现限流的。
如图所示:
2. 使用zset实现滑动窗口
通过使用springAOP和redis中的zset进行实现滑动窗口限流
定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RateLimiter {
/**
* 限流时间,单位秒
*/
int time() default 5;
/**
* 限流次数
*/
int count() default 10;
}
对应注解限流处理切面(主要)
@Aspect @Component public class RateLimiterAspect { private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class); @Autowired private RedisTemplate redisTemplate; /** * 实现限流(新思路) * @param point * @param rateLimiter * @throws Throwable */ @Before("@annotation(rateLimiter)") public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable { // 在 {time} 秒内仅允许访问 {count} 次。 int time = rateLimiter.time(); int count = rateLimiter.count(); // 根据用户IP(可选)和接口方法,构造key String combineKey = getCombineKey(point); System.err.println(combineKey); // 记录本次访问的时间结点 long currentMs = System.currentTimeMillis(); redisTemplate.opsForZSet().add(combineKey, String.valueOf(currentMs), currentMs); // 这一步是为了防止一直存在于内存中 redisTemplate.expire(combineKey, time, TimeUnit.SECONDS); // 移除{time}秒之前的访问记录(滑动窗口思想) redisTemplate.opsForZSet().removeRangeByScore(combineKey, 0, currentMs - time * 1000); // 获得当前窗口内的访问记录数 Long currCount = redisTemplate.opsForZSet().zCard(combineKey); // 限流判断 if (currCount !=null && currCount > count) { //返回异常提示 log.error("[limit] 限制请求数'{}',当前请求数'{}',缓存key'{}'", count, currCount, combineKey); //todo 返回异常 } } /** * @param point 切入点 * @return 组合key */ private String getCombineKey(JoinPoint point) { StringBuilder sb = new StringBuilder("rate_limit:"); MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); Class<?> targetClass = method.getDeclaringClass(); // keyPrefix + "-" + class + "-" + method //类名加方法名 return sb.append("-").append( targetClass.getName() ) .append("-").append(method.getName()).toString(); } }
使用@RateLimiter注解进行限流
@RateLimiter(time = 1, count = 10)
@GetMapping("/testIndex")
public String index(){
System.out.println("处理请求");
return "finish";
}
3. 优缺点
1. 实现原理
漏桶限流算法是一种常用的流量整形(Traffic Shaping)和流量控制(Traffic Policing)的算法,它可以有效地控制数据的传输速率以及防止网络拥塞。
主要的作用:
实现原理:
漏桶是一个很形象的比喻,外部请求就像是水一样不断注入水桶中,而水桶已经设置好了最大出水速率,漏桶会以这个速率匀速放行请求,而当水超过桶的最大容量后则被丢弃。不管上面的水流速度有多块,漏桶水滴的流出速度始终保持不变。消息中间件就采用的漏桶限流的思想。
如图所示:
核心步骤:
2. 简易实现
public class LeakyBucket { private long capacity; // 漏桶容量 private long rate; // 流出速率 private long water; // 当前水量 private long lastTime; // 上次请求时间 public LeakyBucket(long capacity, long rate) { this.capacity = capacity; this.rate = rate; this.water = 0; this.lastTime = System.currentTimeMillis(); } public synchronized boolean allow() { long now = System.currentTimeMillis(); long elapsedTime = now - lastTime; lastTime = now; // 先漏水,根据流出速率计算漏掉的水量 water = Math.max(0, water - elapsedTime * rate); // 检查水量是否超出了容量 if (water < capacity) { water++; return true; // 请求通过,水量增加 } else { return false; // 请求被拒绝,水量已满 } } }
3. 优缺点
由于漏桶的缺陷比较明显,所以在实际业务场景中,使用的比较少。
1. 实现原理
令牌桶算法是基于漏桶算法的一种改进,主要在于令牌桶算法能够在限制服务调用的平均速率的同时,还能够允许一定程度内的突发调用。
实现原理:
令牌桶算法的一个重要特性是,它能够应对突发流量。当桶中有足够的令牌时,可以一次性处理多个请求,这对于需要处理突发流量的应用场景非常有用。但是又不会无限制的增加处理速率导致压垮服务器,因为桶内令牌数量是有限制的。
2. 代码实现
Guava 中的 RateLimiter 就是基于令牌桶实现的,可以直接拿来使用。
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>19.0</version>
</dependency>
public void acquireTest() {
//每秒固定生成5个令牌
RateLimiter rateLimiter = RateLimiter.create(5);
for (int i = 0; i < 10; i++) {
double time = rateLimiter.acquire();
logger.info("等待时间:{}s", time);
}
}
3. 优缺点
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。