一般限流都是在网关这一层做,比如Nginx、Openresty、kong、zuul、Spring Cloud Gateway等;也可以在应用层通过Aop这种方式去做限流。
限流算法包含:计数器、固定窗口、滑动窗口、漏桶算法(Leaky Bucket)、令牌桶算法(Token Bucket)、分布式限流。
计数限流
例如系统能同时处理100个请求,保存一个计数器,处理了一个请求,计数器加一,一个请求处理完毕之后计数器减一。计数器单机可以使用Atomic原子类,分布式使用Redis。
缺点:突发流量顶不住。
固定窗口限流
在计数上主要多了时间窗口的概念。计数器每过一个时间窗口就重置。可以定义一个Aspect,传入maxCount和时间,在给定时间段内只能有maxCount个请求。
缺点:存在临界问题。即在窗口临界处有突发流量。
滑动窗口限流
相对于固定窗口,滑动窗口除了需要引入计数器之外还需要记录时间窗口内每个请求到达的时间点。每个请求的时间记录可以利用 Redis 的 zset
存储,利用ZREMRANGEBYSCORE
删除时间窗口之外的数据,再用 ZCARD
计数。
漏桶(Leaky Bucket)算法
水滴持续滴入漏桶中,底部定速流出。如果水滴滴入的速率大于流出的速率,当存水超过桶的大小的时候就会溢出。有点类似线程池。
缺点:不能面对突发流量时,加快系统响应速度
令牌桶(Token Bucket)算法
令牌桶是定速地往桶里塞入令牌,然后请求只有拿到了令牌才能通过,之后再被服务器处理。有点类似信号量。
限流组件
Nginx和阿里的 Sentinel 都实现了漏桶算法,SpringCloud Gateway和Guava Ratelimiter实现了令牌桶。
Nginx
Nginx 中的限流模块 limit_req_zone
Spring Cloud Gateway
定义了RateLimiter接口来达到限频效果,通过RedisRateLimiterFactory生成这个bean。通过Redis的Lua脚本来实现,该lua脚本基于令牌桶(Token Bucket)算法实现限频限流,支持从服务、用户、IP或自定义等维度限流。
使用Gateway则是配置文件:
server:
port: 12000
spring:
application:
name: gateway2
redis:
host: localhost
port: 6379
timeout: 6000ms # 连接超时时长(毫秒)
jedis:
pool:
max-active: 1000 # 连接池最大连接数(使用负值表示没有限制)
max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制)
max-idle: 10 # 连接池中的最大空闲连接
min-idle: 5 # 连接池中的最小空闲连接
cloud:
gateway:
discovery:
locator:
enabled: true # gateway可以通过开启以下配置来打开根据服务的serviceId来匹配路由,默认是大写
routes:
- id: limit_route
uri: lb://openapi
predicates:
- Path=/**
filters:
- name: RequestRateLimiter
args:
key-resolver: '#{@userKeyResolver}' #用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象。
redis-rate-limiter.replenishRate: 1 # 令牌桶每秒填充平均速率
redis-rate-limiter.burstCapacity: 1 #令牌桶总容量
在上面的配置文件,指定程序的端口为8081,配置了 redis的信息,并配置了RequestRateLimiter的限流过滤器,该过滤器需要配置三个参数:
- burstCapacity,令牌桶总容量。
- replenishRate,令牌桶每秒填充平均速率。
- key-resolver,用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象。
可以根据用户的key来进行限流,但是统一配置,并不能对单个用户进行
Guava RateLimiter
在一个指定的速率上分发许可(permit),当每次来请求的时候,线程会阻塞,直到获取到可用的permit,使用完这些permit之后不需要进行释放的操作。
// 通过工厂方法创建RateLimiter实例
RateLimiter rateLimiter = RateLimiter.create(2);
// 去获取permit
rateLimiter.acquire(1);
// 可以带上timeout
rateLimiter.tryAcquire(2, 10, TimeUnit.MILLISECONDS);
下面的程序将花费10s去执行。
@Test
public void givenLimitedResource_whenUseRateLimiter_thenShouldLimitPermits() {
// given
RateLimiter rateLimiter = RateLimiter.create(100);
// when
long startTime = ZonedDateTime.now().getSecond();
IntStream.range(0, 1000).forEach(i -> {
rateLimiter.acquire();
doSomeLimitedOperation();
});
long elapsedTimeSeconds = ZonedDateTime.now().getSecond() - startTime;
// then
assertThat(elapsedTimeSeconds >= 10);
}
java -Dserver.port=7070 -Dcsp.sentinel.dashboard.server=localhost:7070 -Dproject.name=sentinel-dashboard -Dsentinel.dashboard.auth.username=sentinel -Dsentinel.dashboard.auth.password=123456 -jar sentinel-dashboard-1.7.2.jar
Sentinel
####
SnowJena
基于令牌桶算法实现的分布式无锁限流框架,支持熔断降级,支持动态配置规则,支持可视化监控,开箱即用。
public class AppTest {
Logger logger = LoggerFactory.getLogger(getClass());
/**
* 分布式限流
*/
@Test
public void test4() throws InterruptedException {
// 1.限流配置
RateLimiterRule rateLimiterRule = new RateLimiterRuleBuilder()
.setApp("Application")
.setId("myId")
.setLimit(1) //每秒1个令牌
.setBatch(1) //每批次取1个令牌
.setLimiterModel(LimiterModel.CLOUD) //分布式限流,需启动TicketServer控制台
.build();
// 2.配置TicketServer地址(支持集群、加权重)
Map<String, Integer> map = new HashMap<>();
map.put("127.0.0.1:8521", 1);
// 3.全局配置
RateLimiterConfig config = RateLimiterConfig.getInstance();
config.setTicketServer(map);
// 4.工厂模式生产限流器
RateLimiter limiter = RateLimiterFactory.of(rateLimiterRule, config);
// 5.使用
while (true) {
if (limiter.tryAcquire()) {
logger.info("ok");
}
Thread.sleep(10);
}
}
}
需要进行限流配置,可视化界面支持对限流器进行配置。
syj-ratelimit
syj-ratelimit为提供了两个注解来进行限流,它们是@ClassRateLimit和@MethodRateLimit。顾名思义,它们一个是用在类上的一个是用在方法上的。他们的功能是一样的,之所以分出来两个注解的原因就是为了解决当一个类的不同接口需要进行不同的限流方案问题。
以@ClassRateLimit为例,使用固定窗口来进行限流。(可支持对用户进行限流)
public @interface ClassRateLimit {
/**
* 限流类型。默认值:ALL。可选值:ALL,IP,USER,CUSTOM
*/
public CheckTypeEnum checkType() default CheckTypeEnum.ALL;
/**
* 限流次数。默认值10
*/
public long limit() default 10;
/**
* 限流时间间隔,以秒为单位。默认值60
*/
public long refreshInterval() default 60;
}
distributed-limit
这个项目是一个Api限流的解决方案,采用的是令牌桶的方式。如果使用的Redis则是分布式限流,如果采用guava的LimitRater,则是本地限流。 分两个维度限流,一个是用户维度,一个Api维度,读者可自定义。
Controller上使用,基于注解、AOP
在Controller上加 @Limit注解,其中identifier为识别身份的,key为限流的key,limtNum为限制的次数,seconds为多少秒,后2个配置的作用是在多少秒最大的请求次数 。其中identifier和key支持Spel表达式。如果仅API维度,则identifier 为空即可;如果仅用户维度,key为空即可。(对用户级别的限流同样是采用固定窗口)
@RestController
public class TestController {
@GetMapping("/test")
@Limit(identifier = "forezp", key = "test", limtNum = 10, seconds = 1)
public String Test() {
return "11";
}
}
仅次操作就可以限流了。
另外如果是以注解的形式进行限流,如果以identifier即请求用户维度去限流,可以动态的设置的identifier的值,示例如下:
@Component
public class IndentifierInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取用户的信息,比如解析Token获取用户名,
// 这么做主要是在基于@Limit注解在Controller的时候,能都动态设置identifier信息
// 从而以用户维度进行限流
String identifier = "forezp";
IdentifierThreadLocal.set( identifier );
return true;
}
}
需求
- 所有用户有默认的流量控制规则,且可配置
- 可针对特定用户的特定产品进行流量控制规则配置
- 可视化配置和监控
方案
- 令牌桶的方案,使用redis实现分布式限流,限流器类
- 注解Limiter,拦截器动态地根据用户的请求参数切换到不同的限流器类(先鉴权)
- 可视化界面添加限流,修改限流
- 数据库限流表
- redis缓存,数据库数据一致
- QPS监控
guava 分布式