目录
1、限流算法
参考文档:常用4种限流算法介绍及比较
1.1 固定时间窗口算法
固定时间窗口算法是使用计数器在一个统计周期内累加访问次数,当达到设定的限流阈值时,触发限流策略。下一个统计周期开始时,进行清零,重新计数。
此算法在单机还是分布式环境下实现都非常简单,使用redis的incr原子自增性和线程安全即可轻松实现。这个算法通常用于QPS限流和统计总访问量,对于秒级以上的时间周期来说,
会存在一个非常严重的问题,那就是临界问题,如下图:
假设1min内服务器的负载能力为100,因此一个周期的访问量限制在100,然而在第一个周期的最后5秒和下一个周期的开始5秒时间段内,分别涌入100的访问量,虽然没有超过每个周期的限制量,
但是整体上10秒内已达到200的访问量,已远远超过服务器的负载能力,由此可见,计数器算法方式限流对于周期比较长的限流,存在很大的弊端。
基于Redis固定时间窗口限流算法实现(自定义实现):
// 知识点
@DistributedRateLimiter(permitsPerTimeUnit=3,timeUnit=TimeUnit.SECONDS,resourceId="knowledgePoint")
@RequestMapping("/knowledgePoint")
public ResultModel<List<ReportKnowledgePointAnalysisVo>> knowledgePoint(
@RequestBody String paramJson) {
//knowledgePointrateLimiter.acquire();
log.info("知识点 统计数据参数:{}", paramJson);
HashMap<String, Object> paramMap = JSON.parseObject(paramJson, new TypeReference<HashMap<String, Object>>() {
});
Long hwpId = DataUtils.toLong(paramMap.get("hwpId"));
Long stuId = DataUtils.toLong(paramMap.get("stuId"));
if (EmptyUtils.isEmpty(hwpId) || EmptyUtils.isEmpty(stuId)) {
return ResultModel.error_501("参数错误,hwpId或stuId为空");
}
try {
List<ReportKnowledgePointAnalysisVo> list = this.homeworkStuReportPointStatService
.queryStuKonwledgePointStatData(hwpId, stuId);
return ResultModel.ok_200("查询学生报告:知识点统计数据成功!", list);
} catch (Throwable e) {
log.error("异常", e);
String msg = ExceptionUtils.getMessage(e);
return ResultModel.error_501(msg);
}
}
package com.isatk.yn.aop;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import com.isatk.yn.constant.RedisKeyPrefixConst;
import com.isatk.yn.domain.DistributedRateLimiter;
import com.isatk.yn.module.common.service.RedisLockComponent;
import com.isatk.yn.module.reliablemsg.util.SpelUtil;
import com.isatk.yn.util.DateUtil;
import lombok.extern.slf4j.Slf4j;
/**
*
* @ClassName: DistributedRateLimiterAOP
* @Description: 分布分布式的限流切面,替代guava RateLimiter
* @author: QiaoLi
* @date: Aug 24, 2020 2:21:23 PM
*/
@Order(3)
@Slf4j
@Aspect
@Component
public class DistributedRateLimiterAOP {
@Autowired
public RedisTemplate<String, String> redisTemplate;
@Autowired
private RedisLockComponent redisLockComponent;
public DistributedRateLimiterAOP() {
}
@Around(value = "@annotation(com.isatk.yn.domain.DistributedRateLimiter)")
public Object doRateLimiter(ProceedingJoinPoint joinPoint) throws Throwable {
Object returnObject = null;
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
DistributedRateLimiter rateLimiter = methodSignature.getMethod()
.getDeclaredAnnotation(DistributedRateLimiter.class);
String resourceId = SpelUtil.parseSpel(method, joinPoint.getArgs(), rateLimiter.resourceId(), String.class,
rateLimiter.resourceId());
double permitsPerTimeUnit = rateLimiter.permitsPerTimeUnit();
double accuirePermits = rateLimiter.accuirePermits();
TimeUnit timeUnit = rateLimiter.timeUnit();
if (StringUtils.isEmpty(resourceId)) {
throw new RuntimeException("resourceId不能为空,请在注解中设置resourceId");
}
String timeStr = getTimeStr(timeUnit, new Date());
String timeStrKey = RedisKeyPrefixConst.DISTRIBUTED_RATE_LIMITER + "." + timeStr;
String timeStrLockKey = RedisKeyPrefixConst.DISTRIBUTED_RATE_LIMITER + "." + timeStr + "." + "lock";
try {
redisLockComponent.getLock(timeStrLockKey, 1, TimeUnit.SECONDS);
String actualPermitsPerTimeUnitStr = this.redisTemplate.opsForValue().get(timeStrKey);
if (StringUtils.isEmpty(actualPermitsPerTimeUnitStr)) {
this.redisTemplate.opsForValue().set(timeStrKey, "0", 3, TimeUnit.MINUTES);
actualPermitsPerTimeUnitStr = "0";
}
double actualPermitsPerTimeUnit = 0;
actualPermitsPerTimeUnit = Double.valueOf(actualPermitsPerTimeUnitStr);
if (actualPermitsPerTimeUnit >= permitsPerTimeUnit) {
log.warn("流量过大,请求被拦截,当前流量:{} ,最大流量:{}", actualPermitsPerTimeUnit, permitsPerTimeUnit);
throw new RuntimeException("流量过大,请求被拦截!");
} else {
log.info("当前流量:{} ,最大流量:{}", actualPermitsPerTimeUnit, permitsPerTimeUnit);
}
try {
returnObject = joinPoint.proceed();
} catch (Exception e) {
throw e;
} finally {
//多绑在10分钟,以方便观察数据
this.redisTemplate.opsForValue().set(timeStrKey,
String.valueOf(accuirePermits + actualPermitsPerTimeUnit),
600 + timeUnit.toSeconds(new Double(permitsPerTimeUnit).longValue()), timeUnit.SECONDS);
}
return returnObject;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
this.redisLockComponent.releaseLock(timeStrLockKey);
}
}
private static String getTimeStr(TimeUnit timeUnit, Date date) {
String timeStr = null;
if (TimeUnit.SECONDS == timeUnit) {
timeStr = DateUtil.getFormatTime(date, "yyyy-MM-dd HH:mm:ss");
} else if (TimeUnit.MINUTES == timeUnit) {
timeStr = DateUtil.getFormatTime(date, "yyyy-MM-dd HH:mm");
} else if (TimeUnit.HOURS == timeUnit) {
timeStr = DateUtil.getFormatTime(date, "yyyy-MM-dd HH");
} else if (TimeUnit.DAYS == timeUnit) {
timeStr = DateUtil.getFormatTime(date, "yyyy-MM-dd");
} else {
throw new IllegalArgumentException("非法timeUnit:" + timeUnit);
}
return timeStr;
}
}
1.2 滑动时间窗口算法
为了解决固定时间窗口算法的临界问题,引入了滑动时间窗口算法。滑动时间窗口算法是将时间周期分为N个小周期,分别记录每个小周期内访问次数,
并且根据时间滑动删除过期的小周期。如下图,假设时间周期为1min,将1min再分为2个小周期,统计每个小周期的访问数量,则可以看到,第一个时间周期内,
访问数量为75,第二个时间周期内,访问数量为100,超过100的访问则被限流掉了 。
1.3 令牌桶算法
令牌桶算法是程序以固定的速率向令牌桶中投入令牌,例如,限流QPS=10,则令牌桶算法每秒向令牌桶中投入10个令牌,当请示到达时,向令牌桶申请一个令牌,如果申请成功,
则处理请示,申请失败,执行限流策略。
1.4 漏桶算法
漏桶算法是指当请示到达时,如果此时漏桶已满,则执行限流策略;如果未满,则请示进入漏桶,并且漏桶以固定的速率让请求通过,直到漏桶为空。
1.5 各种算法对比
算法 |
确定参数 |
空间复杂度 |
时间复杂度 |
限制突发流量 |
平滑限流 |
分布式环境下实现难度 |
固定时间窗口 |
计数周期T、 周期内最大访问数N |
O(1) (记录周期内访问次数及周期开始时间) |
O(1) |
否 |
否 |
低 |
滑动时间窗口 |
计数周期T、 周期内最大访问数N |
O(N) (记录每个小周期中的访问数量) |
O(N) |
是 |
相对实现。滑动窗口的格子划分的越多, |
中 |
令牌桶 |
令牌产生速度r、令牌桶容量N |
O(1) (记录当前令牌桶中令牌数) |
O(N) |
是 |
是 |
高 |
漏桶 |
漏桶流出速度r、漏桶容量N |
O(1) (记录当前漏桶中容量) |
O(N) |
是 |
是 |
高 |
2、限流开源组件
2.1 Guava RateLimiter
Guava中的RateLimiter是基于令牌桶算法来实现限流的,它是单进程限流实现,无法对分布式系统进行限流。
在单体应用的具体实施方案参考:限流实战:guava的RateLimiter实现令牌桶算法限流
2.2 Netflix Hystrix
Netflix Hystrix是基于滑动时间窗口算法来实现限流的,它是单进程限流实现,无法对分布式系统进行限流。
在单体应用的具体实施方案参考:
@HystrixCommand(commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value = "3000")
},threadPoolProperties = {
//并发,缺省为10
@HystrixProperty(name = "coreSize", value = "2")
})
@RequestMapping ("/inventory-service/getStockNum")
public Integer getStockNum (@RequestBody PProduct product)
{
InventoryService inventoryService = InventoryServiceUtil.getInstance(product.getPublisherUserId());
if (inventoryService == null) {
log.error("InventoryService getInstance failed, 商品发布者用户为:{} 。", product.getPublisherUserId());
return DoubleClickInventoryServiceImpl.getInstance().getStockNum(product);
}
return inventoryService.getStockNum(product) == null ? 0 : inventoryService.getStockNum(product);
}
2.3 Alibaba Sentinel
2.4 限流开源组件对比
Sentinel | Hystrix | resilience4j | |
隔离策略 | 信号量隔离(并发线程数限流) | 线程池隔离/信号量隔离 | 信号量隔离 |
熔断降级策略 | 基于响应时间、异常比率、异常数 | 基于异常比率 | 基于异常比率、响应时间 |
实时统计实现 | 滑动窗口(LeapArray) | 滑动窗口(基于 RxJava) | Ring Bit Buffer |
动态规则配置 | 支持多种数据源 | 支持多种数据源 | 有限支持 |
扩展性 | 多个扩展点 | 插件的形式 | 接口的形式 |
基于注解的支持 | 支持 | 支持 | 支持 |
限流 | 基于 QPS,支持基于调用关系的限流 | 有限的支持 | Rate Limiter |
流量模式 | 支持预热模式、匀速器模式、预热排队模式 | 不支持 | 简单的 Rate Limiter 模式 |
系统自适应保护 | 支持 | 不支持 | 不支持 |
控制台 | 提供开箱即用的控制台,可配置规则、查看秒级监控、机器发现等 | 简单的监控查看 | 不提供控制台,可对接其它监控系统 |