一、限流
为什么要进行限流?
1.瞬时流量过高,服务被压垮?
2.恶意用户高频光顾,导致服务器宕机?
3.消息消费过快,导致数据库压力过大,性能下降甚至崩溃?
什么是限流?
有哪些限流算法?
二、限流算法
1. 固定窗口
public class FixedWindowRateLimiter {
Logger logger = LoggerFactory.getLogger(FixedWindowRateLimiter.class);
//时间窗口大小,单位毫秒
long windowSize;
//允许通过的请求数
int maxRequestCount;
//当前窗口通过的请求数
AtomicInteger counter = new AtomicInteger(0);
//窗口右边界
long windowBorder;
public FixedWindowRateLimiter(long windowSize, int maxRequestCount) {
this.windowSize = windowSize;
this.maxRequestCount = maxRequestCount;
this.windowBorder = System.currentTimeMillis() + windowSize;
}
public synchronized boolean tryAcquire() {
long currentTime = System.currentTimeMillis();
if (windowBorder < currentTime) {
logger.info("window reset");
do {
windowBorder += windowSize;
} while (windowBorder < currentTime);
counter = new AtomicInteger(0);
}
if (counter.intValue() < maxRequestCount) {
counter.incrementAndGet();
logger.info("tryAcquire success");
return true;
} else {
logger.info("tryAcquire fail");
return false;
}
}
}
优点:实现简单,容易理解
1.限流不够平滑。例如:限流是每秒3个,在第一毫秒发送了3个请求,达到限流,窗口剩余时间的请求都将会被拒绝,体验不好。
2. 滑动窗口
1.把3秒钟划分为3个小窗,每个小窗限制请求不能超过50秒。
public class SlidingWindowRateLimiter {
Logger logger = LoggerFactory.getLogger(FixedWindowRateLimiter.class);
//时间窗口大小,单位毫秒
long windowSize;
//分片窗口数
int shardNum;
//允许通过的请求数
int maxRequestCount;
//各个窗口内请求计数
int[] shardRequestCount;
//请求总数
int totalCount;
//当前窗口下标
int shardId;
//每个小窗口大小,毫秒
long tinyWindowSize;
//窗口右边界
long windowBorder;
public SlidingWindowRateLimiter(long windowSize, int shardNum, int maxRequestCount) {
this.windowSize = windowSize;
this.shardNum = shardNum;
this.maxRequestCount = maxRequestCount;
this.shardRequestCount = new int[shardNum];
this.tinyWindowSize = windowSize / shardNum;
this.windowBorder = System.currentTimeMillis();
}
public synchronized boolean tryAcquire() {
long currentTime = System.currentTimeMillis();
if (windowBorder < currentTime) {
logger.info("window reset");
do {
shardId = (++shardId) % shardNum;
totalCount -= shardRequestCount[shardId];
shardRequestCount[shardId] = 0;
windowBorder += tinyWindowSize;
} while (windowBorder < currentTime);
}
if (totalCount < maxRequestCount) {
logger.info("tryAcquire success:{}", shardId);
shardRequestCount[shardId]++;
totalCount++;
return true;
} else {
logger.info("tryAcquire fail");
return false;
}
}
}
3. 漏桶算法
a.控制数据注入网络的速度。
a.一个固定容量的漏桶,按照固定速率出水(处理请求);
b.当流入水(请求数量)的速度过大会直接溢出(请求数量超过限制则直接拒绝)。
public class LeakyBucketRateLimiter {
Logger logger = LoggerFactory.getLogger(LeakyBucketRateLimiter.class);
//桶的容量
int capacity;
//桶中现存水量
AtomicInteger water = new AtomicInteger();
//开始漏水时间
long leakTimestamp;
//水流出的速率,即每秒允许通过的请求数
int leakRate;
public LeakyBucketRateLimiter(int capacity, int leakRate) {
this.capacity = capacity;
this.leakRate = leakRate;
}
public synchronized boolean tryAcquire() {
//桶中没有水, 重新开始计算
if (water.get() == 0) {
logger.info("start leaking");
leakTimestamp = System.currentTimeMillis();
water.incrementAndGet();
return water.get() < capacity;
}
//先漏水,计算剩余水量
long currentTime = System.currentTimeMillis();
int leakedWater = (int) ((currentTime - leakTimestamp) / 1000 * leakRate);
logger.info("lastTime:{}, currentTime:{}. LeakedWater:{}", leakTimestamp, currentTime, leakedWater);
//可能时间不足,则先不漏水
if (leakedWater != 0) {
int leftWater = water.get() - leakedWater;
//可能水已漏光。设为0
water.set(Math.max(0, leftWater));
leakTimestamp = System.currentTimeMillis();
}
logger.info("剩余容量:{}", capacity - water.get());
if (water.get() < capacity) {
logger.info("tryAcquire sucess");
water.incrementAndGet();
return true;
} else {
logger.info("tryAcquire fail");
return false;
}
}
}
1.平滑流量。由于漏桶算法以固定的速率处理请求,可以有效地平滑和整形流量,避免流量的突发和波动(类似于消息队列的削峰填谷的作用)。
1.无法处理突发流量:由于漏桶的出口速度是固定的,无法处理突发流量。例如,即使在流量较小的时候,也无法以更快的速度处理请求。
2.可能会丢失数据:如果入口流量过大,超过了桶的容量,那么就需要丢弃部分请求。在一些不能接受丢失请求的场景中,这可能是一个问题。
3.不适合速率变化大的场景:如果速率变化大,或者需要动态调整速率,那么漏桶算法就无法满足需求。
4.令牌算法
1.系统以固定的速率向桶中添加令牌;
2.当有请求到来时,会尝试从桶中移除一个令牌,如果桶中有足够的令牌,则请求可以被处理或数据包可以被发送;
3.如果桶中没有令牌,那么请求将被拒绝;
4.桶中的令牌数不能超过桶的容量,如果新生成的令牌超过了桶的容量,那么新的令牌会被丢弃。
1.可以处理突发流量:令牌桶算法可以处理突发流量。当桶满时,能够以最大速度处理请求。这对于需要处理突发流量的应用场景非常有用。
2.限制平均速率:在长期运行中,数据的传输率会被限制在预定义的平均速率(即生成令牌的速率)。
1.可能导致过载:如果令牌产生的速度过快,可能会导致大量的突发流量,这可能会使网络或服务过载。
2.需要存储空间:令牌桶需要一定的存储空间来保存令牌,可能会导致内存资源的浪费。
三、应用实践
1. 引入依赖
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.3-jre</version>
</dependency>
2. API直接使用
public void acquireTest() {
//每秒固定生成5个令牌
RateLimiter rateLimiter = RateLimiter.create(5);
for (int i = 0; i < 10; i++) {
double time = rateLimiter.acquire();
logger.info("等待时间:{}s", time);
}
}
结果:
public void acquireSmoothly() {
RateLimiter rateLimiter = RateLimiter.create(5, 3, TimeUnit.SECONDS);
long startTimeStamp = System.currentTimeMillis();
for (int i = 0; i < 15; i++) {
double time = rateLimiter.acquire();
logger.info("等待时间:{}s, 总时间:{}ms", time, System.currentTimeMillis() - startTimeStamp);
}
}
结果:
3.AOP切面
(RetentionPolicy.RUNTIME)
({ElementType.METHOD})
public Limit {
// 资源主键
String key() default "";
//最多访问次数,代表请求总数量
double permitsPerSeconds();
// 时间:即timeout时间内,只允许有permitsPerSeconds个请求总数量访问,超过的将被限制不能访问
long timeout();
//时间类型
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
//提示信息
String msg() default "系统繁忙,请稍后重试";
}
public class LimitAspect {
Logger logger = LoggerFactory.getLogger(LimitAspect.class);
private final Map<String, RateLimiter> limitMap = Maps.newConcurrentMap();
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
//拿limit的注解
Limit limit = method.getAnnotation(Limit.class);
if (limit != null) {
// key作用:不同的接口,不同的流量控制
String key = limit.key();
RateLimiter rateLimiter;
//验证缓存是否有命中key
if (!limitMap.containsKey(key)) {
//创建令牌桶
rateLimiter = RateLimiter.create(limit.permitsPerSeconds());
limitMap.put(key, rateLimiter);
logger.info("新建了令牌桶={},容量={}", key, limit.permitsPerSeconds());
}
rateLimiter = limitMap.get(key);
//拿令牌
boolean acquire = rateLimiter.tryAcquire(limit.timeout(), limit.timeUnit());
//拿不到令牌,直接返回异常信息
if (!acquire) {
logger.debug("令牌桶={},获取令牌失败", key);
throw new RuntimeException(limit.msg());
}
}
return joinPoint.proceed();
}
}
@Limit(key = "query",permitsPerSeconds = 1,timeout = 1,msg = "触发接口限流,请重试")
{
"timestamp": "2023-12-07 11:21:47",
"status": 500,
"error": "Internal Server Error",
"path": "/table/query"
}
若是放在service服务的接口上,返回如下
{
"code": -1,
"message": "触发接口限流,请重试",
"data": "fail"
}
四、总结
五、扩展资料
源码解析:高性能限流器Guava RateLimiter:https://zhuanlan.zhihu.com/p/358822328?utm_id=0
如果您发现该资源为电子书等存在侵权的资源或对该资源描述不正确等,可点击“私信”按钮向作者进行反馈;如作者无回复可进行平台仲裁,我们会在第一时间进行处理!
添加我为好友,拉您入交流群!
请使用微信扫一扫!