背景
前言
限流算法在分布式领域是一个经常被提起的话题,当系统的处理能力有限时, 如何阻止计划外的请求继续对系统施压,这是一个需要重视的问题。
如果没有限流机制,一旦外部请求超过系统承载的压力,就会出现系统宕机等严重问题。
加入限流正是为了保证系统负载在可以承受的范围内。
实际案例分析
比如双11的秒杀环节,我们在上线前预估了能应对的秒杀qps是 100w/s,但是实际可能达到了1000w/s,这种情况下这多出来的900w请求很可能压垮我们的数据库,甚至可能会导致集群雪崩,进而影响到接下来所有的用户正常访问。
常见的限流策略介绍
固定时间窗口算法
滑动时间窗口算法
漏桶算法
令牌桶算法
一、固定时间窗口算法
固定窗口算法又叫计数器算法,是一种简单方便的限流算法。
主要通过一个支持原子操作的计数器来累计n秒内的请求次数,当n秒内计数达到限流阈值时触发拒绝策略。
每过n秒,计数器重置为 0 开始重新计数。
优点
1、实现和理解起来比较简单,时间窗口是固定和隔离的,每个时间窗口都互不影响,性能也比较高。
缺点
1、正如图示一样,他的最大问题就是临界状态,在临界状态最坏情况会受到两倍流量请求。
2、还有一种是在一个单元时间窗内前期如果很快的消耗完请求阈值。那么剩下的时间将会无法请求。这样就会因为一瞬间的流量导致一段时间内系统不可用。这在互联网高可用的系统中是不能接受的。
二、滑动时间窗口算法
滑动窗口为固定窗口的改良版,解决了固定窗口在窗口切换时会受到两倍于阈值数量的请求,滑动窗口在固定窗口的基础上,将一个窗口分为若干个等份的小窗口,每个小窗口对应不同的时间点,拥有独立的计数器,当请求的时间点大于当前窗口的最大时间点时,则将窗口向前平移一个小窗口(将第一个小窗口的数据舍弃,第二个小窗口变成第一个小窗口,当前请求放在最后一个小窗口),整个窗口的所有请求数相加不能大于阈值。其中,Sentinel就是采用滑动窗口算法来实现限流的
- 把3秒钟划分为3个小窗,每个小窗限制请求不能超过50秒。
- 比如我们设置,3秒内不能超过150个请求,那么这个窗口就可以容纳3个小窗,并且随着时间推移,往前滑动。每次请求过来后,都要统计滑动窗口内所有小窗的请求总量。
优点
1、优化了固定时间窗口的临界问题,限流更加精准(窗口拆分的越小限流越精准)。
缺点
1、实现较为复杂,会占用较多内存,每个请求都需要重新统计最新时间窗口内的请求数,性能较低。
2、无法应对短时间高并发(突刺现象)。
三、漏桶算法
漏桶限流算法的核心就是:不管上面的水流速度有多块,漏桶水滴的流出速度始终保持不变。
假设限流要求是每分钟允许3w个请求,可以将漏桶的容积看作3w,每次请求加水量为1,漏桶中水的流出速度为3w/1分钟。
当出现最大3w个并发时,漏桶会被瞬间注满水,后续请求都会被拒绝。只有随着水量以固定速度流出后,漏斗中有剩余空间容纳新的水量,系统才能接受的新的请求。
优点
1、面对限流更加的柔性,不在粗暴的拒绝。
2、增加了接口的接收性。
3、保证下流服务接收的稳定性,均匀下发。
缺点
1、资源利用率低:漏桶并不能高效地利用可用的资源。因为它只在固定的时间间隔放行请求,所以在很多情况下,流量非常低,即使不存在资源争用,也无法有效地消耗资源。
2、饥饿问题:当短时间内有大量突发请求,即使服务器没有任何负载,由于漏桶中的水还没有流出,请求会大量被拒绝。
四、令牌桶算法
令牌桶是网络流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一种算法。对于每一个请求,都需要从令牌桶中获得一个令牌;如果没有获得令牌,则需要触发限流策略。系统会以恒定速度(r tokens/sec)往固定容量的令牌桶中放入令牌。令牌桶有固定的大小,如果令牌桶被填满,则会丢弃令牌。
会存在三种情况:
请求速度 >令牌生成速度:当令牌被取空后,会被限流。
请求速度=令牌生成速度:流量处于平稳状态。
请求速度<令牌生成速度:请求可被正常处理,桶满则丢弃令牌。
优点
1、和漏桶算法其实是一样的,不过优化了漏桶出水速度恒定的问题。可以适应突发流量。
缺点
1、系统刚上线就出现大量流量,那么可能会出现由于此时桶中还没有令牌,而导致请求被误杀的情况。(这种情况可以通过上线预热避免)
redis集成前准备
现在我们企业级应用一般用的比较多的是redis的哨兵模式(sentinel)和集群模式(cluster),我们此次专栏就以哨兵模式(sentinel)为例来实现这几种限流策略。
安装redis哨兵模式
SpringBoot3 集成redis
一、maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
二、application.yml配置
spring:
data:
redis:
sentinel:
master: redis_master
nodes: 127.0.0.1:26379,127.0.0.1:26380,127.0.0.1:26381
password: Java1104
lettuce:
pool:
max-active: 20
max-idle: 8
min-idle: 2
三、redisTemplate配置
package com.limit.rule.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @author shilei
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
后续代码实现会持续更新,敬请期待!!!