限流算法
2020-03-06 本文已影响0人
愤怒的老照
没有单位时间访问量要求
这种不需要考虑时间,只需要考虑线程安全就可以,用信号量可以解决
// 最大访问量为100;
int maxRequestNum = 100;
Semaphore handler = new Semaphore(maxRequestNum);
public void request(){
if (!handler.tryAcquire()){
return ;
}
// 调用接口
}
QPS限流(是一段时间内(一般指1秒)的请求个数。)
计数器法
这种方法是QPS中最简单的,以1秒为时间间隔,同一秒内请求数小于最大限制就可以执行,否则不执行,下一秒后清空请求数即可
// 最大访问量为100;
int maxRequestNum = 100;
Semaphore handler = new Semaphore(maxRequestNum);
long timestap = System.currentTimeMillis(); // 每个时间段的第一个时刻
long interval = 1000; // 时间间隔1000ms
public void request(){
long now = System.currentTimeMillis();
if (now < timestap + interval){
if (handler.tryAcquire()){
// 执行业务
}else {
return ;
}
}else {
// 初始化下一个时间段
timestap = now;
handler = new Semaphore(maxRequestNum);
// 执行业务
}
}
这种方法的缺点也是很明显的,粒度比较粗,假如一秒内的最大访问请求数量为100个,用户在第一毫秒就将这些请求全部使用,则这一秒内剩下的请求将全部不处理。
滑动窗口

这个是网上找的一个图,滑动窗口的思想就是将粒度细化,比如上图将1秒分为4分,每过0.25秒窗口像右滑动1/4,也就是每0.25秒腾出对应的访问数量。假如每秒最大访问量为100次,第1秒的后0.75秒和第二秒的前0.25秒访问量共100次,用计数法第二秒的后0.75秒将无法访问,如果使用滑动窗口的话,将会清空第一窗口的数量,使系统能再次访问。
// 最大访问量为100;
int maxRequestNum = 100;
AtomicInteger[] handlers = new AtomicInteger[4];
AtomicInteger sum;
int index = 0;
public void init(){
// 初始化
Arrays.stream(handlers)
.map(item -> {
return new AtomicInteger(0);
});
sum = new AtomicInteger(0);
}
public synchronized void request(){
// 对应窗口的访问量++;
handlers[index].incrementAndGet();
if (sum.incrementAndGet() < maxRequestNum){
// 执行业务
}else {
// 限流
}
}
//窗口滑动,每0.25秒执行一次
public void slide(){
// 窗口滑动
index = (index + 1) % handlers.length;
//对应窗口数量清空
int count = handlers[index].getAndSet(0);
// 窗口滑动,整体数减去窗口对应的数量
sum.addAndGet(count);
}
漏桶算法

这种方法是将水(请求)滴到桶中,桶中的水(请求)以固定速率漏出去,当水溢出来时,不处理请求,桶没有满时,处理请求。因为这种方法是固定速率的,所以没有计数器法中的边界。我们将算法中的水换成实际应用中的请求,我们可以看到漏桶算法天生就限制了请求的速度。
long timestamp = System.currentTimeMillis();
int capacity; // 桶的容量
int rate; // 水流出来的速率
long water; //桶中的水数量
public void request(){
long now = System.currentTimeMillis();
// 速率固定,可以使用相减来获取水的数量
water = Math.max(0, water - (now - timestamp) * rate);
if (water < capacity){
water ++;
// 执行请求
}else {
// 限流
}
}
令牌桶

为啥俺感觉这俩作用是一样的呢?网上说的令牌桶算法可以解决突发访问难道是加一个缓冲区???我来贴解释了https://www.cnblogs.com/xrq730/p/11025029.html
,可以使用Guava-RateLimiter
long timestamp = System.currentTimeMillis();
int capacity; //令牌桶容量
int rate; // 放入令牌的速率
long sum; //当前令牌的数量
public void request(){
long now = System.currentTimeMillis();
sum = Math.min(capacity, sum + (now - timestamp) * rate);
timestamp = now;
if (sum < 1){
// 限流
}else {
sum -= 1;
// 执行业务
}
}
实战
为了方便使用,通常封装成注解,使用切面来完成
Limit注解:
@Inherited
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Limit {
}
切面类
@Aspect
@Component
@Slf4j
public class LimitAspect {
private static final int limit = 100;
private ConcurrentHashMap<String, RateLimiter> handler = new ConcurrentHashMap<>();
@Pointcut("@annotation(com.whut.insurance.common.annotation.Limit)")
public void pointcut() {
}
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) {
HttpServletResponse response = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getResponse();
// 获取许可。如果有返回true,否则返回false
Signature sig = joinPoint.getSignature();
MethodSignature msig = (MethodSignature) sig;
Object target = joinPoint.getTarget();
boolean token = false;
try {
Method currentMethod = target.getClass().getMethod(msig.getName(), msig.getParameterTypes());
if (handler.containsKey(currentMethod.getName())){
token = handler.get(currentMethod.getName()).tryAcquire();
}else {
RateLimiter rateLimiter = RateLimiter.create(limit);
handler.put(currentMethod.getName(), rateLimiter);
token = rateLimiter.tryAcquire();
}
} catch (NoSuchMethodException e) {
log.error("get method error[{}]", e.getMessage());
}
System.out.println(handler.toString());
Object obj = null;
try {
if (token) {
obj = joinPoint.proceed();
} else {
ResponseWriter.println(response, ServerResponse.createByErrorCodeMessage(
ResponseCode.FREQUENT_VISIT.getCode(),
ResponseCode.FREQUENT_VISIT.getDesc()
));
}
} catch (Throwable e) {
log.error("get method error[{}]", e.getMessage());
}
return obj;
}
}
使用
@Limit
@PostMapping("/captcha")
public void captcha(@RequestBody @Valid RandomKeyParam randomKey, HttpServletResponse response) throws IOException, IOException {}