限流专题研究

目录

1、限流算法

1.1 固定时间窗口算法

1.2 滑动时间窗口算法

1.3 令牌桶算法

1.4 漏桶算法

1.5 各种算法对比

2、限流开源组件

2.1 Guava RateLimiter

2.2 Netflix Hystrix

2.3 Alibaba Sentinel

2.4 限流开源组件对比


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 模式
系统自适应保护 支持 不支持

不支持

控制台 提供开箱即用的控制台,可配置规则、查看秒级监控、机器发现等 简单的监控查看 不提供控制台,可对接其它监控系统

猜你喜欢

转载自blog.csdn.net/s2008100262/article/details/112260166