限流
使用成本需要消耗成本,用户可能疯狂刷量
解决问题:
-
控制成本 => 限制用户调用总次数
-
限流 => 用户在短时间内疯狂发请求,导致服务器资源被占满,其他用户无法使用
限流阈值如何设置? => 参考正常用户的使用
限流的几种算法
-
固定窗口限流
维护一个计数器,将单位时间段作为一个窗口,计数器记录这个窗口的请求次数
-
当次数小于限流阈值 => 允许访问 => 计数器 + 1
-
次数大于阈值 => 拒绝访问
-
当前的时间窗口过去之后 => 计数器清零
缺点:
-
有临界问题 => 假设限流阈值为5,单位时间窗口是 1 小时,在当前时间窗口前 59 分内都没有用户发起请求,在最后一秒钟的时候,并发了 5 个请求,然后到了第二个时间窗口的第一秒钟的时候也并发了 5 个请求,这时就发生了一个问题 => 在这两秒钟之内并发了 10 个请求,就超过了一个小时之内只能处理 5 个请求的限制了。
-
-
滑动窗口问题
动窗口问题能解决固定窗口临界值的问题,将单位时间周期分为 n 个小周期,分别记录每个小周期内接口的访问次数,根据时间滑动删除过期的小周期
-
假设单位时间为 1 秒钟,限流阈值为 5,滑动窗口就把她划分为 5 个周期,滑动窗口(单位时间)被划分为 5 个小格子,每格表示 0.2 s,每过 0.2 s,时间窗口往右滑动一格,每个小周期,都有自己的计数器,如果请求是 在0.83 s 到达的,0.8 ~ 1.0 s对应的计数器就会加一
实现:
/** * 单位时间划分的小周期(单位时间是1分钟,10s一个小格子窗口,一共6个格子) */ private int SUB_CYCLE = 10; /** * 每分钟限流请求数 */ private int thresholdPerMin = 100; /** * 计数器, k-为当前窗口的开始时间值秒,value为当前窗口的计数 */ private final TreeMap<Long, Integer> counters = new TreeMap<>(); /** * 滑动窗口时间算法实现 */ boolean slidingWindowsTryAcquire() { long currentWindowTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) / SUB_CYCLE * SUB_CYCLE; //获取当前时间在哪个小周期窗口 int currentWindowNum = countCurrentWindow(currentWindowTime); //当前窗口总请求数 //超过阀值限流 if (currentWindowNum >= thresholdPerMin) { return false; } //计数器+1 counters.get(currentWindowTime)++; return true; } /** * 统计当前窗口的请求数 */ private int countCurrentWindow(long currentWindowTime) { //计算窗口开始位置 long startTime = currentWindowTime - SUB_CYCLE* (60s/SUB_CYCLE-1); int count = 0; //遍历存储的计数器 Iterator<Map.Entry<Long, Integer>> iterator = counters.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<Long, Integer> entry = iterator.next(); // 删除无效过期的子窗口计数器 if (entry.getKey() < startTime) { iterator.remove(); } else { //累加当前窗口的所有计数器之和 count =count + entry.getValue(); } } return count; }
-
-
漏桶算法
往漏桶了中以任意速率注入水,以固定的速率流出水,当水超过桶的容量时,就会溢出,即被丢弃。因为桶的容量不变 => 保证总体的速率
-
流入的水滴,可以看作是访问系统的请求,这个流入速率是不确定的。
-
桶的容量一般表示系统所能处理的请求数。
-
如果桶的容量满了,就达到限流的阀值,就会丢弃水滴(拒绝请求)
-
流出的水滴,是恒定过滤的,对应服务按照固定的速率处理请求。
缺点:
-
在正常流量的时候,系统按照固定的速率处理请求,但是在面对突发流量的时候,漏桶算法还是循规蹈矩地处理请求,但是我们在这时候希望系统尽快处理请求提高用户体验
/** * 每秒处理数(出水率) */ private long rate; /** * 当前剩余水量 */ private long currentWater; /** * 最后刷新时间 */ private long refreshTime; /** * 桶容量 */ private long capacity; /** * 漏桶算法 * @return */ boolean leakybucketLimitTryAcquire() { long currentTime = System.currentTimeMillis(); //获取系统当前时间 long outWater = (currentTime - refreshTime) / 1000 * rate; //流出的水量 =(当前时间-上次刷新时间)* 出水率 long currentWater = Math.max(0, currentWater - outWater); // 当前水量 = 之前的桶内水量-流出的水量 refreshTime = currentTime; // 刷新时间 // 当前剩余水量还是小于桶的容量,则请求放行 if (currentWater < capacity) { currentWater++; return true; } // 当前剩余水量大于等于桶的容量,限流 return false; }
-
-
令牌桶算法
在面对突发流量的时候,我们可以使用令牌桶算法
令牌桶算法的原理:
根据限流答小 => 定速往令牌桶里放令牌 => 系统接受到一个用户请求 => 区令牌桶里要一个令牌 => 拿到令牌 => 处理请求 => 拿不到令牌 => 拒绝请求
实现:
/** * 每秒处理数(放入令牌数量) */ private long putTokenRate; /** * 最后刷新时间 */ private long refreshTime; /** * 令牌桶容量 */ private long capacity; /** * 当前桶内令牌数 */ private long currentToken = 0L; /** * 漏桶算法 * @return */ boolean tokenBucketTryAcquire() { long currentTime = System.currentTimeMillis(); //获取系统当前时间 long generateToken = (currentTime - refreshTime) / 1000 * putTokenRate; //生成的令牌 =(当前时间-上次刷新时间)* 放入令牌的速率 currentToken = Math.min(capacity, generateToken + currentToken); // 当前令牌数量 = 之前的桶内令牌数量+放入的令牌数量 refreshTime = currentTime; // 刷新时间 //桶里面还有令牌,请求正常处理 if (currentToken > 0) { currentToken--; //令牌数量-1 return true; } return false; }
限流粒度
-
针对某个方法限流 => 单位时间内最多同时允许 XX 个操作使用这个方法
-
针对某个用户限流 => 单个用户单位时间内最多执行 XX 次操作
-
针对某个用户操作某个方法限流 => 单个用户单位时间内最多执行 XX 次这个方法
限流的实现
-
本地限流(单机限流)
每个服务器单独限流,一般适用于单体项目
Guava RateLimiter:
public static void main(String[] args) { // 创建 RedissonClient RedisClient redisson = RedisClient.create(); // 获取限流器 RSemaphore semaphore = redisson.getSemaphore("mySemaphore"); // 尝试获取许可证 boolean result = semphore.tryAcquire(); if (result) { // 处理请求 } else { // 超过流量限制,需要做何处理 } }
-
Redisson 限流实现
Redisson 内置了一个限流的工具类,可以帮助 利用 Redis 来存储、统计
-
引入 Redisson 依赖
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.21.3</version> </dependency>
-
创建 RedisConfig 配置类,用于初始化 RedissonCilent 对象单例
-
编写 RedistLimiterManager(专门提供 RedisLimiter 限流基础服务,提高了通用能力,可以放到任何一个项目里)
/** * @author zzt * 专门提高 RedisLimiter 限流基础服务器的(提供了通用的能力) * Redis 实现限流 */ @Service public class RedisLimiterManager { @Resource private RedissonClient redissonClient; /** * * @param key 区分不同的限流器,比如不同的用户 id 应该分别统计 */ public void doRateLimiter(String key) { //创建一个名为 user_limiter 的限流器,每秒最多访问 1 次 RRateLimiter rateLimiter = redissonClient.getRateLimiter(key); rateLimiter.trySetRate(RateType.OVERALL, 1, 1, RateIntervalUnit.SECONDS); // 来一次请求,则请求拿一个令牌 boolean canOperate = rateLimiter.tryAcquire(1); if (!canOperate) { throw new BusinessException(ErrorCode.TOO_MANY_REQUEST); } } }
-