欢迎加入【阿里云开发者公众号】读者群
阿里妹导读
一、限流
为什么要进行限流?
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;//可能水已漏光。设为0water.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. 引入依赖 2. API直接使用<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>32.1.3-jre</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);}}
结果:
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;//验证缓存是否有命中keyif (!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 欢迎加入【阿里云开发者公众号】读者群
微信扫一扫
关注该公众号