共计 5204 个字符,预计需要花费 14 分钟才能阅读完成。
分布式高并发零碎常见的用来爱护零碎的三把利器:缓存、降级、限流。
有天深夜发现公司的点评后盾零碎数据库 cpu 打到 96% 以上,排查发现是有人应用脚本歹意拜访咱们的零碎。
失常状况下,调用量每秒 1 次左右,然而依据监控零碎发现 歹意申请拜访的接口每秒调用 20 次左右,并且调用的接口是慢接口,导致 cpu 应用飙升。为了爱护零碎,除了缓存和降级外,咱们采纳 限流 来针对这种歹意申请做限度,保障失常用户的应用,抵挡歹意申请。
🍓限流
什么是限流呢?限流是限度达到零碎的并发申请数量,保证系统可能失常响应局部用户申请,而对于超过限度的流量,则通过拒绝服务的形式保障整体零碎的可用性。
依据限流作用范畴,能够分为 单机限流 和分布式限流 ;
依据限流形式,又分为 计数器、滑动窗口、漏桶限令牌桶限流
🍀计数器
计数器是一种最简略限流算法,其原理就是:在一段时间距离内,对申请进行计数,与阀值进行比拟判断是否须要限流,一旦到了工夫临界点,将计数器清零。
比方在 1 秒钟内对申请限度为 50 次
限流逻辑:1. 在程序中设置一个变量 count,当来一个申请我就将这个数 +1,同时记录申请工夫。2. 当下一个申请来的时候判断 count 的计数值是否超过设定的频次 50,以及以后申请的工夫和第一次申请工夫是否在 1 秒钟内。3. 如果在 1 秒钟内并且超过设定的频次则证实申请过多,前面的申请就回绝掉。4. 如果该申请与第一个申请的间隔时间大于计数周期,且 count 值还在限流范畴内,就重置 count。
有余:边界状况解决,假如有个用户在第 1 秒内的最初几毫秒霎时发送 40 个申请,当 1 秒完结后 counter 清零了,他在下一秒的前几毫秒时候又发送 40 个申请。相当于在间断的 1 秒内不止发送了 50 个申请,然而咱们的限流没限制住。
🌹滑动窗口
滑动窗口是针对计数器存在的临界点缺点,所谓滑动窗口(Sliding window)是一种流量控制技术,这个词呈现在 TCP 协定中。滑动窗口把固定工夫片进行划分,并且随着工夫的流逝,进行挪动,固定数量的能够挪动的格子,进行计数并判断阀值。
限流逻辑:1. 其实计数器就是滑动窗口,只不过只有一个窗格而已。2. 想让限流做的更准确只须要划分更多的窗格就能够了,为了更准确咱们也不晓得到底该设置多少个格子。3. 格子的数量影响着滑动窗口算法的精度,仍然有工夫片的概念,无奈基本解决临界点问题。
有余:无奈基本解决临界点问题。
🌼漏桶
漏桶算法(Leaky Bucket),原理就是一个固定容量的漏桶,依照固定速率流出水滴。
用过水龙头都晓得,关上龙头开关水就会流下滴到水桶里,而漏桶指的是水桶上面有个破绽能够出水, 如果水龙头开的特地大那么水流速就会过大,这样就可能导致水桶的水满了而后溢出。
漏桶算法有以下特点::1. 漏桶具备固定容量,出水速率是固定常量(流出申请)2. 如果桶是空的,则不需流出水滴
3. 能够以任意速率流入水滴到漏桶(流入申请)4. 如果流入水滴超出了桶的容量,则流入的水滴溢出(新申请被回绝)
有余:漏桶限度的是常量流出速率(即流出速率是一个固定常量值),所以最大的速率就是出水的速率,不能呈现突发流量。
🌻令牌桶
令牌桶算法(Token Bucket)是网络流量整形(Traffic Shaping)和速率限度(Rate Limiting)中最常应用的一种算法。典型状况下,令牌桶算法用来管制发送到网络上的数据的数目,并容许突发数据的发送。
漏桶算法有以下特点::1. 令牌按固定的速率被放入令牌桶中
2. 桶中最多寄存 B 个令牌,当桶满时,新增加的令牌被抛弃或回绝
3. 如果桶中的令牌有余 N 个,则不会删除令牌,且申请将被限流(抛弃或阻塞期待)
咱们有一个固定的桶,桶里寄存着令牌(token)。一开始桶是空的,零碎按固定的工夫(rate)往桶里增加令牌,直到桶里的令牌数满,多余的申请会被抛弃。当申请来的时候,从桶里移除一个令牌,如果桶是空的则拒绝请求或者阻塞。
容许肯定水平 突发流量,是比拟好的限流算法
🍊实现
下面介绍了限流算法,上面介绍几种常见限流算法的应用
🍇基于 redis 的计数器限流
定义限流注解
@Inherited
@Documented
@Target({ElementType.FIELD, ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AccessLimit {
/**
* 指定工夫范畴 申请次数
*/
int maxCount() default 50;
/**
* 申请次数的指定工夫范畴 秒数(redis 数据过期工夫)
*/
int second() default 1;}
限流切面
@Slf4j
@Aspect
@Component
public class AccessLimitAspect {@ApolloJsonValue("${app.service.limit.teacherId:[]}")
private List<String> teacherIds;
@Autowired
private RedissonClient redissonClient;
@Autowired
private HttpServletRequest httpServletRequest;
@SneakyThrows
@Around("@annotation(accessLimit)")
public Object doLimit(ProceedingJoinPoint proceedingJoinPoint, AccessLimit accessLimit) {String employeeId = httpServletRequest.getHeader("employeeId");
log.info("employeeId:{}", employeeId);
Assert.notNull(employeeId, "employeeId must not be null!");
if (teacherIds.contains(employeeId)) {log.info("request limit start, employeeId {}", employeeId);
// 获取注解内容信息
int seconds = accessLimit.second();
int maxCount = accessLimit.maxCount();
// 存储 key
String key = employeeId;
// 曾经拜访的次数
Integer count = redissonClient.get(key);
log.info("曾经拜访的次数:{}", count);
if (null == count || -1 == count) {redissonClient.set(key, 1, seconds, TimeUnit.SECONDS);
}
if (count < maxCount) {redissonClient.increment(key);
}
if (count >= maxCount) {log.warn("申请过于频繁请稍后再试");
return null;
}
log.info("request limit end, employeeId {}", employeeId);
}
return proceedingJoinPoint.proceed();}
}
🍍基于 redis 的令牌桶限流
定义限流注解
@Inherited
@Documented
@Target({ElementType.FIELD, ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AccessLimit {}
限流切面
@Slf4j
@Aspect
@Component
public class AccessLimitAspect {@ApolloJsonValue("${app.service.limit.teacherId:[]}")
private List<String> teacherIds;
@Autowired
private RedissonClient redissonClient;
@Autowired
private HttpServletRequest httpServletRequest;
@SneakyThrows
@Around("@annotation(accessLimit)")
public Object doLimit(ProceedingJoinPoint proceedingJoinPoint, AccessLimit accessLimit) {String employeeId = httpServletRequest.getHeader("employeeId");
log.info("employeeId:{}", employeeId);
Assert.notNull(employeeId, "employeeId must not be null!");
if (teacherIds.contains(employeeId)) {log.info("request limit start, employeeId {}", employeeId);
// 应用 redisson 限流器,每秒限度 50 次申请
RRateLimiter limiter = redissonClient.getRateLimiter("limit_teacher");
limiter.trySetRate(RateType.OVERALL, 50, 1, RateIntervalUnit.SECONDS);
limiter.acquire();
log.info("request limit end, employeeId {}", employeeId);
}
return proceedingJoinPoint.proceed();}
@Around("@within(cn.tinman.clouds.jojoread.admin.limit.AccessLimit)")
public Object limit(ProceedingJoinPoint joinPoint) {MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 办法上的降级注解优先于类上的
AccessLimit limit = AnnotationUtils.findAnnotation(signature.getMethod(), AccessLimit.class);
if (Objects.isNull(limit)) {limit = AnnotationUtils.findAnnotation(joinPoint.getTarget().getClass(), AccessLimit.class);
}
Assert.notNull(limit, "@AccessLimit must not be null!");
return doLimit(joinPoint, limit);
}
}
🍒基于 Guava 的令牌桶限流(单机)
限度 QPS 为 2,也就是每隔 500ms 生成一个令牌
RateLimiter rateLimiter = RateLimiter.create(2);
for (int i = 0; i < 10; i++) {String time = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_TIME);
System.out.println(time + ":" + rateLimiter.tryAcquire());
Thread.sleep(250);
}
程序每隔 250ms 获取一次令牌,所以两次获取中只有一次会胜利。
18:19:06.797557:true
18:19:07.061419:false
18:19:07.316283:true
18:19:07.566746:false
18:19:07.817035:true
18:19:08.072483:false
🍰总结
限流次要利用场景有:
- 电商零碎(特地是 6.18、双 11、双 12 等)中的秒杀流动,应用限流避免应用软件歹意刷单;
- 根底 api 接口限流:例如天气信息获取,IP 对应城市接口,百度、腾讯等对外提供的根底接口,都是通过限流来实现收费与付费间接的转换。
- 零碎宽泛调用的 api 接口,重大耗费网络、内存等资源,须要正当限流。
除了针对服务器进行限流,咱们也能够对容器进行限流,比方 Tomcat、Nginx 等限流伎俩。
- Tomcat 能够设置最大线程数(maxThreads),当并发超过最大线程数会排队期待执行;
- Nginx 提供了两种限流伎俩:一是管制速率,二是管制并发连接数。
限流算法
redis 实现限流
guava 的令牌桶限流